diff --git a/cmd/opensave-app/frontend/src/components/AboutModal.svelte b/cmd/opensave-app/frontend/src/components/AboutModal.svelte index 6700d08a..6f366f86 100644 --- a/cmd/opensave-app/frontend/src/components/AboutModal.svelte +++ b/cmd/opensave-app/frontend/src/components/AboutModal.svelte @@ -5,6 +5,7 @@ import { navigate } from '../lib/stores.js'; import { backdropClose } from '../lib/backdrop.js'; import { DISCORD_URL, GITHUB_URL } from '../lib/links.js'; + import { t } from '../lib/i18n.js'; import logoUrl from '../assets/logo.png'; import X from 'lucide-svelte/icons/x'; import ExternalLink from 'lucide-svelte/icons/external-link'; @@ -39,18 +40,18 @@ diff --git a/cmd/opensave-app/frontend/src/components/CollectionsDialog.svelte b/cmd/opensave-app/frontend/src/components/CollectionsDialog.svelte index 37bf0f33..e3bda82b 100644 --- a/cmd/opensave-app/frontend/src/components/CollectionsDialog.svelte +++ b/cmd/opensave-app/frontend/src/components/CollectionsDialog.svelte @@ -9,6 +9,7 @@ import Trash2 from 'lucide-svelte/icons/trash-2'; import Plus from 'lucide-svelte/icons/plus'; import Modal from './ui/Modal.svelte'; + import { t } from '../lib/i18n.js'; import ModalFoot from './ui/ModalFoot.svelte'; import { askConfirm } from '../lib/stores.js'; import { collectionsDialog } from '../lib/gameactions.js'; @@ -45,13 +46,14 @@ async function remove(c) { const n = c.gameIds.length; - if (!(await askConfirm(`Delete the collection “${c.name}”? ${n ? `Its ${n} game${n === 1 ? '' : 's'} stay tracked; only the grouping goes.` : 'It is empty.'}`, { title: 'Delete collection?', confirmText: 'Delete', danger: true }))) return; + const body = $t('collections.deleteConfirmBody', { name: c.name }) + ' ' + (n ? $t('collections.deleteConfirmGames', { n }) : $t('collections.deleteConfirmEmpty')); + if (!(await askConfirm(body, { title: $t('collections.deleteConfirmTitle'), confirmText: $t('common.delete'), danger: true }))) return; await deleteCollection(c.id); } {#if game} - + {game.name}
    @@ -80,22 +82,22 @@ {c.gameIds.length} {#if !c.builtin} - - + + {/if} {/if} {/each}
- - + +
-

Collections are for finding games in the library; they don't change how a game syncs.

+

{$t('collections.hint')}

- +
{/if} diff --git a/cmd/opensave-app/frontend/src/components/CommandPalette.svelte b/cmd/opensave-app/frontend/src/components/CommandPalette.svelte index a8a84410..9ad15c8e 100644 --- a/cmd/opensave-app/frontend/src/components/CommandPalette.svelte +++ b/cmd/opensave-app/frontend/src/components/CommandPalette.svelte @@ -3,7 +3,6 @@ // Enter. Everything in it is also somewhere on screen; this is the way to // it that does not need the mouse or knowing where it is. import { createEventDispatcher, onMount, tick } from 'svelte'; - import { plural } from '../lib/format.js'; import Search from 'lucide-svelte/icons/search'; import CornerDownLeft from 'lucide-svelte/icons/corner-down-left'; import { navigate, toast, syncPause } from '../lib/stores.js'; @@ -14,6 +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 { 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: `Go to ${p.label}`, kind: 'Page', idle: true, keywords: [p.id], run: () => navigate(p.id) })), - { label: 'Sync all games', kind: 'Action', idle: true, run: syncAll }, - { label: 'Snapshot every game now', kind: 'Action', idle: true, keywords: ['backup', 'save', 'all'], run: snapshotAll }, - { label: 'Scan for saves', kind: 'Action', idle: true, keywords: ['find', 'detect', 'auto-scan'], run: () => navigate('home', { scan: Date.now() }) }, - { label: 'Track a folder', kind: 'Action', idle: true, keywords: ['add', 'game'], run: () => navigate('home', { add: true }) }, + ...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 }) }, { - label: isDark() ? 'Switch to light theme' : 'Switch to dark theme', + label: isDark() ? $t('palette.toLight') : $t('palette.toDark'), kind: 'Action', idle: true, keywords: ['theme', 'appearance', 'mode'], run: () => appearance.update((a) => ({ ...a, theme: isDark() ? 'light' : 'dark' })) }, - { label: 'Keyboard shortcuts', kind: 'Help', idle: true, keywords: ['keys', 'help'], run: () => dispatch('help') }, + { label: $t('shortcuts.title'), kind: 'Help', idle: true, keywords: ['keys', 'help'], run: () => dispatch('help') }, { - label: 'Show the setup guide', + label: $t('palette.showSetup'), kind: 'Help', keywords: ['tutorial', 'getting started', 'onboarding', 'help', 'checklist'], run: () => { @@ -52,9 +52,9 @@ } }, ...($syncPause.paused - ? [{ label: 'Resume syncing', kind: 'Action', idle: true, keywords: ['unpause', 'start'], run: resumeSync }] + ? [{ label: $t('pause.resume'), kind: 'Action', idle: true, keywords: ['unpause', 'start'], run: resumeSync }] : PAUSE_CHOICES.map((c) => ({ - label: `Pause syncing ${c.label}`, + label: $t('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: `Show ${c.name}`, + label: $t('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: `Sync ${g.name}`, kind: 'Action', weight: -1, run: () => runGameAction('sync', g) }, - { label: `Snapshot ${g.name}`, kind: 'Action', weight: -1, keywords: ['backup', 'save'], run: () => runGameAction('snapshot', g) }, - ...(canLaunch(g) ? [{ label: `Launch ${g.name}`, kind: 'Action', weight: -1, keywords: ['play', 'start'], run: () => runGameAction('launch', g) }] : []), - { label: `Open ${g.name} save folder`, kind: 'Action', weight: -1, keywords: ['explorer', 'files'], run: () => runGameAction('folder', g) }, - { label: `Rename ${g.name}`, kind: 'Action', weight: -1, keywords: ['name', 'title'], run: () => runGameAction('rename', 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) } ]), // Snapshots with words of their own — a note, or a comment someone typed // — found by those words: "boss" finds "Before the final boss". @@ -91,7 +91,9 @@ try { const res = await api.post('/api/snapshots/all', {}); toast( - res.failed?.length ? `Snapshot of ${plural(res.taken, 'game')}; ${res.failed.length} could not be taken` : `Took a snapshot of ${plural(res.taken, 'game')}`, + res.failed?.length + ? $t('palette.snapshotAllPartial', { n: res.taken, failed: res.failed.length }) + : $t('palette.snapshotAllDone', { n: res.taken }), res.failed?.length ? 'error' : 'success' ); } catch (e) { @@ -101,7 +103,7 @@ async function syncAll() { for (const g of $visibleGames) api.post(`/api/games/${g.id}/sync`).catch(() => {}); - toast('Sync triggered for all games'); + toast($t('home.syncAllToast')); } function choose(entry) { @@ -135,14 +137,14 @@