From fcacac1db49c3cfe8b1cde6c11d7f94f5e1bd647 Mon Sep 17 00:00:00 2001 From: vanch Date: Sun, 8 Feb 2026 22:44:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E6=B7=BB=E5=8A=A0=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=B8=AD=E6=96=87=E6=9C=AC=E5=9C=B0=E5=8C=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 i18n 配置和中英文翻译文件 (ui/i18n.ts, ui/locales/*.json) - 所有核心组件 i18n 化: - App.tsx - 音频播放错误消息 - Sidebar.tsx - 侧边栏导航 - CreatePanel.tsx - 创作面板 - LibraryView.tsx - 曲库视图 - Player.tsx - 播放器 - RightSidebar.tsx - 右侧边栏 - SettingsModal.tsx - 设置弹窗 - SongList.tsx - 歌曲列表 - SongProfile.tsx - 歌曲详情 - UserProfile.tsx - 用户资料 - TrainingPanel.tsx - 模型训练 - StemSplittingPanel.tsx - 音轨分离 - VoiceCloningPanel.tsx - 声音克隆 - MidiPanel.tsx - MIDI 工具 - SearchPage.tsx - 搜索页面 - 新增翻译模块: training/stem_splitting/voice_cloning/midi/search/console/genres/create_extra/vocal_languages - 支持浏览器语言自动检测和 localStorage 持久化 - 修复后端消息覆盖前端翻译的问题 --- .gitignore | 1 + ui/App.tsx | 30 +- ui/components/CreatePanel.tsx | 204 +++++----- ui/components/LibraryView.tsx | 18 +- ui/components/MidiPanel.tsx | 68 ++-- ui/components/Player.tsx | 39 +- ui/components/RightSidebar.tsx | 42 +- ui/components/SearchPage.tsx | 27 +- ui/components/SettingsModal.tsx | 148 +++---- ui/components/Sidebar.tsx | 25 +- ui/components/SongList.tsx | 51 ++- ui/components/SongProfile.tsx | 20 +- ui/components/StemSplittingPanel.tsx | 76 ++-- ui/components/TrainingPanel.tsx | 135 ++++--- ui/components/UserProfile.tsx | 56 +-- ui/components/VoiceCloningPanel.tsx | 114 +++--- ui/i18n.ts | 26 ++ ui/index.tsx | 1 + ui/locales/en.json | 574 +++++++++++++++++++++++++++ ui/locales/zh.json | 574 +++++++++++++++++++++++++++ ui/package-lock.json | 115 +++++- ui/package.json | 5 +- 22 files changed, 1832 insertions(+), 517 deletions(-) create mode 100644 ui/i18n.ts create mode 100644 ui/locales/en.json create mode 100644 ui/locales/zh.json diff --git a/.gitignore b/.gitignore index 3f63869..3c6b3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ venv_test/ venv/ env/ ENV/ +checkpoints/ # ---- CDMF runtime-heavy / user data (DO NOT COMMIT) ---- models/* diff --git a/ui/App.tsx b/ui/App.tsx index e3cd9a3..b8e83c5 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -23,9 +23,11 @@ import { TrainingPanel } from './components/TrainingPanel'; import { StemSplittingPanel } from './components/StemSplittingPanel'; import { VoiceCloningPanel } from './components/VoiceCloningPanel'; import { MidiPanel } from './components/MidiPanel'; +import { useTranslation } from 'react-i18next'; export default function App() { + const { t } = useTranslation(); // Responsive const { isMobile, isDesktop } = useResponsive(); @@ -436,9 +438,9 @@ export default function App() { if (audio.error && audio.error.code !== 1) { console.error("Audio playback error:", audio.error); if (audio.error.code === 4) { - showToast('This song is no longer available.', 'error'); + showToast(t('app.song_unavailable'), 'error'); } else { - showToast('Unable to play this song.', 'error'); + showToast(t('app.unable_to_play'), 'error'); } } setIsPlaying(false); @@ -476,7 +478,7 @@ export default function App() { if (err instanceof Error && err.name !== 'AbortError') { console.error("Playback failed:", err); if (err.name === 'NotSupportedError') { - showToast('This song is no longer available.', 'error'); + showToast(t('app.song_unavailable'), 'error'); } setIsPlaying(false); } @@ -725,7 +727,7 @@ export default function App() { if (activeJobsRef.current.has(job.jobId)) { console.warn(`Job ${job.jobId} timed out`); cleanupJob(job.jobId, tempId); - showToast('Generation timed out', 'error'); + showToast(t('app.generation_timed_out'), 'error'); } }, 600000); @@ -736,7 +738,7 @@ export default function App() { if (activeJobsRef.current.size === 0) { setIsGenerating(false); } - const msg = e instanceof Error ? e.message : 'Generation failed. Please try again.'; + const msg = e instanceof Error ? e.message : t('app.generation_failed', { error: 'Unknown' }); showToast(msg, 'error'); } }; @@ -839,7 +841,7 @@ export default function App() { const handleDeleteSong = async (song: Song) => { // Show confirmation dialog const confirmed = window.confirm( - `Are you sure you want to delete "${song.title}"? This action cannot be undone.` + t('app.confirm_delete_song', { title: song.title }) ); if (!confirmed) return; @@ -876,10 +878,10 @@ export default function App() { // Remove from play queue if present setPlayQueue(prev => prev.filter(s => s.id !== song.id)); - showToast('Song deleted successfully'); + showToast(t('app.song_deleted_success')); } catch (error) { console.error('Failed to delete song:', error); - showToast('Failed to delete song', 'error'); + showToast(t('app.delete_song_failed'), 'error'); } }; @@ -893,10 +895,10 @@ export default function App() { setSongToAddToPlaylist(null); playlistsApi.getMyPlaylists(token ?? undefined).then(r => setPlaylists(r.playlists)); } - showToast('Playlist created successfully!'); + showToast(t('app.playlist_created_success')); } catch (error) { console.error('Create playlist error:', error); - showToast('Failed to create playlist', 'error'); + showToast(t('app.playlist_create_failed'), 'error'); } }; @@ -910,11 +912,11 @@ export default function App() { try { await playlistsApi.addSong(playlistId, songToAddToPlaylist.id, token ?? ''); setSongToAddToPlaylist(null); - showToast('Song added to playlist'); + showToast(t('app.song_added_to_playlist')); playlistsApi.getMyPlaylists(token ?? undefined).then(r => setPlaylists(r.playlists)); } catch (error) { console.error('Add song error:', error); - showToast('Failed to add song to playlist', 'error'); + showToast(t('app.add_song_failed'), 'error'); } }; @@ -1083,7 +1085,7 @@ export default function App() { onClick={() => setMobileShowList(!mobileShowList)} className="bg-zinc-800 text-white px-4 py-2 rounded-full shadow-lg border border-white/10 flex items-center gap-2 text-sm font-bold" > - {mobileShowList ? 'Tools' : 'View List'} + {mobileShowList ? t('common.tools') : t('common.view_list')} @@ -1160,7 +1162,7 @@ export default function App() { onClick={() => setMobileShowList(!mobileShowList)} className="bg-zinc-800 text-white px-4 py-2 rounded-full shadow-lg border border-white/10 flex items-center gap-2 text-sm font-bold" > - {mobileShowList ? 'Create Song' : 'View List'} + {mobileShowList ? t('app.create_song') : t('common.view_list')} diff --git a/ui/components/CreatePanel.tsx b/ui/components/CreatePanel.tsx index b902906..606ac5f 100644 --- a/ui/components/CreatePanel.tsx +++ b/ui/components/CreatePanel.tsx @@ -3,6 +3,7 @@ import { Sparkles, ChevronDown, Settings2, Trash2, Music2, Sliders, Dices, Hash, import { GenerationParams, Song } from '../types'; import { useAuth } from '../context/AuthContext'; import { generateApi, preferencesApi, aceStepModelsApi, type LoraAdapter } from '../services/api'; +import { useTranslation } from 'react-i18next'; /** Tasks that require ACE-Step Base model only (see docs/ACE-Step-Tutorial.md). */ const TASKS_REQUIRING_BASE = ['lego', 'extract', 'complete'] as const; @@ -156,6 +157,7 @@ const VOCAL_LANGUAGES = [ type CreateMode = 'simple' | 'custom' | 'cover' | 'lego'; export const CreatePanel: React.FC = ({ onGenerate, isGenerating, initialData, onOpenSettings }) => { + const { t } = useTranslation(); const { isAuthenticated, token } = useAuth(); // Mode: simple | custom | cover | lego @@ -965,19 +967,19 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati onClick={() => { setCreateMode('simple'); setLegoValidationError(''); setCoverValidationError(''); }} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${createMode === 'simple' ? 'bg-white dark:bg-zinc-800 text-black dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-300'}`} > - Simple + {t('create.modes.simple')} @@ -999,13 +1001,13 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati {/* Title (same as Custom mode) */}
- Title + {t('create.inputs.title')}
setTitle(e.target.value)} - placeholder="Name your song" + placeholder={t('create.inputs.title_placeholder')} className="w-full bg-transparent p-3 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 focus:outline-none" />
@@ -1013,12 +1015,12 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati {/* Genre preset + Song Description */}
- Describe Your Song - + {t('create.inputs.describe_song')} +
- +