From 194973e52d2175b9ece3a844a97b8fc3d7743115 Mon Sep 17 00:00:00 2001 From: Janlhin Date: Fri, 16 Oct 2026 09:11:51 +0800 Subject: [PATCH 1/3] i18n: extract the save-conflict and location-conflict modals --- .../src/components/ConflictModal.svelte | 101 ++++++++++-------- .../components/LocationConflictModal.svelte | 81 ++++++++------ cmd/opensave-app/frontend/src/locales/en.json | 78 ++++++++++++++ .../frontend/src/locales/zh-Hans.json | 54 ++++++++++ .../frontend/src/locales/zh-Hant.json | 54 ++++++++++ 5 files changed, 287 insertions(+), 81 deletions(-) diff --git a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte index b16e4151..1a856f64 100644 --- a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte +++ b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte @@ -5,6 +5,8 @@ import { visited } from '../lib/later.js'; import { api } from '../lib/api.js'; import { demandAttention } from '../lib/notify.js'; + // `fmtTime` below has a parameter named `t`, so the translator comes in aliased. + import { t as i18n, segments } from '../lib/i18n.js'; import Chevron from './ui/Chevron.svelte'; import Gamepad2 from 'lucide-svelte/icons/gamepad-2'; import Monitor from 'lucide-svelte/icons/monitor'; @@ -58,7 +60,7 @@ } $: gameName = current ? ($games[current[0]]?.name ?? current[0]) : ''; $: conflict = current ? current[1] : null; - $: peerName = conflict ? (conflict.peer.Name ?? conflict.peer.name ?? 'the other device') : ''; + $: peerName = conflict ? (conflict.peer.Name ?? conflict.peer.name ?? $i18n('conflict.otherDevice')) : ''; $: peerType = conflict ? $peers[conflict.peer.ID ?? conflict.peer.id]?.deviceType : ''; $: localIcon = isHandheld($settings?.deviceType) ? Gamepad2 : Monitor; $: peerIcon = isHandheld(peerType) ? Gamepad2 : Monitor; @@ -71,9 +73,9 @@ const fresh = list.filter(([gid]) => !seen.has(gid)); if (fresh.length > 0) { const game = $games[fresh[0][0]]; - const name = game?.name ?? 'a game'; - demandAttention('conflicts', { title: name, body: 'Its save changed here and on another device at once — choose which to keep.', game }); - toast(`Save conflict for “${name}” — choose which version to keep`, 'error'); + const name = game?.name ?? $i18n('conflict.aGame'); + demandAttention('conflicts', { title: name, body: $i18n('conflict.attentionBody'), game }); + toast($i18n('conflict.attentionToast', { name }), 'error'); } seen = new Set(list.map(([gid]) => gid)); // Housekeeping: forget "applying" markers for conflicts that no longer @@ -136,9 +138,9 @@ // different measurements. function sideSummary(stats) { const files = stats?.files; - if (files === 0) return 'save folder is empty'; + if (files === 0) return $i18n('conflict.saveFolderEmpty'); if (typeof files !== 'number') return ''; - return `whole save: ${files} file${files === 1 ? '' : 's'} · ${fmtSize(stats?.totalBytes ?? -1)}`; + return $i18n('conflict.wholeSave', { n: files, size: fmtSize(stats?.totalBytes ?? -1) }); } async function resolve(resolution) { @@ -156,7 +158,7 @@ }); applying = new Set(applying).add(gameId); if (resolution !== 'keep-local') { - toast(`Applying your choice for “${name}” — transferring ${peerName}'s files…`, 'info'); + toast($i18n('conflict.applyingToast', { name, peer: peerName }), 'info'); } showDiff = false; } catch (e) { @@ -167,12 +169,30 @@ } const fmtTime = (t) => (t ? new Date(t).toLocaleString() : '—'); - const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : 'unknown'); + const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : $i18n('conflict.unknown')); const fmtSize = (n) => n < 0 ? '—' : n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B'; const diffIcon = (s) => (s === 'changed' ? '✱' : s === 'only-remote' ? '+' : '−'); const diffLabel = (s) => - s === 'changed' ? 'differs' : s === 'only-remote' ? `only on ${peerName}` : 'only on this device'; + s === 'changed' + ? $i18n('conflict.differs') + : s === 'only-remote' + ? $i18n('conflict.onlyOnPeer', { peer: peerName }) + : $i18n('conflict.onlyOnDevice'); + + // The hint paragraph reads as one sentence stream in every language; the + // three option names arrive already quoted and are bolded in place. + const hintParams = (peerName) => ({ + both: $i18n('conflict.keepBothQuoted'), + mine: $i18n('conflict.keepMineQuoted'), + theirs: $i18n('conflict.keepTheirsQuoted'), + peer: peerName + }); + $: hintParts = segments($i18n('conflict.hintAll', hintParams(peerName)), hintParams(peerName), [ + 'both', + 'mine', + 'theirs' + ]); @@ -180,75 +200,68 @@ {#if current && conflict}
{/if} {/if}
- - + + + title={theirsDeletes > 0 ? $i18n('conflict.keepTheirsTitle', { n: theirsDeletes }) : ''}>{$i18n('conflict.keepTheirs')}

- Nothing is lost whichever you pick. “Keep both” (recommended) keeps - playing yours — {peerName} receives it too — and keeps {peerName}'s version here on a branch you can switch - to. “Keep mine” makes your - version the shared one — {peerName} receives it (their old save is snapshotted first). - “Keep theirs” adopts {peerName}'s version here, snapshotting yours first. - Restore anything from the game's Snapshots / Branches tabs. + + {#each hintParts as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each}

diff --git a/cmd/opensave-app/frontend/src/components/LocationConflictModal.svelte b/cmd/opensave-app/frontend/src/components/LocationConflictModal.svelte index abd206f3..ec825728 100644 --- a/cmd/opensave-app/frontend/src/components/LocationConflictModal.svelte +++ b/cmd/opensave-app/frontend/src/components/LocationConflictModal.svelte @@ -13,6 +13,7 @@ import { visited } from '../lib/later.js'; import { api } from '../lib/api.js'; import { demandAttention } from '../lib/notify.js'; + import { t, segments } from '../lib/i18n.js'; import Chevron from './ui/Chevron.svelte'; import Gamepad2 from 'lucide-svelte/icons/gamepad-2'; import Monitor from 'lucide-svelte/icons/monitor'; @@ -42,7 +43,7 @@ $: pending = ($locationConflicts ?? []).filter((c) => !applying.has(keyOf(c)) && !later.has(keyOf(c))); $: current = pending[0]; // one at a time, like the whole-game screen $: gameName = current ? ($games[current.gameId]?.name ?? current.gameId) : ''; - $: peerName = current ? (current.peer?.name ?? current.peer?.Name ?? 'the other device') : ''; + $: peerName = current ? (current.peer?.name ?? current.peer?.Name ?? $t('conflict.otherDevice')) : ''; $: peerType = current ? $peers[current.peer?.id ?? current.peer?.ID]?.deviceType : ''; $: localIcon = isHandheld($settings?.deviceType) ? Gamepad2 : Monitor; $: peerIcon = isHandheld(peerType) ? Gamepad2 : Monitor; @@ -54,9 +55,9 @@ const fresh = list.filter((c) => !seen.has(keyOf(c))); if (fresh.length > 0) { const game = $games[fresh[0].gameId]; - const name = game?.name ?? 'a game'; - demandAttention('conflicts', { title: name, body: `Its “${fresh[0].root}” folder differs on both devices — choose which to keep.`, game }); - toast(`The “${fresh[0].root}” folder of “${name}” differs on both devices`, 'error'); + const name = game?.name ?? $t('conflict.aGame'); + demandAttention('conflicts', { title: name, body: $t('locConflict.attentionBody', { root: fresh[0].root }), game }); + toast($t('locConflict.attentionToast', { root: fresh[0].root, name }), 'error'); } seen = new Set(list.map(keyOf)); // Forget in-flight markers for conflicts that are gone. @@ -83,7 +84,7 @@ }); applying = new Set(applying).add(keyOf(c)); if (resolution === 'keep-remote') { - toast(`Bringing ${peerName}'s “${c.root}” folder over…`, 'info'); + toast($t('locConflict.bringingToast', { peer: peerName, root: c.root }), 'info'); } showDiff = false; } catch (e) { @@ -93,12 +94,29 @@ } } - const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : 'unknown'); + const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : $t('conflict.unknown')); const fmtSize = (n) => n < 0 ? '—' : n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B'; const diffIcon = (s) => (s === 'changed' ? '✱' : s === 'only-remote' ? '+' : '−'); const diffLabel = (s) => - s === 'changed' ? 'differs' : s === 'only-remote' ? `only on ${peerName}` : 'only on this device'; + s === 'changed' + ? $t('conflict.differs') + : s === 'only-remote' + ? $t('conflict.onlyOnPeer', { peer: peerName }) + : $t('conflict.onlyOnDevice'); + + // The hint paragraph reads as one sentence stream in every language; the + // option names arrive already quoted and are bolded in place. + const hintUndoParams = (peerName) => ({ + mine: $t('conflict.keepMineQuoted'), + theirs: $t('locConflict.useTheirsQuoted', { peer: peerName }), + peer: peerName + }); + $: hintUndoParts = segments( + $t('locConflict.hintUndo', hintUndoParams(peerName)), + hintUndoParams(peerName), + ['mine', 'theirs'] + ); $: localMs = current?.localStats?.latestMtimeMs ?? 0; $: remoteMs = current?.remoteStats?.latestMtimeMs ?? 0; @@ -111,9 +129,9 @@ function sideSummary(stats) { const files = stats?.files; - if (files === 0) return 'folder is empty'; + if (files === 0) return $t('locConflict.folderEmpty'); if (typeof files !== 'number') return ''; - return `whole folder: ${files} file${files === 1 ? '' : 's'} · ${fmtSize(stats?.totalBytes ?? -1)}`; + return $t('locConflict.wholeFolder', { n: files, size: fmtSize(stats?.totalBytes ?? -1) }); } @@ -122,49 +140,45 @@ {#if current}
{/if} {/if}
- +

