From f2a65024ad75f41f9eaab353cbd298b6b417a7bd Mon Sep 17 00:00:00 2001 From: bluntvoice Date: Mon, 2 Nov 2026 19:17:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E5=AE=8C=E5=96=84=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E7=95=8C=E9=9D=A2=E5=8F=8A=E5=8A=A8=E6=80=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用现有语言目录,补全游戏、快照、设备、云备份和活动界面的翻译。 同步更新简体、繁体与英文目录,保留状态标识、路径及同步数据格式。 为界面辅助函数提供即时翻译,并覆盖语言选择器及动态文案回归。 验证:前端 341 项测试及生产构建通过;翻译目录和静态引用校验通过。 --- cmd/opensave-app/frontend/src/App.svelte | 41 +- .../frontend/src/components/AboutModal.svelte | 18 +- .../src/components/CollectionsDialog.svelte | 20 +- .../src/components/CommandPalette.svelte | 50 +- .../frontend/src/components/DeviceIcon.svelte | 3 +- .../src/components/DiscordBanner.svelte | 8 +- .../src/components/DropOverlay.svelte | 6 +- .../src/components/NotificationHub.svelte | 32 +- .../src/components/PauseButton.svelte | 12 +- .../src/components/RestoreDialog.svelte | 24 +- .../src/components/ShortcutsSheet.svelte | 8 +- .../frontend/src/components/Sidebar.svelte | 20 +- .../frontend/src/components/StatusBar.svelte | 28 +- .../frontend/src/components/TitleBar.svelte | 8 +- .../src/components/TransfersPanel.svelte | 16 +- .../src/components/WhatsNewModal.svelte | 14 +- .../frontend/src/components/ui/Modal.svelte | 4 +- .../src/components/ui/MoreInfo.svelte | 3 +- .../src/components/ui/SearchInput.svelte | 3 +- .../src/components/ui/Skeleton.svelte | 4 +- .../frontend/src/lib/cloudproviders.js | 13 +- .../frontend/src/lib/collections.js | 3 +- cmd/opensave-app/frontend/src/lib/emptied.js | 5 +- cmd/opensave-app/frontend/src/lib/format.js | 7 +- .../frontend/src/lib/gameactions.js | 57 +- .../frontend/src/lib/gamestatus.js | 72 +- .../frontend/src/lib/i18n-components.test.js | 26 + .../frontend/src/lib/i18n-ui.test.js | 42 + cmd/opensave-app/frontend/src/lib/i18n.js | 7 +- .../frontend/src/lib/libraryview.js | 21 +- .../frontend/src/lib/notifications.js | 39 +- .../frontend/src/lib/notifyprefs.js | 13 +- cmd/opensave-app/frontend/src/lib/overview.js | 3 +- cmd/opensave-app/frontend/src/lib/restore.js | 7 +- cmd/opensave-app/frontend/src/lib/scan.js | 19 +- .../frontend/src/lib/snapshots.js | 29 +- cmd/opensave-app/frontend/src/lib/stores.js | 47 +- .../frontend/src/lib/syncpause.js | 29 +- cmd/opensave-app/frontend/src/lib/timeago.js | 19 +- cmd/opensave-app/frontend/src/lib/timeline.js | 35 +- .../frontend/src/lib/transfers.js | 5 +- cmd/opensave-app/frontend/src/lib/undo.js | 3 +- cmd/opensave-app/frontend/src/locales/en.json | 938 +++++++++++++++++- .../frontend/src/locales/zh-Hans.json | 917 ++++++++++++++++- .../frontend/src/locales/zh-Hant.json | 917 ++++++++++++++++- cmd/opensave-app/frontend/src/test/setup.js | 4 + .../frontend/src/views/ActivityLog.svelte | 29 +- .../frontend/src/views/Changelog.svelte | 9 +- .../frontend/src/views/CloudBackup.svelte | 56 +- .../frontend/src/views/Devices.svelte | 73 +- .../frontend/src/views/GameDetail.svelte | 17 +- .../frontend/src/views/InternetSync.svelte | 106 +- .../src/views/activity/Timeline.svelte | 25 +- .../src/views/cloud/CloudBrowser.svelte | 69 +- .../src/views/cloud/ExportDialog.svelte | 32 +- .../src/views/cloud/ImportDialog.svelte | 34 +- .../src/views/cloud/OAuthConnect.svelte | 40 +- .../src/views/cloud/OwnOAuthApp.svelte | 40 +- .../src/views/cloud/ProviderFields.svelte | 17 +- .../src/views/cloud/ProviderPicker.svelte | 7 +- .../frontend/src/views/game/AppIdField.svelte | 22 +- .../src/views/game/BranchesTab.svelte | 26 +- .../frontend/src/views/game/CloudTab.svelte | 30 +- .../src/views/game/CompareDialog.svelte | 15 +- .../frontend/src/views/game/ConfigTab.svelte | 70 +- .../frontend/src/views/game/GameHeader.svelte | 109 +- .../frontend/src/views/game/LinkPicker.svelte | 9 +- .../frontend/src/views/game/ManageTab.svelte | 36 +- .../src/views/game/NewBranchDialog.svelte | 24 +- .../src/views/game/SaveLocations.svelte | 29 +- .../src/views/game/SnapshotsTab.svelte | 63 +- .../src/views/game/SyncIgnoreEditor.svelte | 36 +- .../src/views/home/AddGameCard.svelte | 28 +- .../src/views/home/HomeSummary.svelte | 42 +- .../src/views/home/LibraryGrid.svelte | 72 +- .../src/views/home/LibraryTile.svelte | 2 +- .../src/views/home/LibraryViewOptions.svelte | 20 +- .../src/views/home/RecentActivity.svelte | 8 +- .../frontend/src/views/home/ScanDialog.svelte | 70 +- .../frontend/src/views/home/SetupGuide.svelte | 24 +- .../src/views/home/TrackGameDialog.svelte | 62 +- .../views/settings/AppearanceOptions.svelte | 20 +- .../views/settings/ControllerOptions.svelte | 33 +- .../views/settings/NotificationOptions.svelte | 31 +- .../src/views/settings/SnapshotChecks.svelte | 70 +- .../src/views/settings/StorageUsage.svelte | 42 +- cmd/opensave-app/frontend/vite.config.js | 4 + 87 files changed, 3987 insertions(+), 1163 deletions(-) create mode 100644 cmd/opensave-app/frontend/src/lib/i18n-components.test.js create mode 100644 cmd/opensave-app/frontend/src/lib/i18n-ui.test.js create mode 100644 cmd/opensave-app/frontend/src/test/setup.js diff --git a/cmd/opensave-app/frontend/src/App.svelte b/cmd/opensave-app/frontend/src/App.svelte index cce5ee7d..c8b55879 100644 --- a/cmd/opensave-app/frontend/src/App.svelte +++ b/cmd/opensave-app/frontend/src/App.svelte @@ -1,4 +1,5 @@ {#if game} - + {game.name}
    @@ -82,22 +82,22 @@ {c.gameIds.length} {#if !c.builtin} - - + + {/if} {/if} {/each}
- - + +
-

{$t('collections.hint')}

+

{$translateUI('collections.hint')}

- +
{/if} diff --git a/cmd/opensave-app/frontend/src/components/CommandPalette.svelte b/cmd/opensave-app/frontend/src/components/CommandPalette.svelte index 9ad15c8e..2bd39829 100644 --- a/cmd/opensave-app/frontend/src/components/CommandPalette.svelte +++ b/cmd/opensave-app/frontend/src/components/CommandPalette.svelte @@ -13,7 +13,7 @@ import { showSetupAgain } from '../lib/setup.js'; import { appearance, resolvedTheme } from '../lib/appearance.js'; import { PAGES } from '../lib/shortcuts.js'; - import { t } from '../lib/i18n.js'; + import { t as translateUI } from '../lib/i18n.js'; import { rank, snapshotEntries } from '../lib/palette.js'; const dispatch = createEventDispatcher(); @@ -29,21 +29,21 @@ const isDark = () => resolvedTheme($appearance.theme, globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true) === 'dark'; $: entries = [ - ...PAGES.map((p) => ({ label: $t('palette.goTo', { page: p.label }), kind: 'Page', idle: true, keywords: [p.id], run: () => navigate(p.id) })), - { label: $t('palette.syncAll'), kind: 'Action', idle: true, run: syncAll }, - { label: $t('palette.snapshotAll'), kind: 'Action', idle: true, keywords: ['backup', 'save', 'all'], run: snapshotAll }, - { label: $t('home.welcome.scanForSaves'), kind: 'Action', idle: true, keywords: ['find', 'detect', 'auto-scan'], run: () => navigate('home', { scan: Date.now() }) }, - { label: $t('palette.trackFolder'), kind: 'Action', idle: true, keywords: ['add', 'game'], run: () => navigate('home', { add: true }) }, + ...PAGES.map((p) => ({ label: $translateUI('palette.goTo', { page: p.label }), kind: 'Page', idle: true, keywords: [p.id], run: () => navigate(p.id) })), + { label: $translateUI('palette.syncAll'), kind: 'Action', idle: true, run: syncAll }, + { label: $translateUI('palette.snapshotAll'), kind: 'Action', idle: true, keywords: ['backup', 'save', 'all'], run: snapshotAll }, + { label: $translateUI('home.welcome.scanForSaves'), kind: 'Action', idle: true, keywords: ['find', 'detect', 'auto-scan'], run: () => navigate('home', { scan: Date.now() }) }, + { label: $translateUI('palette.trackFolder'), kind: 'Action', idle: true, keywords: ['add', 'game'], run: () => navigate('home', { add: true }) }, { - label: isDark() ? $t('palette.toLight') : $t('palette.toDark'), + label: isDark() ? $translateUI('palette.toLight') : $translateUI('palette.toDark'), kind: 'Action', idle: true, keywords: ['theme', 'appearance', 'mode'], run: () => appearance.update((a) => ({ ...a, theme: isDark() ? 'light' : 'dark' })) }, - { label: $t('shortcuts.title'), kind: 'Help', idle: true, keywords: ['keys', 'help'], run: () => dispatch('help') }, + { label: $translateUI('shortcuts.title'), kind: 'Help', idle: true, keywords: ['keys', 'help'], run: () => dispatch('help') }, { - label: $t('palette.showSetup'), + label: $translateUI('palette.showSetup'), kind: 'Help', keywords: ['tutorial', 'getting started', 'onboarding', 'help', 'checklist'], run: () => { @@ -52,9 +52,9 @@ } }, ...($syncPause.paused - ? [{ label: $t('pause.resume'), kind: 'Action', idle: true, keywords: ['unpause', 'start'], run: resumeSync }] + ? [{ label: $translateUI('pause.resume'), kind: 'Action', idle: true, keywords: ['unpause', 'start'], run: resumeSync }] : PAUSE_CHOICES.map((c) => ({ - label: $t('palette.pauseFor', { when: c.label }), + label: $translateUI('palette.pauseFor', { when: c.label }), kind: 'Action', keywords: ['stop', 'hold', 'offline'], run: () => pauseSync(c.minutes) @@ -62,7 +62,7 @@ ...$collections .filter((c) => c.gameIds.length) .map((c) => ({ - label: $t('palette.show', { name: c.name }), + label: $translateUI('palette.show', { name: c.name }), kind: 'Collection', idle: c.builtin, keywords: ['collection', 'filter'], @@ -73,11 +73,11 @@ })), ...$visibleGames.flatMap((g) => [ { label: g.name, kind: 'Game', idle: true, weight: 1, run: () => runGameAction('open', g) }, - { label: $t('palette.syncGame', { game: g.name }), kind: 'Action', weight: -1, run: () => runGameAction('sync', g) }, - { label: $t('palette.snapshotGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['backup', 'save'], run: () => runGameAction('snapshot', g) }, - ...(canLaunch(g) ? [{ label: $t('palette.launchGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['play', 'start'], run: () => runGameAction('launch', g) }] : []), - { label: $t('palette.openSaveFolder', { game: g.name }), kind: 'Action', weight: -1, keywords: ['explorer', 'files'], run: () => runGameAction('folder', g) }, - { label: $t('palette.renameGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['name', 'title'], run: () => runGameAction('rename', g) } + { label: $translateUI('palette.syncGame', { game: g.name }), kind: 'Action', weight: -1, run: () => runGameAction('sync', g) }, + { label: $translateUI('palette.snapshotGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['backup', 'save'], run: () => runGameAction('snapshot', g) }, + ...(canLaunch(g) ? [{ label: $translateUI('palette.launchGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['play', 'start'], run: () => runGameAction('launch', g) }] : []), + { label: $translateUI('palette.openSaveFolder', { game: g.name }), kind: 'Action', weight: -1, keywords: ['explorer', 'files'], run: () => runGameAction('folder', g) }, + { label: $translateUI('palette.renameGame', { game: g.name }), kind: 'Action', weight: -1, keywords: ['name', 'title'], run: () => runGameAction('rename', g) } ]), // Snapshots with words of their own — a note, or a comment someone typed // — found by those words: "boss" finds "Before the final boss". @@ -92,8 +92,8 @@ const res = await api.post('/api/snapshots/all', {}); toast( res.failed?.length - ? $t('palette.snapshotAllPartial', { n: res.taken, failed: res.failed.length }) - : $t('palette.snapshotAllDone', { n: res.taken }), + ? $translateUI('palette.snapshotAllPartial', { n: res.taken, failed: res.failed.length }) + : $translateUI('palette.snapshotAllDone', { n: res.taken }), res.failed?.length ? 'error' : 'success' ); } catch (e) { @@ -103,7 +103,7 @@ async function syncAll() { for (const g of $visibleGames) api.post(`/api/games/${g.id}/sync`).catch(() => {}); - toast($t('home.syncAllToast')); + toast($translateUI('home.syncAllToast')); } function choose(entry) { @@ -137,14 +137,14 @@