From e74ba3ce8380ad99b914bdd1ae23df8b1abb03b0 Mon Sep 17 00:00:00 2001 From: vanch Date: Mon, 9 Feb 2026 11:55:16 +0800 Subject: [PATCH] feat(i18n): add Chinese/English internationalization support Add i18next with browser language detection, locale files (en/zh), and replace all hardcoded strings across 15 components with t() calls. Includes language switcher UI in Settings modal. Co-Authored-By: Claude --- App.tsx | 4 +- components/CreatePanel.tsx | 292 ++++++------ components/LibraryView.tsx | 30 +- components/Player.tsx | 691 +++++++++++++---------------- components/PlaylistDetail.tsx | 31 +- components/PlaylistModals.tsx | 27 +- components/RightSidebar.tsx | 54 +-- components/SearchPage.tsx | 20 +- components/SettingsModal.tsx | 52 ++- components/Sidebar.tsx | 17 +- components/SongDropdownMenu.tsx | 22 +- components/SongList.tsx | 66 +-- components/SongProfile.tsx | 22 +- components/UserProfile.tsx | 52 +-- components/UsernameModal.tsx | 18 +- components/VideoGeneratorModal.tsx | 668 ++++++++++++---------------- i18n.ts | 26 ++ index.tsx | 1 + locales/en.json | 379 ++++++++++++++++ locales/zh.json | 378 ++++++++++++++++ package-lock.json | 110 ++++- package.json | 5 +- 22 files changed, 1875 insertions(+), 1090 deletions(-) create mode 100644 i18n.ts create mode 100644 locales/en.json create mode 100644 locales/zh.json diff --git a/App.tsx b/App.tsx index 0f8bdfd..b8ee548 100644 --- a/App.tsx +++ b/App.tsx @@ -19,11 +19,13 @@ import { List } from 'lucide-react'; import { PlaylistDetail } from './components/PlaylistDetail'; import { Toast, ToastType } from './components/Toast'; import { SearchPage } from './components/SearchPage'; +import { useTranslation } from 'react-i18next'; export default function App() { // Responsive const { isMobile, isDesktop } = useResponsive(); + const { t } = useTranslation(); // Auth const { user, token, isAuthenticated, isLoading: authLoading, setupUser, logout } = useAuth(); @@ -1286,7 +1288,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('common.createSong') : t('common.viewList')} diff --git a/components/CreatePanel.tsx b/components/CreatePanel.tsx index a041000..1a093fd 100644 --- a/components/CreatePanel.tsx +++ b/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 } from '../services/api'; +import { useTranslation } from 'react-i18next'; interface ReferenceTrack { id: string; @@ -110,6 +111,7 @@ export const CreatePanel: React.FC = ({ onAudioSelectionApplied, }) => { const { isAuthenticated, token, user } = useAuth(); + const { t } = useTranslation(); // Mode const [customMode, setCustomMode] = useState(true); @@ -288,16 +290,6 @@ export const CreatePanel: React.FC = ({ const handleMouseMove = (e: MouseEvent) => { if (!isResizing) return; - // Calculate new height based on mouse position relative to the lyrics container top - // We can't easily get the container top here without a ref to it, - // but we can use dy (delta y) from the previous position if we tracked it, - // OR simpler: just update based on movement if we track the start. - // - // Better approach for absolute sizing: - // 1. Get the bounding rect of the textarea wrapper on mount/resize start? - // We can just rely on the fact that we are dragging the bottom. - // So new height = currentMouseY - topOfElement. - if (lyricsRef.current) { const rect = lyricsRef.current.getBoundingClientRect(); const newHeight = e.clientY - rect.top; @@ -835,12 +827,12 @@ export const CreatePanel: React.FC = ({ )}
- {dragKind === 'audio' ? 'Drop to use audio' : 'Drop to upload'} + {dragKind === 'audio' ? t('create.custom.audio.dropUse') : t('create.custom.audio.dropUpload')}
{dragKind === 'audio' - ? `Using as ${audioTab === 'reference' ? 'Reference' : 'Cover'}` - : `Uploading as ${audioTab === 'reference' ? 'Reference' : 'Cover'}`} + ? (audioTab === 'reference' ? t('create.custom.audio.usingRef') : t('create.custom.audio.usingCover')) + : (audioTab === 'reference' ? t('create.custom.audio.usingRef') : t('create.custom.audio.usingCover'))}
@@ -892,13 +884,13 @@ export const CreatePanel: React.FC = ({ onClick={() => setCustomMode(false)} className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${!customMode ? '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.mode.simple')} @@ -909,12 +901,12 @@ export const CreatePanel: React.FC = ({ {/* Song Description */}
- Describe Your Song + {t('create.simple.describeTitle')}