- A snapshot of every one of this game's folders is taken before either choice is applied, - so both are undoable from the Snapshots tab. “Keep mine” makes this - device's copy the shared one and asks {peerName} to take it — {peerName} gets its own say if - it has newer work. “Use {peerName}'s” brings their copy here. + + {#each hintUndoParts as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each}

- There is no “keep both” for a folder: that option puts the other copy on a branch, and - branches cover the whole game rather than one of its folders. + {$t('locConflict.hintNoKeepBoth')}

diff --git a/cmd/opensave-app/frontend/src/locales/en.json b/cmd/opensave-app/frontend/src/locales/en.json index 9b7d6d99..cc47ff19 100644 --- a/cmd/opensave-app/frontend/src/locales/en.json +++ b/cmd/opensave-app/frontend/src/locales/en.json @@ -343,6 +343,84 @@ "pause.pauseTitle": "Stop syncing with your other devices for a while — snapshots are still taken", "pause.resume": "Resume syncing", "pause.resumeTitle": "Syncing is paused {length}. Snapshots are still taken. Resume now to catch up.", + "conflict.title": "Save conflict — {game}", + "conflict.desc": "Both this device and {peer} changed this save since the last sync. Pick which version to play from.", + "conflict.thisDevice": "This device", + "conflict.playedRecently": "played more recently", + "conflict.diffHere": { + "one": "{n} file differs here", + "other": "{n} files differ here" + }, + "conflict.diffThere": { + "one": "{n} file differs there", + "other": "{n} files differ there" + }, + "conflict.onlyHere": "{n} only on this device", + "conflict.onlyThere": "{n} only on {peer}", + "conflict.lastChange": "last change {time}", + "conflict.saveFolderEmpty": "save folder is empty", + "conflict.wholeSave": { + "one": "whole save: {n} file · {size}", + "other": "whole save: {n} files · {size}" + }, + "conflict.diffCapped": "Counts above cover the first {shown} of {total} differing files.", + "conflict.keepMine": "Keep mine", + "conflict.keepTheirs": "Keep theirs", + "conflict.keepBoth": "Keep both (recommended)", + "conflict.keepBothName": "Keep both", + "conflict.consequenceKeepBoth": "{mine} / {both}: nothing is deleted.", + "conflict.broughtHere": { + "one": "The {n} file only on {peer} is brought here;", + "other": "The {n} files only on {peer} are brought here;" + }, + "conflict.keptWhere": "where both have a file, yours is kept.", + "conflict.consequenceKeepTheirs": "{theirs}:", + "conflict.deletesFiles": { + "one": "deletes 1 file", + "other": "deletes {n} files" + }, + "conflict.deletesTail": " that only this device has (snapshotted first).", + "conflict.deletesNothing": "nothing only this device has is lost.", + "conflict.diffToggle": { + "one": "What's different (1 file)", + "other": "What's different ({n} files)" + }, + "conflict.diffMore": "…and {n} more", + "conflict.decideLater": "Decide later", + "conflict.decideLaterTitle": "Nothing of this game syncs until you decide. Open the game to decide.", + "conflict.keepTheirsTitle": { + "one": "Deletes 1 file only this device has", + "other": "Deletes {n} files only this device has" + }, + "conflict.hintAll": "Nothing is lost whichever you pick. {both} (recommended) keeps playing yours — {peer} receives it too — and keeps {peer}'s version here on a branch you can switch to. {mine} makes your version the shared one — {peer} receives it (their old save is snapshotted first). {theirs} adopts {peer}'s version here, snapshotting yours first. Restore anything from the game's Snapshots / Branches tabs.", + "conflict.keepMineQuoted": "“Keep mine”", + "conflict.keepTheirsQuoted": "“Keep theirs”", + "conflict.keepBothQuoted": "“Keep both”", + "conflict.attentionBody": "Its save changed here and on another device at once — choose which to keep.", + "conflict.attentionToast": "Save conflict for “{name}” — choose which version to keep", + "conflict.applyingToast": "Applying your choice for “{name}” — transferring {peer}'s files…", + "conflict.differs": "differs", + "conflict.onlyOnPeer": "only on {peer}", + "conflict.onlyOnDevice": "only on this device", + "conflict.otherDevice": "the other device", + "conflict.aGame": "a game", + "conflict.unknown": "unknown", + "locConflict.title": "“{root}” folder — {game}", + "locConflict.desc": "This is one of {game}'s extra save folders, and it changed on both this device and {peer}. The game's main save is unaffected and keeps syncing; only this folder is waiting on you.", + "locConflict.changedRecently": "changed more recently", + "locConflict.folderEmpty": "folder is empty", + "locConflict.wholeFolder": { + "one": "whole folder: {n} file · {size}", + "other": "whole folder: {n} files · {size}" + }, + "locConflict.attentionBody": "Its “{root}” folder differs on both devices — choose which to keep.", + "locConflict.attentionToast": "The “{root}” folder of “{game}” differs on both devices", + "locConflict.bringingToast": "Bringing {peer}'s “{root}” folder over…", + "locConflict.decideLaterTitle": "Nothing of this folder syncs until you decide. Open the game to decide.", + "locConflict.useTheirs": "Use {peer}'s", + "locConflict.useTheirsQuoted": "“Use {peer}'s”", + "locConflict.hintUndo": "A snapshot of every one of this game's folders is taken before either choice is applied, so both are undoable from the Snapshots tab. {mine} makes this device's copy the shared one and asks {peer} to take it — {peer} gets its own say if it has newer work. {theirs} brings their copy here.", + "locConflict.hintNoKeepBoth": "There is no “keep both” for a folder: that option puts the other copy on a branch, and branches cover the whole game rather than one of its folders.", "common.open": "Open", "home.trackGame": "Track game", "home.trackGameHint": "Find a game by name, or choose its program or save folder", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hans.json b/cmd/opensave-app/frontend/src/locales/zh-Hans.json index 7d92a282..9996c5d4 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hans.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hans.json @@ -301,6 +301,60 @@ "pause.pauseTitle": "暂时停止与其他设备同步——快照仍会创建", "pause.resume": "恢复同步", "pause.resumeTitle": "同步已暂停({length})。快照仍会创建。立即恢复以补齐。", + "conflict.title": "存档冲突 — {game}", + "conflict.desc": "本设备与 {peer} 在上次同步之后都改动了这个存档。请选择要从哪个版本继续游玩。", + "conflict.thisDevice": "本设备", + "conflict.playedRecently": "最近游玩过", + "conflict.diffHere": "{n} 个文件在这里有差异", + "conflict.diffThere": "{n} 个文件在那里有差异", + "conflict.onlyHere": "{n} 个仅此设备有", + "conflict.onlyThere": "{n} 个仅 {peer} 有", + "conflict.lastChange": "最后改动于 {time}", + "conflict.saveFolderEmpty": "存档文件夹为空", + "conflict.wholeSave": "整个存档:{n} 个文件 · {size}", + "conflict.diffCapped": "上方的计数仅覆盖 {total} 个有差异文件中的前 {shown} 个。", + "conflict.keepMine": "保留我的", + "conflict.keepTheirs": "保留对方的", + "conflict.keepBoth": "两者都保留(推荐)", + "conflict.keepBothName": "两者都保留", + "conflict.consequenceKeepBoth": "{mine} / {both}:不会删除任何文件。", + "conflict.broughtHere": "{peer} 独有的 {n} 个文件会被带到这里;", + "conflict.keptWhere": "双方都有的文件则保留你的版本。", + "conflict.consequenceKeepTheirs": "{theirs}:", + "conflict.deletesFiles": "删除 {n} 个文件", + "conflict.deletesTail": ",这些文件只有此设备才有(会先快照)。", + "conflict.deletesNothing": "不会丢失此设备独有的任何文件。", + "conflict.diffToggle": "有什么不同({n} 个文件)", + "conflict.diffMore": "…还有 {n} 个", + "conflict.decideLater": "稍后决定", + "conflict.decideLaterTitle": "在你做出决定之前,这个游戏不会同步。打开该游戏即可做出决定。", + "conflict.keepTheirsTitle": "将删除 {n} 个仅此设备有的文件", + "conflict.hintAll": "无论选择哪一个都不会丢失数据。{both}(推荐)会继续使用你的版本——{peer} 也会收到它——同时会把 {peer} 的版本保留在这里的一个分支上,你可以切换过去。{mine} 会把你的版本设为共享版本——{peer} 会收到它(对方的旧存档会先快照)。{theirs} 会在这里采用 {peer} 的版本,你的版本会先快照。随时可以在游戏的“快照 / 分支”标签页恢复任何内容。", + "conflict.keepMineQuoted": "“保留我的”", + "conflict.keepTheirsQuoted": "“保留对方的”", + "conflict.keepBothQuoted": "“两者都保留”", + "conflict.attentionBody": "这个存档在本设备与另一台设备上同时发生了改动——请选择要保留哪个。", + "conflict.attentionToast": "“{name}”出现存档冲突——请选择要保留的版本", + "conflict.applyingToast": "正在应用你对“{name}”的选择——正在传输 {peer} 的文件…", + "conflict.differs": "有差异", + "conflict.onlyOnPeer": "仅 {peer} 有", + "conflict.onlyOnDevice": "仅此设备有", + "conflict.otherDevice": "另一台设备", + "conflict.aGame": "某个游戏", + "conflict.unknown": "未知", + "locConflict.title": "“{root}”文件夹 — {game}", + "locConflict.desc": "这是 {game} 的一个额外存档文件夹,本设备与 {peer} 都改动了它。游戏的主存档不受影响、会继续同步;只有这个文件夹在等你处理。", + "locConflict.changedRecently": "最近改动过", + "locConflict.folderEmpty": "文件夹为空", + "locConflict.wholeFolder": "整个文件夹:{n} 个文件 · {size}", + "locConflict.attentionBody": "它的“{root}”文件夹在两台设备上不一致——请选择要保留哪个。", + "locConflict.attentionToast": "“{game}”的“{root}”文件夹在两台设备上不一致", + "locConflict.bringingToast": "正在拉取 {peer} 的“{root}”文件夹…", + "locConflict.decideLaterTitle": "在你做出决定之前,这个文件夹不会同步。打开该游戏即可做出决定。", + "locConflict.useTheirs": "使用{peer}的", + "locConflict.useTheirsQuoted": "“使用{peer}的”", + "locConflict.hintUndo": "无论选择哪一项,都会先对这个游戏的每个文件夹拍快照,因此两个选择都可以在“快照”标签页撤销。{mine} 会把本设备的副本设为共享版本,并让 {peer} 接收它——如果 {peer} 有更新的进度,它会自行决定是否接受。{theirs} 会把对方的副本同步过来。", + "locConflict.hintNoKeepBoth": "文件夹没有“两者都保留”选项:该选项会把另一份副本放到分支上,而分支覆盖的是整个游戏,而不是其中某个文件夹。", "common.open": "打开", "home.trackGame": "跟踪游戏", "home.trackGameHint": "按名称查找游戏,或选择其程序或存档文件夹", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hant.json b/cmd/opensave-app/frontend/src/locales/zh-Hant.json index 4752f44f..c20c551d 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hant.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hant.json @@ -301,6 +301,60 @@ "pause.pauseTitle": "暫時停止與其他裝置同步——快照仍會建立", "pause.resume": "恢復同步", "pause.resumeTitle": "同步已暫停({length})。快照仍會建立。立即恢復以補齊。", + "conflict.title": "存檔衝突 — {game}", + "conflict.desc": "本裝置與 {peer} 在上次同步之後都改動了這個存檔。請選擇要從哪個版本繼續遊玩。", + "conflict.thisDevice": "本裝置", + "conflict.playedRecently": "最近遊玩過", + "conflict.diffHere": "{n} 個檔案在這裡有差異", + "conflict.diffThere": "{n} 個檔案在那裡有差異", + "conflict.onlyHere": "{n} 個僅此裝置有", + "conflict.onlyThere": "{n} 個僅 {peer} 有", + "conflict.lastChange": "最後改動於 {time}", + "conflict.saveFolderEmpty": "存檔資料夾為空", + "conflict.wholeSave": "整個存檔:{n} 個檔案 · {size}", + "conflict.diffCapped": "上方的計數僅覆蓋 {total} 個有差異檔案中的前 {shown} 個。", + "conflict.keepMine": "保留我的", + "conflict.keepTheirs": "保留對方的", + "conflict.keepBoth": "兩者都保留(推薦)", + "conflict.keepBothName": "兩者都保留", + "conflict.consequenceKeepBoth": "{mine} / {both}:不會刪除任何檔案。", + "conflict.broughtHere": "{peer} 獨有的 {n} 個檔案會被帶到這裡;", + "conflict.keptWhere": "雙方都有的檔案則保留你的版本。", + "conflict.consequenceKeepTheirs": "{theirs}:", + "conflict.deletesFiles": "刪除 {n} 個檔案", + "conflict.deletesTail": ",這些檔案只有此裝置才有(會先快照)。", + "conflict.deletesNothing": "不會丟失此裝置獨有的任何檔案。", + "conflict.diffToggle": "有什麼不同({n} 個檔案)", + "conflict.diffMore": "…還有 {n} 個", + "conflict.decideLater": "稍後決定", + "conflict.decideLaterTitle": "在你做出決定之前,這個遊戲不會同步。開啟該遊戲即可做出決定。", + "conflict.keepTheirsTitle": "將刪除 {n} 個僅此裝置有的檔案", + "conflict.hintAll": "無論選擇哪一個都不會丟失資料。{both}(推薦)會繼續使用你的版本——{peer} 也會收到它——同時會把 {peer} 的版本保留在這裡的一個分支上,你可以切換過去。{mine} 會把你的版本設為共享版本——{peer} 會收到它(對方的舊存檔會先快照)。{theirs} 會在這裡採用 {peer} 的版本,你的版本會先快照。隨時可以在遊戲的“快照 / 分支”標籤頁恢復任何內容。", + "conflict.keepMineQuoted": "“保留我的”", + "conflict.keepTheirsQuoted": "“保留對方的”", + "conflict.keepBothQuoted": "“兩者都保留”", + "conflict.attentionBody": "這個存檔在本裝置與另一臺裝置上同時發生了改動——請選擇要保留哪個。", + "conflict.attentionToast": "“{name}”出現存檔衝突——請選擇要保留的版本", + "conflict.applyingToast": "正在應用你對“{name}”的選擇——正在傳輸 {peer} 的檔案…", + "conflict.differs": "有差異", + "conflict.onlyOnPeer": "僅 {peer} 有", + "conflict.onlyOnDevice": "僅此裝置有", + "conflict.otherDevice": "另一臺裝置", + "conflict.aGame": "某個遊戲", + "conflict.unknown": "未知", + "locConflict.title": "“{root}”資料夾 — {game}", + "locConflict.desc": "這是 {game} 的一個額外存檔資料夾,本裝置與 {peer} 都改動了它。遊戲的主存檔不受影響、會繼續同步;只有這個資料夾在等你處理。", + "locConflict.changedRecently": "最近改動過", + "locConflict.folderEmpty": "資料夾為空", + "locConflict.wholeFolder": "整個資料夾:{n} 個檔案 · {size}", + "locConflict.attentionBody": "它的“{root}”資料夾在兩臺裝置上不一致——請選擇要保留哪個。", + "locConflict.attentionToast": "“{game}”的“{root}”資料夾在兩臺裝置上不一致", + "locConflict.bringingToast": "正在拉取 {peer} 的“{root}”資料夾…", + "locConflict.decideLaterTitle": "在你做出決定之前,這個資料夾不會同步。開啟該遊戲即可做出決定。", + "locConflict.useTheirs": "使用{peer}的", + "locConflict.useTheirsQuoted": "“使用{peer}的”", + "locConflict.hintUndo": "無論選擇哪一項,都會先對這個遊戲的每個資料夾拍快照,因此兩個選擇都可以在“快照”標籤頁撤銷。{mine} 會把本裝置的副本設為共享版本,並讓 {peer} 接收它——如果 {peer} 有更新的進度,它會自行決定是否接受。{theirs} 會把對方的副本同步過來。", + "locConflict.hintNoKeepBoth": "資料夾沒有“兩者都保留”選項:該選項會把另一份副本放到分支上,而分支覆蓋的是整個遊戲,而不是其中某個資料夾。", "common.open": "開啟", "home.trackGame": "跟踪遊戲", "home.trackGameHint": "按名稱查詢遊戲,或選擇其程式或存檔資料夾", From 4fa39cdcf0ea4da81161eeba85328ccde009e541 Mon Sep 17 00:00:00 2001 From: Janlhin Date: Fri, 16 Oct 2026 09:29:09 +0800 Subject: [PATCH 2/3] i18n: extract the Settings script, header, tabs and the general and sync tabs --- cmd/opensave-app/frontend/src/locales/en.json | 103 +++++++++ .../frontend/src/locales/zh-Hans.json | 94 ++++++++ .../frontend/src/locales/zh-Hant.json | 94 ++++++++ .../frontend/src/views/Settings.svelte | 200 +++++++++--------- 4 files changed, 387 insertions(+), 104 deletions(-) diff --git a/cmd/opensave-app/frontend/src/locales/en.json b/cmd/opensave-app/frontend/src/locales/en.json index cc47ff19..1da8a413 100644 --- a/cmd/opensave-app/frontend/src/locales/en.json +++ b/cmd/opensave-app/frontend/src/locales/en.json @@ -1,6 +1,109 @@ { "settings.language": "Language", "settings.systemDefault": "System default", + "settings.sync.unknownGameHint": "Tracking automatically works out a folder from where the game lives on the other device, which is why OpenSave usually needs no setup.", + "settings.title": "Settings", + "settings.saveState.saving": "Saving…", + "settings.saveState.notSaved": "Not saved: the {field} — {why}", + "settings.saveState.saveFailed": "Couldn't save: {error}", + "settings.saveState.saved": "Saved", + "settings.saveState.autosave": "Changes save as you make them", + "settings.tab.general": "General", + "settings.tab.sync": "Sync", + "settings.tab.storage": "Storage", + "settings.tab.advanced": "Advanced", + "settings.tab.support": "Support", + "settings.discord.title": "Open the OpenSave Discord in your browser", + "settings.field.relayUrl": "relay URL", + "settings.field.relayPort": "relay hosting port", + "settings.field.port": "daemon port", + "settings.field.deviceName": "device name", + "settings.field.backupsDir": "snapshots folder", + "settings.field.syncBackupsDir": "safety backups folder", + "settings.field.pathTranslations": "path rules", + "settings.cleanUp.saveFailed": "The limits couldn't be saved: {error}", + "settings.cleanUp.removed": { + "one": "Removed {n} old snapshot, freed {mb} MB", + "other": "Removed {n} old snapshots, freed {mb} MB" + }, + "settings.cleanUp.none": "Nothing to clean up — all games are within their limit", + "settings.storage.pickDialog": "Select snapshots storage folder", + "settings.storage.pickSyncDialog": "Select pre-sync safety backups folder", + "settings.storage.scanAddDialog": "Add a folder to scan", + "settings.storage.excludeAddDialog": "Choose a folder to exclude from scans", + "settings.reset.confirmBody": { + "one": "Untrack all {n} game? They'll be removed from your library so you can re-add them from the correct locations. Your save snapshots on disk are kept — nothing is deleted.", + "other": "Untrack all {n} games? They'll be removed from your library so you can re-add them from the correct locations. Your save snapshots on disk are kept — nothing is deleted." + }, + "settings.reset.confirmTitle": "Reset tracking?", + "settings.reset.confirmAction": "Untrack {n}", + "settings.reset.doneToast": { + "one": "Untracked {n} game — snapshots kept", + "other": "Untracked {n} games — snapshots kept" + }, + "settings.general.deviceIdentity": "Device identity", + "settings.general.deviceName": "Device name — how other devices see you", + "settings.general.deviceType": "Device type", + "settings.general.type.desktop": "Desktop (Windows / macOS / Linux PC)", + "settings.general.type.deck": "Steam Deck (SteamOS handheld)", + "settings.general.type.handheld": "Handheld (ROG Ally / Legion Go / emulator)", + "settings.general.type.mobile": "Companion (mobile device)", + "settings.general.typeHint": "Shown to other devices when they discover you.", + "settings.general.deviceId": "Device ID", + "settings.general.deviceIdHint": "This device's unique network identifier (read-only).", + "settings.general.appearance": "Appearance", + "settings.general.controller": "Controller", + "settings.general.notifications": "Notifications", + "settings.general.library": "Library", + "settings.general.libraryHint": "How your games are laid out on Home. Changes apply straight away.", + "settings.general.setupGuide": "Setup guide", + "settings.general.setupGuideHint": "The getting-started checklist on Home: find your saves, add your other devices, back up to the cloud.", + "settings.general.showSetupAgain": "Show it again", + "settings.general.startup": "Startup", + "settings.general.startOnBoot": "Start OpenSave when the computer starts", + "settings.general.startOnBootHint": "Launches minimized to the system tray so syncing runs in the background.", + "settings.general.updates": "Updates", + "settings.general.offerBetas": "Offer me beta versions", + "settings.general.betasOnPreRelease": "You're running {version}, which is a beta, so you'll be offered newer betas whether or not this is ticked — otherwise there'd be no way forward until the final release caught up. Untick it and you'll move to the stable release as soon as it's newer than what you have.", + "settings.general.betasStable": "Betas ship early with the newest fixes and are less tested. You'll be offered the stable release whenever it's newer, so this isn't a one-way door.", + "settings.sync.behavior": "Sync behavior", + "settings.sync.autoSyncOnTrack": "Sync a game immediately when it's first tracked", + "settings.sync.detectNewGames": "Tell me when a game turns up with saves OpenSave isn't keeping", + "settings.sync.detectNewGamesHint": "OpenSave looks every hour, in the background, and only mentions games it hasn't seen before. Nothing is tracked until you choose it.", + "settings.sync.matchByAppId": "Match saves across PCs by Steam App ID", + "settings.sync.matchByAppIdHint": "Links the same game across devices even when it was tracked under different names or drives.", + "settings.sync.matchByAppIdMore": "For example a Steam copy on one PC and a standalone copy on another. Leave this off if you deliberately keep two separate copies of the same game that shouldn't merge. You can always link games by hand from a game's page.", + "settings.sync.unknownGameLabel": "When another device syncs a game this one doesn't have", + "settings.sync.unknownGame.track": "Start tracking it automatically (recommended)", + "settings.sync.unknownGame.ask": "Ask me where to keep it", + "settings.sync.unknownGameMore": "Choose {askMe} if your saves are somewhere that guess would get wrong — a second drive, a folder you moved, or a game that keeps saves in a folder named after your account. Games waiting for a folder appear on the Home page.", + "settings.sync.askShort": "Ask me", + "settings.sync.bandwidthLimit": "Internet bandwidth limit", + "settings.sync.speed.unlimited": "Unlimited (max speed)", + "settings.sync.speed.low": "100 KB/s (very low)", + "settings.sync.speed.medium": "500 KB/s (medium)", + "settings.sync.speed.high": "1 MB/s (high)", + "settings.sync.speed.veryHigh": "5 MB/s (very high)", + "settings.sync.speed.ultra": "10 MB/s (ultra)", + "settings.sync.speedHint": "Only applies to relay (internet) syncs — LAN is never throttled.", + "settings.sync.relay": "Internet relay", + "settings.sync.relayUrl": "WebSocket relay URL", + "settings.sync.relayUrlHint": "The relay that carries syncs across the internet. Join a room from {internetSync}.", + "settings.sync.internetSync": "Internet Sync", + "settings.sync.hostRelay": "Host a WAN relay server on this device", + "settings.sync.hostRelayHint": "Lets friends connect directly to you instead of the public relay.", + "settings.sync.relayPort": "Relay hosting port", + "settings.sync.relayPortHint": "Forward this TCP port on your router so friends on the internet can reach you.", + "settings.sync.lookingUp": "Looking up…", + "settings.sync.hideAddresses": "Hide my addresses", + "settings.sync.showAddresses": "Show my addresses to share", + "settings.sync.shareTitle": "Share these with your friend", + "settings.sync.lanIps": "LAN IPs:", + "settings.sync.publicIp": "Public IP:", + "settings.sync.unavailable": "unavailable", + "settings.sync.relayPortLabel": "Relay port:", + "settings.sync.cloudBackupHint": "Where backups go, whether every snapshot is sent there, and whether newer saves are brought from your other devices are all set on the Cloud Backup page.", + "settings.sync.openCloudBackup": "Open Cloud Backup", "common.cancel": "Cancel", "common.nowTracking": "Now tracking \"{name}\"", "home.addGame.adding": "Adding…", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hans.json b/cmd/opensave-app/frontend/src/locales/zh-Hans.json index 9996c5d4..6a0300a2 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hans.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hans.json @@ -1,6 +1,100 @@ { "settings.language": "语言", "settings.systemDefault": "跟随系统", + "settings.sync.unknownGameHint": "自动跟踪会根据游戏在其他设备上的位置推算出一个文件夹,这也是 OpenSave 通常无需设置的原因。", + "settings.title": "设置", + "settings.saveState.saving": "正在保存…", + "settings.saveState.notSaved": "未保存:{field} —— {why}", + "settings.saveState.saveFailed": "无法保存:{error}", + "settings.saveState.saved": "已保存", + "settings.saveState.autosave": "更改会随做随存", + "settings.tab.general": "常规", + "settings.tab.sync": "同步", + "settings.tab.storage": "存储", + "settings.tab.advanced": "高级", + "settings.tab.support": "支持", + "settings.discord.title": "在浏览器中打开 OpenSave Discord", + "settings.field.relayUrl": "中继 URL", + "settings.field.relayPort": "中继托管端口", + "settings.field.port": "守护进程端口", + "settings.field.deviceName": "设备名称", + "settings.field.backupsDir": "快照文件夹", + "settings.field.syncBackupsDir": "安全备份文件夹", + "settings.field.pathTranslations": "路径规则", + "settings.cleanUp.saveFailed": "上限未能保存:{error}", + "settings.cleanUp.removed": "已删除 {n} 个旧快照,释放了 {mb} MB", + "settings.cleanUp.none": "无可清理——所有游戏都在各自上限之内", + "settings.storage.pickDialog": "选择快照存储文件夹", + "settings.storage.pickSyncDialog": "选择同步前安全备份文件夹", + "settings.storage.scanAddDialog": "添加要扫描的文件夹", + "settings.storage.excludeAddDialog": "选择要从扫描中排除的文件夹", + "settings.reset.confirmBody": "取消跟踪全部 {n} 个游戏?它们将从您的游戏库中移除,以便您从正确的位置重新添加。磁盘上的存档快照会保留——不会删除任何内容。", + "settings.reset.confirmTitle": "重置跟踪?", + "settings.reset.confirmAction": "取消跟踪 {n}", + "settings.reset.doneToast": "已取消跟踪 {n} 个游戏——快照已保留", + "settings.general.deviceIdentity": "设备身份", + "settings.general.deviceName": "设备名称——其他设备看到您的名字", + "settings.general.deviceType": "设备类型", + "settings.general.type.desktop": "桌面电脑(Windows / macOS / Linux PC)", + "settings.general.type.deck": "Steam Deck(SteamOS 掌机)", + "settings.general.type.handheld": "掌机(ROG Ally / Legion Go / 模拟器)", + "settings.general.type.mobile": "伴侣设备(移动设备)", + "settings.general.typeHint": "其他设备发现您时会看到此信息。", + "settings.general.deviceId": "设备 ID", + "settings.general.deviceIdHint": "此设备的唯一网络标识符(只读)。", + "settings.general.appearance": "外观", + "settings.general.controller": "手柄", + "settings.general.notifications": "通知", + "settings.general.library": "游戏库", + "settings.general.libraryHint": "游戏在主页上的排布方式。更改立即生效。", + "settings.general.setupGuide": "设置向导", + "settings.general.setupGuideHint": "主页上的入门清单:找到您的存档、添加您的其他设备、备份到云端。", + "settings.general.showSetupAgain": "再次显示", + "settings.general.startup": "启动", + "settings.general.startOnBoot": "开机时启动 OpenSave", + "settings.general.startOnBootHint": "以最小化到系统托盘的方式启动,让同步在后台进行。", + "settings.general.updates": "更新", + "settings.general.offerBetas": "向我提供测试版", + "settings.general.betasOnPreRelease": "您正在运行 {version},它本身是测试版,因此无论是否勾选此项,您都会收到更新的测试版——否则在正式版赶上之前您将无路可走。取消勾选后,一旦正式版比您当前的版本更新,您就会切换到正式版。", + "settings.general.betasStable": "测试版发布更早、包含最新的修复,但测试程度较低。只要正式版更新,您就会收到正式版,所以这不是一条单行道。", + "settings.sync.behavior": "同步行为", + "settings.sync.autoSyncOnTrack": "游戏首次被跟踪时立即同步", + "settings.sync.detectNewGames": "当出现 OpenSave 未在跟踪其存档的游戏时提醒我", + "settings.sync.detectNewGamesHint": "OpenSave 每小时在后台检查一次,只提及它没见过的游戏。在您选择之前不会跟踪任何内容。", + "settings.sync.matchByAppId": "按 Steam App ID 跨 PC 匹配存档", + "settings.sync.matchByAppIdHint": "即使同一游戏在不同设备上以不同名称或盘符被跟踪,也能将它们关联起来。", + "settings.sync.matchByAppIdMore": "例如一台 PC 上的 Steam 版和另一台上的独立版。如果您有意保留同一款游戏互不合并的两份副本,请保持关闭。您随时可以在游戏页面手动关联游戏。", + "settings.sync.unknownGameLabel": "当其他设备同步了本机没有的游戏时", + "settings.sync.unknownGame.track": "自动开始跟踪(推荐)", + "settings.sync.unknownGame.ask": "询问我保存在哪里", + "settings.sync.unknownGameMore": "如果您的存档位置可能与自动猜测不符——第二块硬盘、移动过的文件夹,或把存档放在以您的账号命名的文件夹中的游戏——请选择“{askMe}”。等待指定文件夹的游戏会显示在主页上。", + "settings.sync.askShort": "询问我", + "settings.sync.bandwidthLimit": "网络带宽限制", + "settings.sync.speed.unlimited": "不限速(最高速度)", + "settings.sync.speed.low": "100 KB/s(很低)", + "settings.sync.speed.medium": "500 KB/s(中等)", + "settings.sync.speed.high": "1 MB/s(高)", + "settings.sync.speed.veryHigh": "5 MB/s(很高)", + "settings.sync.speed.ultra": "10 MB/s(极高)", + "settings.sync.speedHint": "仅适用于中继(互联网)同步——局域网永远不会被限速。", + "settings.sync.relay": "互联网中继", + "settings.sync.relayUrl": "WebSocket 中继 URL", + "settings.sync.relayUrlHint": "中继在互联网上承载同步。从{internetSync}加入房间。", + "settings.sync.internetSync": "联网同步", + "settings.sync.hostRelay": "在本设备上托管 WAN 中继服务器", + "settings.sync.hostRelayHint": "让朋友直接连接您,而不是经由公共中继。", + "settings.sync.relayPort": "中继托管端口", + "settings.sync.relayPortHint": "在路由器上转发此 TCP 端口,让互联网上的朋友能够连到您。", + "settings.sync.lookingUp": "查询中…", + "settings.sync.hideAddresses": "隐藏我的地址", + "settings.sync.showAddresses": "显示我的地址以便分享", + "settings.sync.shareTitle": "将这些分享给您的朋友", + "settings.sync.lanIps": "局域网 IP:", + "settings.sync.publicIp": "公网 IP:", + "settings.sync.unavailable": "不可用", + "settings.sync.relayPortLabel": "中继端口:", + "settings.sync.cloudBackupHint": "备份的去向、每个快照是否都发送到那里、以及是否从其他设备取回较新的存档,这些都在“云备份”页面设置。", + "settings.sync.openCloudBackup": "打开云备份", "common.cancel": "取消", "common.nowTracking": "已开始跟踪“{name}”", "home.addGame.adding": "添加中…", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hant.json b/cmd/opensave-app/frontend/src/locales/zh-Hant.json index c20c551d..20ff3d87 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hant.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hant.json @@ -1,6 +1,100 @@ { "settings.language": "語言", "settings.systemDefault": "跟隨系統", + "settings.sync.unknownGameHint": "自動追蹤會根據遊戲在其他裝置上的位置推算出一個資料夾,這也是 OpenSave 通常無需設定的原因。", + "settings.title": "設定", + "settings.saveState.saving": "正在儲存…", + "settings.saveState.notSaved": "未儲存:{field} —— {why}", + "settings.saveState.saveFailed": "無法儲存:{error}", + "settings.saveState.saved": "已儲存", + "settings.saveState.autosave": "更改會隨做隨存", + "settings.tab.general": "常規", + "settings.tab.sync": "同步", + "settings.tab.storage": "儲存", + "settings.tab.advanced": "高階", + "settings.tab.support": "支援", + "settings.discord.title": "在瀏覽器中開啟 OpenSave Discord", + "settings.field.relayUrl": "中繼 URL", + "settings.field.relayPort": "中繼託管埠", + "settings.field.port": "守護程序埠", + "settings.field.deviceName": "裝置名稱", + "settings.field.backupsDir": "快照資料夾", + "settings.field.syncBackupsDir": "安全備份資料夾", + "settings.field.pathTranslations": "路徑規則", + "settings.cleanUp.saveFailed": "上限未能儲存:{error}", + "settings.cleanUp.removed": "已刪除 {n} 箇舊快照,釋放了 {mb} MB", + "settings.cleanUp.none": "無可清理——所有遊戲都在各自上限之內", + "settings.storage.pickDialog": "選擇快照儲存資料夾", + "settings.storage.pickSyncDialog": "選擇同步前安全備份資料夾", + "settings.storage.scanAddDialog": "新增要掃描的資料夾", + "settings.storage.excludeAddDialog": "選擇要從掃描中排除的資料夾", + "settings.reset.confirmBody": "取消追蹤全部 {n} 個遊戲?它們將從您的遊戲庫中移除,以便您從正確的位置重新新增。磁碟上的存檔快照會保留——不會刪除任何內容。", + "settings.reset.confirmTitle": "重置追蹤?", + "settings.reset.confirmAction": "取消追蹤 {n}", + "settings.reset.doneToast": "已取消追蹤 {n} 個遊戲——快照已保留", + "settings.general.deviceIdentity": "裝置身份", + "settings.general.deviceName": "裝置名稱——其他裝置看到您的名字", + "settings.general.deviceType": "裝置型別", + "settings.general.type.desktop": "桌面電腦(Windows / macOS / Linux PC)", + "settings.general.type.deck": "Steam Deck(SteamOS 掌機)", + "settings.general.type.handheld": "掌機(ROG Ally / Legion Go / 模擬器)", + "settings.general.type.mobile": "伴侶裝置(移動裝置)", + "settings.general.typeHint": "其他裝置發現您時會看到此資訊。", + "settings.general.deviceId": "裝置 ID", + "settings.general.deviceIdHint": "此裝置的唯一網路識別符號(只讀)。", + "settings.general.appearance": "外觀", + "settings.general.controller": "手柄", + "settings.general.notifications": "通知", + "settings.general.library": "遊戲庫", + "settings.general.libraryHint": "遊戲在主頁上的排布方式。更改立即生效。", + "settings.general.setupGuide": "設定嚮導", + "settings.general.setupGuideHint": "主頁上的入門清單:找到您的存檔、新增您的其他裝置、備份到雲端。", + "settings.general.showSetupAgain": "再次顯示", + "settings.general.startup": "啟動", + "settings.general.startOnBoot": "開機時啟動 OpenSave", + "settings.general.startOnBootHint": "以最小化到系統托盤的方式啟動,讓同步在後臺進行。", + "settings.general.updates": "更新", + "settings.general.offerBetas": "向我提供測試版", + "settings.general.betasOnPreRelease": "您正在執行 {version},它本身是測試版,因此無論是否勾選此項,您都會收到更新的測試版——否則在正式版趕上之前您將無路可走。取消勾選後,一旦正式版比您當前的版本更新,您就會切換到正式版。", + "settings.general.betasStable": "測試版釋出更早、包含最新的修復,但測試程度較低。只要正式版更新,您就會收到正式版,所以這不是一條單行道。", + "settings.sync.behavior": "同步行為", + "settings.sync.autoSyncOnTrack": "遊戲首次被追蹤時立即同步", + "settings.sync.detectNewGames": "當出現 OpenSave 未在追蹤其存檔的遊戲時提醒我", + "settings.sync.detectNewGamesHint": "OpenSave 每小時在後臺檢查一次,只提及它沒見過的遊戲。在您選擇之前不會追蹤任何內容。", + "settings.sync.matchByAppId": "按 Steam App ID 跨 PC 匹配存檔", + "settings.sync.matchByAppIdHint": "即使同一遊戲在不同裝置上以不同名稱或磁碟機代號被追蹤,也能將它們關聯起來。", + "settings.sync.matchByAppIdMore": "例如一臺 PC 上的 Steam 版和另一臺上的獨立版。如果您有意保留同一款遊戲互不合並的兩份副本,請保持關閉。您隨時可以在遊戲頁面手動關聯遊戲。", + "settings.sync.unknownGameLabel": "當其他裝置同步了本機沒有的遊戲時", + "settings.sync.unknownGame.track": "自動開始追蹤(推薦)", + "settings.sync.unknownGame.ask": "詢問我儲存在哪裡", + "settings.sync.unknownGameMore": "如果您的存檔位置可能與自動猜測不符——第二塊硬碟、移動過的資料夾,或把存檔放在以您的帳號命名的資料夾中的遊戲——請選擇“{askMe}”。等待指定資料夾的遊戲會顯示在主頁上。", + "settings.sync.askShort": "詢問我", + "settings.sync.bandwidthLimit": "網路頻寬限制", + "settings.sync.speed.unlimited": "不限速(最高速度)", + "settings.sync.speed.low": "100 KB/s(很低)", + "settings.sync.speed.medium": "500 KB/s(中等)", + "settings.sync.speed.high": "1 MB/s(高)", + "settings.sync.speed.veryHigh": "5 MB/s(很高)", + "settings.sync.speed.ultra": "10 MB/s(極高)", + "settings.sync.speedHint": "僅適用於中繼(網際網路)同步——區域網永遠不會被限速。", + "settings.sync.relay": "網際網路中繼", + "settings.sync.relayUrl": "WebSocket 中繼 URL", + "settings.sync.relayUrlHint": "中繼在網際網路上承載同步。從{internetSync}加入房間。", + "settings.sync.internetSync": "聯網同步", + "settings.sync.hostRelay": "在本裝置上託管 WAN 中繼伺服器", + "settings.sync.hostRelayHint": "讓朋友直接連線您,而不是經由公共中繼。", + "settings.sync.relayPort": "中繼託管埠", + "settings.sync.relayPortHint": "在路由器上轉發此 TCP 埠,讓網際網路上的朋友能夠連到您。", + "settings.sync.lookingUp": "查詢中…", + "settings.sync.hideAddresses": "隱藏我的地址", + "settings.sync.showAddresses": "顯示我的地址以便分享", + "settings.sync.shareTitle": "將這些分享給您的朋友", + "settings.sync.lanIps": "區域網 IP:", + "settings.sync.publicIp": "公網 IP:", + "settings.sync.unavailable": "不可用", + "settings.sync.relayPortLabel": "中繼埠:", + "settings.sync.cloudBackupHint": "備份的去向、每個快照是否都傳送到那裡、以及是否從其他裝置取回較新的存檔,這些都在“雲備份”頁面設定。", + "settings.sync.openCloudBackup": "開啟雲備份", "common.cancel": "取消", "common.nowTracking": "已開始跟蹤“{name}”", "home.addGame.adding": "新增中…", diff --git a/cmd/opensave-app/frontend/src/views/Settings.svelte b/cmd/opensave-app/frontend/src/views/Settings.svelte index 2d5fdb9c..f1c04e27 100644 --- a/cmd/opensave-app/frontend/src/views/Settings.svelte +++ b/cmd/opensave-app/frontend/src/views/Settings.svelte @@ -4,6 +4,7 @@ import { settings, toast, askConfirm, gameList, navigate } from '../lib/stores.js'; import { showSetupAgain } from '../lib/setup.js'; import { api, native } from '../lib/api.js'; + import { t, segments } from '../lib/i18n.js'; import { adoptOutsideChanges, changedFields, createAutosave } from '../lib/autosave.js'; import qrcode from 'qrcode-generator'; import { DISCORD_URL, DONATE_URL } from '../lib/links.js'; @@ -94,14 +95,15 @@ let rejected = {}; let refusedWhy = {}; const FIELD_NAMES = { - relayUrl: 'relay URL', - relayPort: 'relay hosting port', - port: 'daemon port', - deviceName: 'device name', - backupsDir: 'snapshots folder', - syncBackupsDir: 'safety backups folder', - pathTranslations: 'path rules' + relayUrl: 'settings.field.relayUrl', + relayPort: 'settings.field.relayPort', + port: 'settings.field.port', + deviceName: 'settings.field.deviceName', + backupsDir: 'settings.field.backupsDir', + syncBackupsDir: 'settings.field.syncBackupsDir', + pathTranslations: 'settings.field.pathTranslations' }; + const fieldName = (key) => (key in FIELD_NAMES ? $t(FIELD_NAMES[key]) : key); $: unsaved = draft ? Object.keys(rejected).filter((key) => same(draft[key], rejected[key])) : []; const firstSentence = (text) => String(text).split(/(?<=\.)\s/)[0]; @@ -174,13 +176,13 @@ try { // Save the limit first so the cleanup uses it, then prune everything. await saver.flush(); - if (saveState?.error) throw new Error(`the limits couldn't be saved: ${saveState.error}`); + if (saveState?.error) throw new Error($t('settings.cleanUp.saveFailed', { error: saveState.error })); const res = await api.post('/api/snapshots/prune', { applyDefaultToAll: true }); const mb = (res.freedBytes / 1048576).toFixed(1); toast( res.removed > 0 - ? `Removed ${res.removed} old snapshot${res.removed === 1 ? '' : 's'}, freed ${mb} MB` - : 'Nothing to clean up — all games are within their limit', + ? $t('settings.cleanUp.removed', { n: res.removed, mb }) + : $t('settings.cleanUp.none'), 'success' ); } catch (e) { @@ -201,7 +203,7 @@ // Custom scan paths async function addScanPath() { - const dir = await native.selectDirectory('Add a folder to scan'); + const dir = await native.selectDirectory($t('settings.storage.scanAddDialog')); if (dir) draft.customScanPaths = [...(draft.customScanPaths ?? []), dir]; saveNow(); } @@ -212,7 +214,7 @@ // Excluded folders — locations the scan should skip entirely. async function addExcludePath() { - const dir = await native.selectDirectory('Choose a folder to exclude from scans'); + const dir = await native.selectDirectory($t('settings.storage.excludeAddDialog')); if (dir) draft.excludePaths = [...(draft.excludePaths ?? []), dir]; saveNow(); } @@ -229,15 +231,16 @@ async function resetTracking() { const n = $gameList.length; if (n === 0) return; - const ok = await askConfirm( - `Untrack all ${n} game${n === 1 ? '' : 's'}? They'll be removed from your library so you can re-add them from the correct locations. Your save snapshots on disk are kept — nothing is deleted.`, - { title: 'Reset tracking?', confirmText: `Untrack ${n}`, danger: true } - ); + const ok = await askConfirm($t('settings.reset.confirmBody', { n }), { + title: $t('settings.reset.confirmTitle'), + confirmText: $t('settings.reset.confirmAction', { n }), + danger: true + }); if (!ok) return; resetting = true; try { const res = await api.post('/api/games/untrack-bulk', { all: true }); - toast(`Untracked ${res.untracked} game${res.untracked === 1 ? '' : 's'} — snapshots kept`, 'success'); + toast($t('settings.reset.doneToast', { n: res.untracked }), 'success'); } catch (e) { toast(e.message, 'error'); } finally { @@ -246,13 +249,13 @@ } async function pickBackupsDir() { - const dir = await native.selectDirectory('Select snapshots storage folder'); + const dir = await native.selectDirectory($t('settings.storage.pickDialog')); if (dir) draft.backupsDir = dir; saveNow(); } async function pickSyncBackupsDir() { - const dir = await native.selectDirectory('Select pre-sync safety backups folder'); + const dir = await native.selectDirectory($t('settings.storage.pickSyncDialog')); if (dir) draft.syncBackupsDir = dir; saveNow(); } @@ -287,22 +290,22 @@
-

Settings

+

{$t('settings.title')}

{#if draft} {#if saveState === 'saving'} - Saving… + {$t('settings.saveState.saving')} {:else if unsaved.length} - Not saved: the {FIELD_NAMES[unsaved[0]] ?? unsaved[0]} — {firstSentence(refusedWhy[unsaved[0]])} + {$t('settings.saveState.notSaved', { field: fieldName(unsaved[0]), why: firstSentence(refusedWhy[unsaved[0]]) })} {:else if saveState?.error} - Couldn't save: {firstSentence(saveState.error)} + {$t('settings.saveState.saveFailed', { error: firstSentence(saveState.error) })} {:else if saveState === 'saved'} - Saved + {$t('settings.saveState.saved')} {:else} - Changes save as you make them + {$t('settings.saveState.autosave')} {/if} {/if} @@ -312,14 +315,14 @@ {:else}
- - - - - + + + + + - +
-

Startup

+

{$t('settings.general.startup')}

- Launches minimized to the system tray so syncing runs in the background. + {$t('settings.general.startOnBootHint')}

-

Updates

+

{$t('settings.general.updates')}

{#if onPreRelease} - You're running {appVersion}, which is a beta, so you'll be offered newer - betas whether or not this is ticked — otherwise there'd be no way forward until the final - release caught up. Untick it and you'll move to the stable release as soon as it's newer - than what you have. + {#each segments($t('settings.general.betasOnPreRelease'), { version: appVersion }, ['version']) as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each} {:else} - Betas ship early with the newest fixes and are less tested. You'll be offered the stable - release whenever it's newer, so this isn't a one-way door. + {$t('settings.general.betasStable')} {/if}

{:else if tab === 'sync'}
-

Sync behavior

+

{$t('settings.sync.behavior')}

- OpenSave looks every hour, in the background, and only mentions games it hasn't seen before. - Nothing is tracked until you choose it. + {$t('settings.sync.detectNewGamesHint')} - Links the same game across devices even when it was tracked under different names or drives. + {$t('settings.sync.matchByAppIdHint')} - For example a Steam copy on one PC and a standalone copy on another. Leave this off if you - deliberately keep two separate copies of the same game that shouldn't merge. You can always - link games by hand from a game's page. + {$t('settings.sync.matchByAppIdMore')}
- + - Tracking automatically works out a folder from where the game lives on the other - device, which is why OpenSave usually needs no setup. + {$t('settings.sync.unknownGameHint')} - Choose Ask me if your saves are somewhere that guess would get wrong — a - second drive, a folder you moved, or a game that keeps saves in a folder named after - your account. Games waiting for a folder appear on the Home page. + {#each segments($t('settings.sync.unknownGameMore'), { askMe: $t('settings.sync.askShort') }, ['askMe']) as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each}
- + - Only applies to relay (internet) syncs — LAN is never throttled. + {$t('settings.sync.speedHint')}
-

Internet relay

+

{$t('settings.sync.relay')}

- + - The relay that carries syncs across the internet. Join a room from Internet Sync. + {#each segments($t('settings.sync.relayUrlHint'), { internetSync: $t('settings.sync.internetSync') }, ['internetSync']) as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each}

- Lets friends connect directly to you instead of the public relay. + {$t('settings.sync.hostRelayHint')}

{#if draft.hostRelay}
- + - Forward this TCP port on your router so friends on the internet can reach you. + {$t('settings.sync.relayPortHint')}
{#if relayInfoShown && relayInfo} {/if} {/if} @@ -529,13 +522,12 @@
-

Cloud backup

+

{$t('home.cloudBackup')}

- Where backups go, whether every snapshot is sent there, and whether newer saves are - brought from your other devices are all set on the Cloud Backup page. + {$t('settings.sync.cloudBackupHint')}

- +
{:else if tab === 'storage'}
From 8836b9be2ac26cc16840b498a13a77ae7a4ccf65 Mon Sep 17 00:00:00 2001 From: Janlhin Date: Fri, 16 Oct 2026 09:32:28 +0800 Subject: [PATCH 3/3] i18n: extract the Settings storage, advanced and support tabs --- cmd/opensave-app/frontend/src/locales/en.json | 63 +++++++++ .../frontend/src/locales/zh-Hans.json | 57 ++++++++ .../frontend/src/locales/zh-Hant.json | 57 ++++++++ .../frontend/src/views/Settings.svelte | 126 ++++++++---------- 4 files changed, 236 insertions(+), 67 deletions(-) diff --git a/cmd/opensave-app/frontend/src/locales/en.json b/cmd/opensave-app/frontend/src/locales/en.json index 1da8a413..d8d89aea 100644 --- a/cmd/opensave-app/frontend/src/locales/en.json +++ b/cmd/opensave-app/frontend/src/locales/en.json @@ -1,6 +1,69 @@ { "settings.language": "Language", "settings.systemDefault": "System default", + "settings.storage.spaceUsed": "Space used", + "settings.storage.canRestore": "Can they be restored?", + "settings.storage.snapshotStorage": "Snapshot storage", + "settings.storage.snapshotsFolder": "Snapshots folder", + "settings.storage.browse": "Browse", + "settings.storage.snapshotsFolderHint": "Where version-history snapshots (ZIP archives) are stored.", + "settings.storage.safetyFolder": "Pre-sync safety backups folder", + "settings.storage.safetyFolderPlaceholder": "Default: ~/.opensave/backups", + "settings.storage.safetyFolderHint": "A safety copy of your save is taken here before every incoming sync, so a bad sync is always reversible.", + "settings.storage.retention": "Retention", + "settings.storage.autoDelete": "Auto-delete old automatic snapshots", + "settings.storage.autoDeleteHint": "The snapshots OpenSave takes on its own — before a sync replaces files, when a game saves, at a conflict. Snapshots you took yourself are kept, and so is the newest one on every branch, whatever its age. Runs shortly after start and every few hours.", + "settings.storage.deleteOlderThan": "Delete when older than", + "settings.storage.days": { + "one": "{n} day", + "other": "{n} days" + }, + "settings.storage.snapshotHistory": "Snapshot history", + "settings.storage.maxAuto": "Automatic snapshots to keep per game", + "settings.storage.maxAutoHint": "Default limit for newly tracked games (0 = keep everything). The oldest are pruned first, per branch. Change a single game's limit in its Configuration tab.", + "settings.storage.maxManual": "Manual snapshots to keep per game", + "settings.storage.maxManualHint": "Snapshots you take yourself have their own budget, so games that auto-save often (Elden Ring, Dragonsword) can't push them out. {forever}, which is the default.", + "settings.storage.keepForever": "0 = keep forever", + "settings.storage.applying": "Applying…", + "settings.storage.applyLimits": "Apply these limits to every game", + "settings.storage.applyLimitsHint": "Replaces each game's own limits with these, then deletes the snapshots beyond them on every branch. To clean up with each game's own limits, use Space used above.", + "settings.storage.scanner": "Game scanner", + "settings.storage.scanPaths": "Extra folders to scan", + "settings.storage.scanPathsHint": "Scanning already checks Steam and common emulators — add custom libraries here.", + "settings.remove": "Remove", + "settings.storage.addFolder": "Add folder", + "settings.storage.excludePaths": "Folders to exclude", + "settings.storage.excludePathsHint": "Scans skip these folders and everything inside them — handy for stale save locations (like an old GSE saves directory) you don't want offered again.", + "settings.storage.excludeFolder": "Exclude folder", + "settings.reset.title": "Reset tracking", + "settings.reset.hint": "Untrack every game at once, then scan again to add them back from the correct locations — useful after moving games between launchers or drives. This only clears the tracking list; your save snapshots on disk are kept.", + "settings.reset.inProgress": "Untracking…", + "settings.reset.none": "No games tracked", + "settings.reset.button": { + "one": "Untrack all {n} game", + "other": "Untrack all {n} games" + }, + "settings.advanced.network": "Network", + "settings.advanced.daemonPort": "Daemon port", + "settings.advanced.daemonPortHint": "The port this device listens on, for the app and for your other devices on the network. Changing it requires a restart.", + "settings.advanced.pathTranslation": "Cross-platform path translation", + "settings.advanced.pathTranslationHint": "Rewrites another device's save paths to this one's conventions, e.g. \"C:\\Users\\me\\Saves\" → \"/home/deck/saves\".", + "settings.advanced.fromPattern": "From pattern", + "settings.advanced.toPattern": "To pattern", + "settings.advanced.addRule": "Add rule", + "settings.support.title": "Support OpenSave", + "settings.support.lede": "Free and open source, and it stays that way — no accounts, no ads, no telemetry, and nothing locked behind a payment.", + "settings.support.body": "It's built and maintained in spare time. If it's saved you the hassle of copying save folders between machines, you're welcome to chip in.", + "settings.support.point1": "Keeps the public relay online", + "settings.support.point2": "More games and emulators detected", + "settings.support.point3": "Time for fixes and new features", + "settings.support.openAgain": "Open again", + "settings.support.openDonate": "Open donation page", + "settings.support.openedToast": "Opened in your browser — thank you.", + "settings.support.foot": "Prefer not to donate? Reporting a bug or suggesting a feature helps just as much.", + "settings.support.scanToPay": "Scan to pay", + "settings.support.fromPhone": "from your phone", + "settings.support.note": "Payment is handled entirely by Gumroad — OpenSave never sees your card details.", "settings.sync.unknownGameHint": "Tracking automatically works out a folder from where the game lives on the other device, which is why OpenSave usually needs no setup.", "settings.title": "Settings", "settings.saveState.saving": "Saving…", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hans.json b/cmd/opensave-app/frontend/src/locales/zh-Hans.json index 6a0300a2..237334a5 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hans.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hans.json @@ -1,6 +1,63 @@ { "settings.language": "语言", "settings.systemDefault": "跟随系统", + "settings.storage.spaceUsed": "已用空间", + "settings.storage.canRestore": "快照能否恢复?", + "settings.storage.snapshotStorage": "快照存储", + "settings.storage.snapshotsFolder": "快照文件夹", + "settings.storage.browse": "浏览", + "settings.storage.snapshotsFolderHint": "版本历史快照(ZIP 压缩包)的存储位置。", + "settings.storage.safetyFolder": "同步前安全备份文件夹", + "settings.storage.safetyFolderPlaceholder": "默认:~/.opensave/backups", + "settings.storage.safetyFolderHint": "每次接收同步之前都会在此保存一份存档的安全副本,因此糟糕的同步总是可以撤销。", + "settings.storage.retention": "保留策略", + "settings.storage.autoDelete": "自动删除旧的自动快照", + "settings.storage.autoDeleteHint": "OpenSave 自己拍摄的快照——在同步替换文件之前、游戏保存时、发生冲突时。您自己拍摄的快照会保留,每个分支上最新的一个也会保留,无论其存在多久。在启动后不久以及每隔几小时运行一次。", + "settings.storage.deleteOlderThan": "删除早于此时间的快照", + "settings.storage.days": "{n} 天", + "settings.storage.snapshotHistory": "快照历史", + "settings.storage.maxAuto": "每个游戏保留的自动快照数", + "settings.storage.maxAutoHint": "新跟踪游戏的默认上限(0 = 全部保留)。最先删除最旧的,按分支计算。单个游戏的上限可在其配置标签页中修改。", + "settings.storage.maxManual": "每个游戏保留的手动快照数", + "settings.storage.maxManualHint": "您自己拍摄的快照有独立的额度,因此频繁自动存档的游戏(如 Elden Ring、Dragonsword)无法把它们挤掉。{forever},这是默认值。", + "settings.storage.keepForever": "0 = 永久保留", + "settings.storage.applying": "正在应用…", + "settings.storage.applyLimits": "将这些上限应用到每个游戏", + "settings.storage.applyLimitsHint": "用这些上限替换每个游戏自己的上限,然后删除每个分支上超出上限的快照。要按每个游戏自己的上限清理,请使用上方的“已用空间”。", + "settings.storage.scanner": "游戏扫描器", + "settings.storage.scanPaths": "额外扫描的文件夹", + "settings.storage.scanPathsHint": "扫描已覆盖 Steam 和常见模拟器——在此添加自定义游戏库。", + "settings.remove": "移除", + "settings.storage.addFolder": "添加文件夹", + "settings.storage.excludePaths": "要排除的文件夹", + "settings.storage.excludePathsHint": "扫描会跳过这些文件夹及其中的所有内容——适合陈旧的存档位置(如旧的 GSE 存档目录),您不想再次看到它们。", + "settings.storage.excludeFolder": "排除文件夹", + "settings.reset.title": "重置跟踪", + "settings.reset.hint": "一次性取消跟踪所有游戏,然后重新扫描以从正确的位置添加回来——在启动器或硬盘之间移动游戏后很有用。这只会清除跟踪列表;磁盘上的存档快照会保留。", + "settings.reset.inProgress": "正在取消跟踪…", + "settings.reset.none": "未跟踪任何游戏", + "settings.reset.button": "取消跟踪全部 {n} 个游戏", + "settings.advanced.network": "网络", + "settings.advanced.daemonPort": "守护进程端口", + "settings.advanced.daemonPortHint": "此设备监听的端口,供应用和网络上您的其他设备使用。更改后需要重启。", + "settings.advanced.pathTranslation": "跨平台路径转换", + "settings.advanced.pathTranslationHint": "将其他设备的存档路径重写为本机的约定,例如 \"C:\\Users\\me\\Saves\" → \"/home/deck/saves\"。", + "settings.advanced.fromPattern": "来源模式", + "settings.advanced.toPattern": "目标模式", + "settings.advanced.addRule": "添加规则", + "settings.support.title": "支持 OpenSave", + "settings.support.lede": "免费且开源,并将一直如此——没有账号、没有广告、没有遥测,也没有任何内容锁在付费墙后。", + "settings.support.body": "它由业余时间构建和维护。如果它为您省去了在机器之间复制存档文件夹的麻烦,欢迎您出一份力。", + "settings.support.point1": "让公共中继保持在线", + "settings.support.point2": "检测到更多游戏和模拟器", + "settings.support.point3": "有时间修复问题、开发新功能", + "settings.support.openAgain": "再次打开", + "settings.support.openDonate": "打开捐赠页面", + "settings.support.openedToast": "已在您的浏览器中打开——谢谢。", + "settings.support.foot": "不想捐赠?提交错误报告或功能建议同样有帮助。", + "settings.support.scanToPay": "用手机扫码", + "settings.support.fromPhone": "完成支付", + "settings.support.note": "支付完全由 Gumroad 处理——OpenSave 永远不会看到您的银行卡信息。", "settings.sync.unknownGameHint": "自动跟踪会根据游戏在其他设备上的位置推算出一个文件夹,这也是 OpenSave 通常无需设置的原因。", "settings.title": "设置", "settings.saveState.saving": "正在保存…", diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hant.json b/cmd/opensave-app/frontend/src/locales/zh-Hant.json index 20ff3d87..ee5157fd 100644 --- a/cmd/opensave-app/frontend/src/locales/zh-Hant.json +++ b/cmd/opensave-app/frontend/src/locales/zh-Hant.json @@ -1,6 +1,63 @@ { "settings.language": "語言", "settings.systemDefault": "跟隨系統", + "settings.storage.spaceUsed": "已用空間", + "settings.storage.canRestore": "快照能否恢復?", + "settings.storage.snapshotStorage": "快照儲存", + "settings.storage.snapshotsFolder": "快照資料夾", + "settings.storage.browse": "瀏覽", + "settings.storage.snapshotsFolderHint": "版本歷史快照(ZIP 壓縮包)的儲存位置。", + "settings.storage.safetyFolder": "同步前安全備份資料夾", + "settings.storage.safetyFolderPlaceholder": "預設:~/.opensave/backups", + "settings.storage.safetyFolderHint": "每次接收同步之前都會在此儲存一份存檔的安全副本,因此糟糕的同步總是可以撤銷。", + "settings.storage.retention": "保留策略", + "settings.storage.autoDelete": "自動刪除舊的自動快照", + "settings.storage.autoDeleteHint": "OpenSave 自己拍攝的快照——在同步替換檔案之前、遊戲儲存時、發生衝突時。您自己拍攝的快照會保留,每個分支上最新的一個也會保留,無論其存在多久。在啟動後不久以及每隔幾小時執行一次。", + "settings.storage.deleteOlderThan": "刪除早於此時間的快照", + "settings.storage.days": "{n} 天", + "settings.storage.snapshotHistory": "快照歷史", + "settings.storage.maxAuto": "每個遊戲保留的自動快照數", + "settings.storage.maxAutoHint": "新追蹤遊戲的預設上限(0 = 全部保留)。最先刪除最舊的,按分支計算。單個遊戲的上限可在其配置標籤頁中修改。", + "settings.storage.maxManual": "每個遊戲保留的手動快照數", + "settings.storage.maxManualHint": "您自己拍攝的快照有獨立的額度,因此頻繁自動存檔的遊戲(如 Elden Ring、Dragonsword)無法把它們擠掉。{forever},這是預設值。", + "settings.storage.keepForever": "0 = 永久保留", + "settings.storage.applying": "正在應用…", + "settings.storage.applyLimits": "將這些上限應用到每個遊戲", + "settings.storage.applyLimitsHint": "用這些上限替換每個遊戲自己的上限,然後刪除每個分支上超出上限的快照。要按每個遊戲自己的上限清理,請使用上方的“已用空間”。", + "settings.storage.scanner": "遊戲掃描器", + "settings.storage.scanPaths": "額外掃描的資料夾", + "settings.storage.scanPathsHint": "掃描已覆蓋 Steam 和常見模擬器——在此新增自定義遊戲庫。", + "settings.remove": "移除", + "settings.storage.addFolder": "新增資料夾", + "settings.storage.excludePaths": "要排除的資料夾", + "settings.storage.excludePathsHint": "掃描會跳過這些資料夾及其中的所有內容——適合陳舊的存檔位置(如舊的 GSE 存檔目錄),您不想再次看到它們。", + "settings.storage.excludeFolder": "排除資料夾", + "settings.reset.title": "重置追蹤", + "settings.reset.hint": "一次性取消追蹤所有遊戲,然後重新掃描以從正確的位置新增回來——在啟動器或硬碟之間移動遊戲後很有用。這隻會清除追蹤列表;磁碟上的存檔快照會保留。", + "settings.reset.inProgress": "正在取消追蹤…", + "settings.reset.none": "未追蹤任何遊戲", + "settings.reset.button": "取消追蹤全部 {n} 個遊戲", + "settings.advanced.network": "網路", + "settings.advanced.daemonPort": "守護程序埠", + "settings.advanced.daemonPortHint": "此裝置監聽的埠,供應用和網路上您的其他裝置使用。更改後需要重啟。", + "settings.advanced.pathTranslation": "跨平臺路徑轉換", + "settings.advanced.pathTranslationHint": "將其他裝置的存檔路徑重寫為本機的約定,例如 \"C:\\Users\\me\\Saves\" → \"/home/deck/saves\"。", + "settings.advanced.fromPattern": "來源模式", + "settings.advanced.toPattern": "目標模式", + "settings.advanced.addRule": "新增規則", + "settings.support.title": "支援 OpenSave", + "settings.support.lede": "免費且開源,並將一直如此——沒有帳號、沒有廣告、沒有遙測,也沒有任何內容鎖在付費牆後。", + "settings.support.body": "它由業餘時間構建和維護。如果它為您省去了在機器之間複製存檔資料夾的麻煩,歡迎您出一份力。", + "settings.support.point1": "讓公共中繼保持線上", + "settings.support.point2": "檢測到更多遊戲和模擬器", + "settings.support.point3": "有時間修復問題、開發新功能", + "settings.support.openAgain": "再次開啟", + "settings.support.openDonate": "開啟捐贈頁面", + "settings.support.openedToast": "已在您的瀏覽器中開啟——謝謝。", + "settings.support.foot": "不想捐贈?提交錯誤報告或功能建議同樣有幫助。", + "settings.support.scanToPay": "用手機掃碼", + "settings.support.fromPhone": "完成支付", + "settings.support.note": "支付完全由 Gumroad 處理——OpenSave 永遠不會看到您的銀行卡資訊。", "settings.sync.unknownGameHint": "自動追蹤會根據遊戲在其他裝置上的位置推算出一個資料夾,這也是 OpenSave 通常無需設定的原因。", "settings.title": "設定", "settings.saveState.saving": "正在儲存…", diff --git a/cmd/opensave-app/frontend/src/views/Settings.svelte b/cmd/opensave-app/frontend/src/views/Settings.svelte index f1c04e27..15a7903a 100644 --- a/cmd/opensave-app/frontend/src/views/Settings.svelte +++ b/cmd/opensave-app/frontend/src/views/Settings.svelte @@ -531,159 +531,153 @@
{:else if tab === 'storage'}
-

Space used

+

{$t('settings.storage.spaceUsed')}

-

Can they be restored?

+

{$t('settings.storage.canRestore')}

-

Snapshot storage

+

{$t('settings.storage.snapshotStorage')}

- +
- +
- Where version-history snapshots (ZIP archives) are stored. + {$t('settings.storage.snapshotsFolderHint')}
- +
- - + +
- A safety copy of your save is taken here before every incoming sync, so a bad sync is always reversible. + {$t('settings.storage.safetyFolderHint')}
-

Retention

+

{$t('settings.storage.retention')}

- The snapshots OpenSave takes on its own — before a sync replaces files, when a game saves, at a - conflict. Snapshots you took yourself are kept, and so is the newest one on every branch, whatever - its age. Runs shortly after start and every few hours. + {$t('settings.storage.autoDeleteHint')} {#if draft.autoDeleteBackups}
- +
{/if}
-

Snapshot history

+

{$t('settings.storage.snapshotHistory')}

- + - Default limit for newly tracked games (0 = keep everything). The oldest are pruned first, - per branch. Change a single game's limit in its Configuration tab. + {$t('settings.storage.maxAutoHint')}
- + - Snapshots you take yourself have their own budget, so games that auto-save often - (Elden Ring, Dragonsword) can't push them out. 0 = keep forever, which is - the default. + {#each segments($t('settings.storage.maxManualHint'), { forever: $t('settings.storage.keepForever') }, ['forever']) as part}{#if part.bold}{part.text}{:else}{part.text}{/if}{/each}
- Replaces each game's own limits with these, then deletes the snapshots beyond them on - every branch. To clean up with each game's own limits, use Space used above. + {$t('settings.storage.applyLimitsHint')}
-

Game scanner

+

{$t('settings.storage.scanner')}

- - Scanning already checks Steam and common emulators — add custom libraries here. + + {$t('settings.storage.scanPathsHint')} {#each draft.customScanPaths ?? [] as p, i}
{p} - +
{/each} - +
- - Scans skip these folders and everything inside them — handy for stale save locations (like an old GSE saves directory) you don't want offered again. + + {$t('settings.storage.excludePathsHint')} {#each draft.excludePaths ?? [] as p, i}
{p} - +
{/each} - +
-

Reset tracking

+

{$t('settings.reset.title')}

- Untrack every game at once, then scan again to add them back from the correct locations — useful after moving games between launchers or drives. This only clears the tracking list; your save snapshots on disk are kept. + {$t('settings.reset.hint')}
{:else if tab === 'advanced'}
-

Network

+

{$t('settings.advanced.network')}

- + - The port this device listens on, for the app and for your other devices on the network. Changing it requires a restart. + {$t('settings.advanced.daemonPortHint')}
-

Cross-platform path translation

+

{$t('settings.advanced.pathTranslation')}

- Rewrites another device's save paths to this one's conventions, e.g. "C:\Users\me\Saves" → "/home/deck/saves". + {$t('settings.advanced.pathTranslationHint')} {#each draft.pathTranslations ?? [] as rule, i}
- + - - + +
{/each} - +
{:else if tab === 'support'} @@ -691,10 +685,9 @@
-

Support OpenSave

+

{$t('settings.support.title')}

- Free and open source, and it stays that way — no accounts, no ads, no telemetry, - and nothing locked behind a payment. + {$t('settings.support.lede')}

@@ -702,25 +695,24 @@

- It's built and maintained in spare time. If it's saved you the hassle of copying - save folders between machines, you're welcome to chip in. + {$t('settings.support.body')}

    -
  • Keeps the public relay online
  • -
  • More games and emulators detected
  • -
  • Time for fixes and new features
  • +
  • {$t('settings.support.point1')}
  • +
  • {$t('settings.support.point2')}
  • +
  • {$t('settings.support.point3')}
{#if donateOpened} - Opened in your browser — thank you. + {$t('settings.support.openedToast')} {/if}

- Prefer not to donate? Reporting a bug or suggesting a feature helps just as much. + {$t('settings.support.foot')}

@@ -729,12 +721,12 @@ image service. Kept on a light plate because phone cameras struggle with inverted (light-on-dark) QR codes. -->
{@html qrSvg}
- Scan to pay
from your phone
+ {$t('settings.support.scanToPay')}
{$t('settings.support.fromPhone')}

- Payment is handled entirely by Gumroad — OpenSave never sees your card details. + {$t('settings.support.note')}

{/if}