Add i18n system with 4-language support (en/zh/ja/ko)

- Add I18nContext provider with useI18n hook and localStorage persistence
- Add translations.ts with complete strings for English, Chinese, Japanese, Korean
- Wrap App in I18nProvider, replace hardcoded toast messages with t() calls
- Add language selector to SettingsModal with Globe icon
- Add localization credits in About section
- Default language: English
- Add I18N_USAGE.md documentation
This commit is contained in:
fspecii
2026-02-08 18:19:08 +02:00
parent af5343f9db
commit c219edb037
5 changed files with 2591 additions and 60 deletions
+30 -18
View File
@@ -15,13 +15,17 @@ import { Song, GenerationParams, View, Playlist } from './types';
import { generateApi, songsApi, playlistsApi, getAudioUrl } from './services/api';
import { useAuth } from './context/AuthContext';
import { useResponsive } from './context/ResponsiveContext';
import { I18nProvider, useI18n } from './context/I18nContext';
import { List } from 'lucide-react';
import { PlaylistDetail } from './components/PlaylistDetail';
import { Toast, ToastType } from './components/Toast';
import { SearchPage } from './components/SearchPage';
export default function App() {
function AppContent() {
// i18n
const { t } = useI18n();
// Responsive
const { isMobile, isDesktop } = useResponsive();
@@ -464,9 +468,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('songNotAvailable'), 'error');
} else {
showToast('Unable to play this song.', 'error');
showToast(t('unableToPlay'), 'error');
}
}
setIsPlaying(false);
@@ -502,7 +506,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('songNotAvailable'), 'error');
}
setIsPlaying(false);
}
@@ -631,7 +635,7 @@ export default function App() {
} else if (status.status === 'failed') {
cleanupJob(jobId, tempId);
console.error(`Job ${jobId} failed:`, status.error);
showToast(`Generation failed: ${status.error || 'Unknown error'}`, 'error');
showToast(`${t('generationFailed')}: ${status.error || 'Unknown error'}`, 'error');
}
} catch (pollError) {
console.error(`Polling error for job ${jobId}:`, pollError);
@@ -646,7 +650,7 @@ export default function App() {
if (activeJobsRef.current.has(jobId)) {
console.warn(`Job ${jobId} timed out`);
cleanupJob(jobId, tempId);
showToast('Generation timed out', 'error');
showToast(t('generationTimedOut'), 'error');
}
}, 600000);
}, [token, cleanupJob, refreshSongsList]);
@@ -762,7 +766,7 @@ export default function App() {
if (activeJobsRef.current.size === 0) {
setIsGenerating(false);
}
showToast('Generation failed. Please try again.', 'error');
showToast(t('generationFailed'), 'error');
}
};
@@ -946,10 +950,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('songDeleted'));
} catch (error) {
console.error('Failed to delete song:', error);
showToast('Failed to delete song', 'error');
showToast(t('failedToDeleteSong'), 'error');
}
};
@@ -1001,9 +1005,9 @@ export default function App() {
}
if (failed.length > 0) {
showToast(`Deleted ${succeeded.length}/${songsToDelete.length} songs`, 'error');
showToast(t('songsDeletedPartial').replace('{succeeded}', String(succeeded.length)).replace('{total}', String(songsToDelete.length)), 'error');
} else {
showToast('Songs deleted successfully');
showToast(t('songsDeletedSuccess'));
}
};
@@ -1020,10 +1024,10 @@ export default function App() {
throw new Error('Failed to delete upload');
}
setReferenceTracks(prev => prev.filter(track => track.id !== trackId));
showToast('Upload deleted successfully');
showToast(t('songDeleted'));
} catch (error) {
console.error('Failed to delete upload:', error);
showToast('Failed to delete upload', 'error');
showToast(t('failedToDeleteSong'), 'error');
}
};
@@ -1038,10 +1042,10 @@ export default function App() {
setSongToAddToPlaylist(null);
playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists));
}
showToast('Playlist created successfully!');
showToast(t('playlistCreated'));
} catch (error) {
console.error('Create playlist error:', error);
showToast('Failed to create playlist', 'error');
showToast(t('failedToCreatePlaylist'), 'error');
}
};
@@ -1055,11 +1059,11 @@ export default function App() {
try {
await playlistsApi.addSong(playlistId, songToAddToPlaylist.id, token);
setSongToAddToPlaylist(null);
showToast('Song added to playlist');
showToast(t('songAddedToPlaylist'));
playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists));
} catch (error) {
console.error('Add song error:', error);
showToast('Failed to add song to playlist', 'error');
showToast(t('failedToAddSong'), 'error');
}
};
@@ -1286,7 +1290,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('createSong') : t('viewList')}
<List size={16} />
</button>
</div>
@@ -1415,3 +1419,11 @@ export default function App() {
</div>
);
}
export default function App() {
return (
<I18nProvider>
<AppContent />
</I18nProvider>
);
}