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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user