diff --git a/cmd/opensave-app/frontend/src/lib/i18n.js b/cmd/opensave-app/frontend/src/lib/i18n.js
index 7c4ca599..4de76310 100644
--- a/cmd/opensave-app/frontend/src/lib/i18n.js
+++ b/cmd/opensave-app/frontend/src/lib/i18n.js
@@ -14,20 +14,30 @@
import { writable, derived } from 'svelte/store';
import en from '../locales/en.json';
-import zhCN from '../locales/zh-CN.json';
+import zhHans from '../locales/zh-Hans.json';
+import zhHant from '../locales/zh-Hant.json';
export const SYSTEM_LOCALE = 'system';
-/** The languages offered besides the system default. */
-export const LOCALES = { en: 'English', 'zh-CN': '简体中文' };
+/** The languages offered besides the system default.
+ *
+ * Chinese is split by script, not by region: which characters a reader wants
+ * is not the same question as where they are, so `zh-CN` / `zh-TW` / `zh-HK`
+ * and the rest are not offered as separate languages. Each region maps onto
+ * one of the two scripts here instead. */
+export const LOCALES = { en: 'English', 'zh-Hans': '简体中文', 'zh-Hant': '繁體中文' };
-const CATALOGS = { en, 'zh-CN': zhCN };
+const CATALOGS = { en, 'zh-Hans': zhHans, 'zh-Hant': zhHant };
const FALLBACK = 'en';
const KEY = 'opensave.locale';
-/** A locale id if valid, otherwise null. */
+/** A locale id if valid, otherwise null. A Chinese tag saved before the
+ * script split (`zh-CN`, `zh-TW`, `zh-HK`, bare `zh`, …) maps through
+ * `scriptFor`, so someone who picked Chinese keeps it. */
export function sanitizeLocale(raw) {
- return typeof raw === 'string' && (raw === SYSTEM_LOCALE || raw in LOCALES) ? raw : null;
+ if (typeof raw !== 'string') return null;
+ if (raw === SYSTEM_LOCALE || raw in LOCALES) return raw;
+ return scriptFor(raw);
}
/** The saved choice, or 'system' when the user has never picked one. */
@@ -49,10 +59,25 @@ export function saveLocale(l, storage = globalThis.localStorage) {
}
}
+/** Regions whose Chinese is written with traditional characters. Every other
+ * Chinese region — and a bare 'zh' — is read as simplified. */
+const TRADITIONAL_REGIONS = ['tw', 'hk', 'mo'];
+
+/** The Chinese script an OS/browser language tag is written in, or null when
+ * the tag is not Chinese. A tag that already names its script wins over the
+ * region guess. */
+export function scriptFor(language) {
+ const tag = (language ?? '').toLowerCase();
+ if (!tag.startsWith('zh')) return null;
+ if (tag.startsWith('zh-hant')) return 'zh-Hant';
+ if (tag.startsWith('zh-hans')) return 'zh-Hans';
+ return TRADITIONAL_REGIONS.includes(tag.split('-')[1]) ? 'zh-Hant' : 'zh-Hans';
+}
+
/** 'system' settled against the OS language, or the explicit pick itself. */
export function resolveLocale(l, language = globalThis.navigator?.language) {
if (l && l !== SYSTEM_LOCALE) return l;
- return (language ?? '').toLowerCase().startsWith('zh') ? 'zh-CN' : FALLBACK;
+ return scriptFor(language) ?? FALLBACK;
}
/** The chosen language. Setting it persists immediately ('system' unpersists). */
diff --git a/cmd/opensave-app/frontend/src/lib/i18n.test.js b/cmd/opensave-app/frontend/src/lib/i18n.test.js
index efa936f3..45e761e8 100644
--- a/cmd/opensave-app/frontend/src/lib/i18n.test.js
+++ b/cmd/opensave-app/frontend/src/lib/i18n.test.js
@@ -1,30 +1,39 @@
import { describe, expect, it } from 'vitest';
import en from '../locales/en.json';
-import zhCN from '../locales/zh-CN.json';
-import { LOCALES, loadLocale, pluralRulesFor, resolveLocale, sanitizeLocale, segments, translate } from './i18n.js';
+import zhHans from '../locales/zh-Hans.json';
+import zhHant from '../locales/zh-Hant.json';
+import { LOCALES, loadLocale, pluralRulesFor, resolveLocale, sanitizeLocale, scriptFor, segments, translate } from './i18n.js';
// The catalogs are flat, dot-keyed objects; a plural value is one value that
// must expose an "other" form. Comparing top-level keys is enough.
const keysOf = (catalog) => Object.keys(catalog);
+/** Every catalog, as [locale id, catalog]. A new language is one line here
+ * plus one in LOCALES; the checks below then cover it for free. */
+const CATALOGS = [['en', en], ['zh-Hans', zhHans], ['zh-Hant', zhHant]];
+
describe('locale catalogs', () => {
it('declares every catalog id in LOCALES', () => {
- for (const id of ['en', 'zh-CN']) expect(LOCALES[id], id).toBeTruthy();
+ for (const [id] of CATALOGS) expect(LOCALES[id], id).toBeTruthy();
});
- it('has no keys in zh-CN that en.json does not have', () => {
+ it('has no key outside en.json in any catalog', () => {
const enKeys = new Set(keysOf(en));
- for (const key of keysOf(zhCN)) expect(enKeys.has(key), `extra key: ${key}`).toBe(true);
+ for (const [name, catalog] of CATALOGS) {
+ for (const key of keysOf(catalog)) expect(enKeys.has(key), `${name}: extra key: ${key}`).toBe(true);
+ }
});
- it('has a zh-CN value for every key in en.json', () => {
- const zhKeys = new Set(keysOf(zhCN));
- for (const key of keysOf(en)) expect(zhKeys.has(key), `missing key: ${key}`).toBe(true);
+ it('has a value for every key in en.json, in every catalog', () => {
+ for (const [name, catalog] of CATALOGS) {
+ const keys = new Set(keysOf(catalog));
+ for (const key of keysOf(en)) expect(keys.has(key), `${name}: missing key: ${key}`).toBe(true);
+ }
});
it('keeps every value non-empty, and plurals with an "other" form', () => {
- for (const [catalog, name] of [[en, 'en'], [zhCN, 'zh-CN']]) {
+ for (const [name, catalog] of CATALOGS) {
for (const [key, v] of Object.entries(catalog)) {
if (v !== null && typeof v === 'object') {
expect(typeof v.other, `${name}:${key} plural needs "other"`).toBe('string');
@@ -74,22 +83,47 @@ describe('segments', () => {
});
describe('locale choice', () => {
- it('rejects unknown locale ids', () => {
+ it('rejects unknown locale ids, and region tags that are not languages', () => {
expect(sanitizeLocale('fr')).toBe(null);
- expect(sanitizeLocale('zh-CN')).toBe('zh-CN');
+ expect(sanitizeLocale('zh-Hans')).toBe('zh-Hans');
+ expect(sanitizeLocale('zh-Hant')).toBe('zh-Hant');
expect(sanitizeLocale('system')).toBe('system');
});
+ it('maps a Chinese choice saved before the script split onto its script', () => {
+ expect(sanitizeLocale('zh-CN')).toBe('zh-Hans');
+ expect(sanitizeLocale('zh-SG')).toBe('zh-Hans');
+ expect(sanitizeLocale('zh-TW')).toBe('zh-Hant');
+ expect(sanitizeLocale('zh-HK')).toBe('zh-Hant');
+ expect(sanitizeLocale('zh')).toBe('zh-Hans');
+ expect(loadLocale({ getItem: () => 'zh-CN' })).toBe('zh-Hans');
+ expect(loadLocale({ getItem: () => 'zh-TW' })).toBe('zh-Hant');
+ });
+
it("reports 'system' when the user has never picked one", () => {
expect(loadLocale({ getItem: () => null })).toBe('system');
- expect(loadLocale({ getItem: () => 'zh-CN' })).toBe('zh-CN');
+ expect(loadLocale({ getItem: () => 'zh-Hant' })).toBe('zh-Hant');
});
it('resolves system against the OS language, keeps explicit picks', () => {
- expect(resolveLocale('system', 'zh-CN')).toBe('zh-CN');
+ expect(resolveLocale('system', 'zh-CN')).toBe('zh-Hans');
+ expect(resolveLocale('system', 'zh-TW')).toBe('zh-Hant');
expect(resolveLocale('system', 'en-US')).toBe('en');
expect(resolveLocale('system', '')).toBe('en');
- expect(resolveLocale('zh-CN', 'en-US')).toBe('zh-CN');
+ expect(resolveLocale('zh-Hant', 'en-US')).toBe('zh-Hant');
+ });
+
+ it('reads the Chinese script out of an OS language tag', () => {
+ expect(scriptFor('zh-CN')).toBe('zh-Hans');
+ expect(scriptFor('zh-SG')).toBe('zh-Hans');
+ expect(scriptFor('zh-Hans-CN')).toBe('zh-Hans');
+ expect(scriptFor('zh')).toBe('zh-Hans');
+ expect(scriptFor('zh-TW')).toBe('zh-Hant');
+ expect(scriptFor('zh-HK')).toBe('zh-Hant');
+ expect(scriptFor('zh-MO')).toBe('zh-Hant');
+ expect(scriptFor('zh-Hant-HK')).toBe('zh-Hant');
+ expect(scriptFor('en-US')).toBe(null);
+ expect(scriptFor('')).toBe(null);
});
it('survives storage that refuses to read', () => {
diff --git a/cmd/opensave-app/frontend/src/lib/restoreprogress.test.js b/cmd/opensave-app/frontend/src/lib/restoreprogress.test.js
index 570bb7a2..8d0408cf 100644
--- a/cmd/opensave-app/frontend/src/lib/restoreprogress.test.js
+++ b/cmd/opensave-app/frontend/src/lib/restoreprogress.test.js
@@ -30,7 +30,7 @@ describe('restoreProgressText', () => {
expect(say('en', { phase: 'later', done: 0, total: 0 })).toContain('Restoring');
});
it('says the line in the chosen language', () => {
- const line = say('zh-CN', { phase: 'writing', done: 60000, total: 240000, secondsLeft: 150 });
+ const line = say('zh-Hans', { phase: 'writing', done: 60000, total: 240000, secondsLeft: 150 });
expect(line).toContain('正在写入文件');
expect(line).toContain('25%');
expect(line).toContain('约 3 分钟');
diff --git a/cmd/opensave-app/frontend/src/locales/en.json b/cmd/opensave-app/frontend/src/locales/en.json
index 562805af..37e34a1f 100644
--- a/cmd/opensave-app/frontend/src/locales/en.json
+++ b/cmd/opensave-app/frontend/src/locales/en.json
@@ -189,7 +189,7 @@
"home.welcome.title": "Welcome to OpenSave",
"home.welcome.trackManually": "Track a folder manually",
"home.trackGame": "Track game",
- "home.trackGameTitle": "Find a game by name, or choose its program or save folder",
+ "home.trackGameHint": "Find a game by name, or choose its program or save folder",
"home.addGame.recognisedAs": "Recognised as \"{name}\" — its cover art comes from the game database.",
"home.findGame.title": "Track a game from the game database",
"home.findGame.findByName": "Find by name",
@@ -231,5 +231,32 @@
},
"game.config.deviceSettings": "Device settings",
"game.config.syncDeviceSettings": "Sync these settings between devices too",
- "game.config.deviceSettingsHint": "The game database lists these as this game's settings, not its save, so each device keeps its own (graphics, resolution). They are still in every snapshot."
+ "game.config.deviceSettingsHint": "The game database lists these as this game's settings, not its save, so each device keeps its own (graphics, resolution). They are still in every snapshot.",
+ "common.close": "Close",
+ "common.dismiss": "Dismiss",
+ "common.loading": "Loading",
+ "titleBar.minimise": "Minimise",
+ "titleBar.maximise": "Maximise",
+ "statusBar.reconnecting": "Reconnecting…",
+ "statusBar.reconnectingTitle": "The app has lost touch with its background service and is reconnecting",
+ "statusBar.noSyncs": "No syncs in progress",
+ "statusBar.syncing": {
+ "one": "Syncing {n} game…",
+ "other": "Syncing {n} games…"
+ },
+ "statusBar.devicesOnline": {
+ "one": "{n} device online",
+ "other": "{n} devices online"
+ },
+ "statusBar.resume": "Resume",
+ "statusBar.resumeTitle": "Resume syncing and catch up",
+ "statusBar.pause": "Pause syncing",
+ "statusBar.transfersHint": "What is moving between your devices",
+ "statusBar.paletteHint": "Jump to a game, a page or an action",
+ "statusBar.aboutTitle": "About OpenSave",
+ "transfers.title": "Transfers",
+ "transfers.empty": "Nothing has moved between your devices since OpenSave started.",
+ "transfers.now": "Now",
+ "transfers.recently": "Recently",
+ "transfers.readError": "Couldn't read transfers: {error}"
}
diff --git a/cmd/opensave-app/frontend/src/locales/zh-CN.json b/cmd/opensave-app/frontend/src/locales/zh-Hans.json
similarity index 88%
rename from cmd/opensave-app/frontend/src/locales/zh-CN.json
rename to cmd/opensave-app/frontend/src/locales/zh-Hans.json
index d4718ae1..61697d1a 100644
--- a/cmd/opensave-app/frontend/src/locales/zh-CN.json
+++ b/cmd/opensave-app/frontend/src/locales/zh-Hans.json
@@ -168,13 +168,13 @@
"home.welcome.title": "欢迎使用 OpenSave",
"home.welcome.trackManually": "手动跟踪文件夹",
"home.trackGame": "跟踪游戏",
- "home.trackGameTitle": "按名称查找游戏,或选择其程序或存档文件夹",
+ "home.trackGameHint": "按名称查找游戏,或选择其程序或存档文件夹",
"home.addGame.recognisedAs": "已识别为“{name}”——封面来自游戏数据库。",
"home.findGame.title": "从游戏数据库跟踪游戏",
"home.findGame.findByName": "按名称查找",
"home.findGame.searchPlaceholder": "例如:Crimson Desert",
"home.findGame.searching": "搜索中…",
- "home.findGame.noSavesHere": "此电脑上未找到存档",
+ "home.findGame.noSavesHere": "此电脑上没有找到它的存档",
"home.findGame.trackedAs": "已作为“{name}”跟踪",
"home.findGame.nothingFound": "游戏数据库中没有这个名称的游戏。",
"home.findGame.chooseProgram": "选择游戏程序…",
@@ -182,14 +182,14 @@
"home.findGame.programDialog": "选择游戏程序",
"home.findGame.saveFolderDialog": "选择游戏的存档文件夹",
"home.findGame.saveFolderOf": "“{name}”的存档文件夹",
- "home.findGame.theGame": "该游戏",
+ "home.findGame.theGame": "这款游戏",
"home.findGame.notKnown": "不在游戏数据库中——请为它命名;如果尚未设置存档文件夹,请选择一个。",
"home.findGame.saveFolder": "存档文件夹",
- "home.findGame.noSavesYet": "此电脑上尚未找到它的存档。请选择它的存档位置,或先玩一次再跟踪。",
+ "home.findGame.noSavesYet": "此电脑上还没有找到它的存档。请选择它的存档文件夹,或先玩一次再跟踪。",
"home.findGame.otherSaveFolder": "选择其他存档文件夹…",
- "home.findGame.program": "程序",
+ "home.findGame.program": "游戏程序",
"home.findGame.pickAnother": "← 选择其他游戏",
- "home.findGame.already": "此处已作为“{tracked}”跟踪。再次跟踪会将相同的文件同步两次;请改为将该条目关联到 {name},使其获得游戏数据库中的名称、App ID 和封面。",
+ "home.findGame.already": "此处已作为“{tracked}”跟踪。再次跟踪会将相同的文件同步两次;请把该条目关联到 {name},使其获得游戏数据库中的名称、App ID 和封面。",
"home.findGame.link": "将“{tracked}”关联到 {name}",
"home.findGame.linked": "“{tracked}”现在是 {name}",
"restoreProgress.rebuilding": "正在重建快照",
@@ -204,5 +204,26 @@
"snapshots.incomplete.more": " 等另外 {n} 个文件",
"game.config.deviceSettings": "设备设置",
"game.config.syncDeviceSettings": "也在设备之间同步这些设置",
- "game.config.deviceSettingsHint": "游戏数据库将这些文件列为该游戏的设置而非存档,因此每台设备保留各自的设置(画质、分辨率)。它们仍包含在每个快照中。"
+ "game.config.deviceSettingsHint": "游戏数据库将这些文件列为该游戏的设置而非存档,因此每台设备保留各自的设置(画质、分辨率)。它们仍包含在每个快照中。",
+ "common.close": "关闭",
+ "common.dismiss": "忽略",
+ "common.loading": "加载中",
+ "titleBar.minimise": "最小化",
+ "titleBar.maximise": "最大化",
+ "statusBar.reconnecting": "正在重连…",
+ "statusBar.reconnectingTitle": "应用与后台服务失去联系,正在重连",
+ "statusBar.noSyncs": "当前没有进行中的同步",
+ "statusBar.syncing": "正在同步 {n} 个游戏…",
+ "statusBar.devicesOnline": "{n} 台设备在线",
+ "statusBar.resume": "恢复",
+ "statusBar.resumeTitle": "恢复同步并补齐",
+ "statusBar.pause": "暂停同步",
+ "statusBar.transfersHint": "设备之间正在传输的内容",
+ "statusBar.paletteHint": "跳转到游戏、页面或操作",
+ "statusBar.aboutTitle": "关于 OpenSave",
+ "transfers.title": "传输",
+ "transfers.empty": "自 OpenSave 启动以来,设备之间还没有传输过任何内容。",
+ "transfers.now": "进行中",
+ "transfers.recently": "最近",
+ "transfers.readError": "无法读取传输记录:{error}"
}
diff --git a/cmd/opensave-app/frontend/src/locales/zh-Hant.json b/cmd/opensave-app/frontend/src/locales/zh-Hant.json
new file mode 100644
index 00000000..4db04909
--- /dev/null
+++ b/cmd/opensave-app/frontend/src/locales/zh-Hant.json
@@ -0,0 +1,229 @@
+{
+ "settings.language": "語言",
+ "settings.systemDefault": "跟隨系統",
+ "common.cancel": "取消",
+ "common.open": "開啟",
+ "common.nowTracking": "已開始跟踪“{name}”",
+ "home.addGame.adding": "新增中…",
+ "home.addGame.changeFolder": "更換資料夾…",
+ "home.addGame.chooseFile": "選擇要跟踪的存檔檔案",
+ "home.addGame.name": "名稱",
+ "home.addGame.nameHint": "它顯示在你的遊戲庫中,也會顯示在其他裝置上。",
+ "home.addGame.namePlaceholder": "例如:Elden Ring",
+ "home.addGame.savePath": "存檔資料夾或檔案",
+ "home.addGame.startTracking": "開始跟踪",
+ "home.addGame.title": "跟踪遊戲",
+ "home.addGame.trackFileInstead": "改為跟踪單個檔案…",
+ "home.library.bulkDone": "已為 {n} 個遊戲{verb}",
+ "home.library.bulkPartial": "已為 {done}/{total} 個遊戲{verb} — {failed}",
+ "home.library.clearFilters": "清除篩選",
+ "home.library.collectionsLabel": "收藏夾",
+ "home.library.empty": "沒有符合條件的遊戲。",
+ "home.library.emptyFor": "沒有匹配“{query}”的遊戲。",
+ "home.library.favourite": "收藏",
+ "home.library.favouriteTitle": "將每個遊戲加入收藏",
+ "home.library.reverseTitle": "倒序排列",
+ "home.library.reversedTitle": "已倒序 — 恢復原來的順序",
+ "home.library.searchLabel": "查詢遊戲",
+ "home.library.searchPlaceholder": "查詢遊戲…",
+ "home.library.select": "選擇",
+ "home.library.selectAll": "全選({count})",
+ "home.library.selectedCount": "已選 {n} 個",
+ "home.library.showAll": "顯示全部",
+ "home.library.showing": "顯示 {shown}/{total} 個。",
+ "home.library.snapshot": "快照",
+ "home.library.snapshotTitle": "為每個遊戲建立快照",
+ "home.library.sort": "排序",
+ "home.library.sync": "同步",
+ "home.library.syncTitle": "將每個遊戲與你的裝置同步",
+ "home.library.title": "遊戲庫",
+ "home.library.unselectAll": "取消全選",
+ "home.library.untrackSelected": "取消跟踪所選",
+ "home.library.verbFavourite": "加入收藏",
+ "home.library.verbSnapshot": "建立快照",
+ "home.library.verbSync": "完成同步",
+ "home.library.view": "檢視",
+ "home.library.viewHint": "也可以在設定 → 常規中調整。",
+ "home.library.viewLabel": "遊戲庫檢視",
+ "home.offered.chooseFolder": "選擇資料夾…",
+ "home.offered.decline": "拒絕",
+ "home.offered.declineToast": "不會再詢問你關於 {name} 的事,之後仍可自行跟踪。",
+ "home.offered.folderPrompt": "在本機上為“{name}”選擇資料夾",
+ "home.offered.intro": "其他裝置正在同步這些遊戲,但本機還不知道該把存檔放在哪裡。選擇資料夾後才會開始同步。",
+ "home.offered.pathHint": "在另一臺裝置上位於 {path}。",
+ "home.offered.title": "等待選擇資料夾",
+ "home.offered.trackedToast": "{name} 已在本機開始跟踪",
+ "home.recent.allActivity": "全部動態",
+ "home.recent.empty": "暫無記錄。快照和同步會在發生時顯示在這裡。",
+ "home.recent.label": "最近動態",
+ "home.scan.alreadyTracked": "已跟踪 ({n})",
+ "home.scan.badResponse": "掃描返回的不是存檔列表。請重啟 OpenSave 後重新掃描。",
+ "home.scan.clear": "清除選擇",
+ "home.scan.emptyNoMatches": "沒有符合此篩選條件的結果。",
+ "home.scan.emptyNothingDetected": "未檢測到任何存檔。你仍可以手動跟踪任意資料夾。",
+ "home.scan.exclude": "排除",
+ "home.scan.excludeTitle": "不再在後續掃描中顯示此位置",
+ "home.scan.excludeToast": "“{name}”不會再出現在掃描結果中。",
+ "home.scan.excluding": "正在排除…",
+ "home.scan.filterPlaceholder": "按名稱或路徑篩選…",
+ "home.scan.folderList.headFound": "已在 {n} 個位置中找到 {name}。",
+ "home.scan.folderList.headTick": "勾選屬於此存檔的資料夾——它們會作為一個遊戲來跟踪。",
+ "home.scan.folderList.tagAlt": "另一個副本——可能是舊的安裝",
+ "home.scan.folderList.tagInside": "已包含在上面的資料夾中",
+ "home.scan.folderList.tagLocation": "同一存檔的一部分",
+ "home.scan.folderList.tagSave": "存檔資料夾",
+ "home.scan.folderList.tagTracked": "已跟踪",
+ "home.scan.foundInFolders": "在 {n} 個資料夾中找到",
+ "home.scan.foundSummary": "已找到 {n} 個存檔位置——{m} 個可跟踪",
+ "home.scan.foundSummaryEmpty": "已找到 {n} 個存檔位置——{m} 個可跟踪,{e} 個空資料夾已隱藏",
+ "home.scan.mergedPartialFail": "已跟踪“{name}”,但有 {n} 個資料夾失敗:{detail}",
+ "home.scan.oneSave": "· 其中 {n} 個屬於同一存檔",
+ "home.scan.scanningDetail": "正在掃描 Steam、模擬器和已配置的資料夾…",
+ "home.scan.selectAll": "全選 ({n})",
+ "home.scan.showEmpty": "顯示 {n} 個空資料夾",
+ "home.scan.showEmptyTitle": "這些資料夾確實存在但裡面沒有檔案——Steam 會為你擁有的每個遊戲都建一個,無論存檔是否放在其中。完全沒有存檔的遊戲也會出現在這裡。",
+ "home.scan.showTracked": "顯示已跟踪",
+ "home.scan.showTrackedTitle": "同時顯示已跟踪的存檔",
+ "home.scan.subScanning": "正在掃描你的系統…",
+ "home.scan.tab.all": "全部",
+ "home.scan.tab.emulators": "模擬器",
+ "home.scan.tab.games": "遊戲",
+ "home.scan.tab.repacks": "硬碟版",
+ "home.scan.title": "掃描結果",
+ "home.scan.track": "跟踪",
+ "home.scan.trackAll": "跟踪全部 {n} 個",
+ "home.scan.trackAsOne": "作為一個遊戲跟踪",
+ "home.scan.trackAsOneTitle": "將勾選的所有資料夾作為一個遊戲跟踪,不考慮掃描給出的分組。適用於掃描未能識別的拆分存檔。",
+ "home.scan.trackPartialFail": "正在跟踪“{name}”——有 {n} 個資料夾未能新增:{detail}",
+ "home.scan.trackSelected": "跟踪所選 ({n})",
+ "home.scan.tracked": "已跟踪",
+ "home.scan.trackedAcrossFolders": "已跟踪“{name}”,共 {n} 個資料夾",
+ "home.scan.trackedGames": "已跟踪 {n} 個遊戲",
+ "home.scan.trackedGamesWithExtras": "已跟踪 {n} 個遊戲,並附加 {m} 個位置",
+ "home.scan.type.emulator": "模擬器",
+ "home.scan.type.game": "遊戲",
+ "home.scan.type.repack": "硬碟版",
+ "home.setup.chooseWhere": "選擇位置",
+ "home.setup.hideTitle": "隱藏此引導",
+ "home.setup.later": "稍後",
+ "home.setup.noCloudBackup": "不做雲備份",
+ "home.setup.onlyThisDevice": "暫時只用這臺裝置",
+ "home.setup.pairDevice": "配對裝置",
+ "home.setup.pickFolder": "選擇資料夾",
+ "home.setup.progress": "已完成 {n} / 3",
+ "home.setup.skipped": "已跳過",
+ "home.setup.stepCloud.text": "把每個快照的副本存到 Google Drive、OneDrive、Dropbox 或你自己的資料夾裡。",
+ "home.setup.stepCloud.title": "備份到雲端",
+ "home.setup.stepDevices.text": "一臺電腦、一臺 Steam Deck 或裝有 OpenSave 的掌機——在同一個 Wi-Fi 下,或透過網際網路從任意位置連線。",
+ "home.setup.stepDevices.title": "新增你的其他裝置",
+ "home.setup.stepSaves.text": "OpenSave 會掃描 Steam、模擬器和常見的存檔資料夾,由你選擇要跟踪的內容。",
+ "home.setup.stepSaves.title": "找到你的遊戲存檔",
+ "home.setup.title": "開始設定 OpenSave",
+ "home.summary.cloudBackup": "雲備份",
+ "home.summary.cloudBackupTitle": "開啟雲備份",
+ "home.summary.cloudNotSetUp": "未設定",
+ "home.summary.cloudOff": "已關閉",
+ "home.summary.cloudOn": "已開啟",
+ "home.summary.devicesOnline": "共 {paired} 臺,{online} 臺線上",
+ "home.summary.devicesTitle": "開啟裝置",
+ "home.summary.lastSnapshot": "最近快照",
+ "home.summary.lastSnapshotTitle": "{name} · {when}",
+ "home.summary.lowSpace": "存放快照的磁碟只剩 {size} 可用空間,新的快照可能存不下。",
+ "home.summary.nonePaired": "未配對",
+ "home.summary.noneYet": "暫無",
+ "home.summary.offline": "離線",
+ "home.summary.online": "線上",
+ "home.summary.pausedDetail": "同步已暫停({length})。快照仍會建立,恢復後會自動補齊。",
+ "home.summary.resumeNow": "立即恢復",
+ "home.summary.seeSpace": "檢視佔用情況",
+ "home.summary.sharedSavingTitle": "磁碟佔用比快照自身大小之和少 {size},因為未變動的檔案只存一份。開啟設定 → 儲存",
+ "home.summary.spaceUsed": "已用空間",
+ "home.summary.spaceUsedTitle": "開啟設定 → 儲存",
+ "home.tile.inFavourites": "已在收藏中",
+ "home.tile.onBranch": "在 {branch} 上",
+ "home.tile.played": "已遊玩 {length}",
+ "home.tile.snapshots": "{n} 個快照",
+ "home.viewOptions.autoFit": "自動排滿",
+ "home.viewOptions.coverStyle": "封面樣式",
+ "home.viewOptions.covers": "封面",
+ "home.viewOptions.fixedNote": "指定每行數量後,磁貼會平分這一行的寬度。",
+ "home.viewOptions.overview": "在主頁顯示最近動態",
+ "home.viewOptions.tileSize": "磁貼大小",
+ "home.viewOptions.tilesPerRow": "每行磁貼數",
+ "home.chooseFolderDialog": "選擇要跟踪的存檔資料夾",
+ "home.cloudBackup": "雲備份",
+ "home.devices": "裝置",
+ "home.scanSaves": "掃描存檔",
+ "home.scanSavesTitle": "查詢本機上的遊戲存檔",
+ "home.scanning": "掃描中…",
+ "home.syncAll": "全部同步",
+ "home.syncAllToast": "已觸發全部遊戲的同步",
+ "home.syncPaused": "同步已暫停",
+ "home.title": "主頁",
+ "home.trackFolder": "跟踪資料夾",
+ "home.trackFolderTitle": "選擇要跟踪的存檔資料夾或檔案",
+ "home.welcome.hint": "然後開啟 {devices} 配對另一臺電腦或 Steam Deck,或開啟 {cloud} 將快照鏡像到雲端。",
+ "home.welcome.intro": "讓你的遊戲存檔在所有裝置間保持同步——無需帳號,不被雲端綁架。先找到你的存檔:",
+ "home.welcome.scanForSaves": "掃描存檔",
+ "home.welcome.title": "歡迎使用 OpenSave",
+ "home.welcome.trackManually": "手動跟踪資料夾",
+ "home.trackGame": "跟踪遊戲",
+ "home.trackGameHint": "按名稱查詢遊戲,或選擇其程式或存檔資料夾",
+ "home.addGame.recognisedAs": "已識別為“{name}”——封面來自遊戲資料庫。",
+ "home.findGame.title": "從遊戲資料庫跟踪遊戲",
+ "home.findGame.findByName": "按名稱查詢",
+ "home.findGame.searchPlaceholder": "例如:Crimson Desert",
+ "home.findGame.searching": "搜尋中…",
+ "home.findGame.noSavesHere": "此電腦上沒有找到它的存檔",
+ "home.findGame.trackedAs": "已作為“{name}”跟踪",
+ "home.findGame.nothingFound": "遊戲資料庫中沒有這個名稱的遊戲。",
+ "home.findGame.chooseProgram": "選擇遊戲程式…",
+ "home.findGame.chooseSaveFolder": "選擇其存檔資料夾…",
+ "home.findGame.programDialog": "選擇遊戲程式",
+ "home.findGame.saveFolderDialog": "選擇遊戲的存檔資料夾",
+ "home.findGame.saveFolderOf": "“{name}”的存檔資料夾",
+ "home.findGame.theGame": "這款遊戲",
+ "home.findGame.notKnown": "不在遊戲資料庫中——請為它命名;如果尚未設定存檔資料夾,請選擇一個。",
+ "home.findGame.saveFolder": "存檔資料夾",
+ "home.findGame.noSavesYet": "此電腦上還沒有找到它的存檔。請選擇它的存檔資料夾,或先玩一次再跟踪。",
+ "home.findGame.otherSaveFolder": "選擇其他存檔資料夾…",
+ "home.findGame.program": "遊戲程式",
+ "home.findGame.pickAnother": "← 選擇其他遊戲",
+ "home.findGame.already": "此處已作為“{tracked}”跟踪。再次跟踪會將相同的檔案同步兩次;請把該條目關聯到 {name},使其獲得遊戲資料庫中的名稱、App ID 和封面。",
+ "home.findGame.link": "將“{tracked}”關聯到 {name}",
+ "home.findGame.linked": "“{tracked}”現在是 {name}",
+ "restoreProgress.rebuilding": "正在重建快照",
+ "restoreProgress.checking": "正在檢查快照",
+ "restoreProgress.writing": "正在寫入檔案",
+ "restoreProgress.restoring": "正在恢復",
+ "restoreProgress.line": "{phase} — {percent}%({done}/{total})",
+ "restoreProgress.lineWithTimeLeft": "{phase} — {percent}%({done}/{total}),還剩{left}",
+ "restoreProgress.underAMinute": "不到一分鐘",
+ "restoreProgress.aboutMinutes": "約 {n} 分鐘",
+ "snapshots.incomplete": "“{name}”:自動快照一直缺少遊戲正在佔用的 {files}。在這些檔案可以讀取之前(通常是遊戲關閉後),它的更改不會被備份或同步。",
+ "snapshots.incomplete.more": " 等另外 {n} 個檔案",
+ "game.config.deviceSettings": "裝置設定",
+ "game.config.syncDeviceSettings": "也在裝置之間同步這些設定",
+ "game.config.deviceSettingsHint": "遊戲資料庫將這些檔案列為該遊戲的設定而非存檔,因此每臺裝置保留各自的設定(畫質、解析度)。它們仍包含在每個快照中。",
+ "common.close": "關閉",
+ "common.dismiss": "忽略",
+ "common.loading": "載入中",
+ "titleBar.minimise": "最小化",
+ "titleBar.maximise": "最大化",
+ "statusBar.reconnecting": "正在重連…",
+ "statusBar.reconnectingTitle": "應用與後臺服務失去聯絡,正在重連",
+ "statusBar.noSyncs": "當前沒有進行中的同步",
+ "statusBar.syncing": "正在同步 {n} 個遊戲…",
+ "statusBar.devicesOnline": "{n} 臺裝置線上",
+ "statusBar.resume": "恢復",
+ "statusBar.resumeTitle": "恢復同步並補齊",
+ "statusBar.pause": "暫停同步",
+ "statusBar.transfersHint": "裝置之間正在傳輸的內容",
+ "statusBar.paletteHint": "跳轉到遊戲、頁面或操作",
+ "statusBar.aboutTitle": "關於 OpenSave",
+ "transfers.title": "傳輸",
+ "transfers.empty": "自 OpenSave 啟動以來,裝置之間還沒有傳輸過任何內容。",
+ "transfers.now": "進行中",
+ "transfers.recently": "最近",
+ "transfers.readError": "無法讀取傳輸記錄:{error}"
+}
diff --git a/cmd/opensave-app/frontend/src/views/Home.svelte b/cmd/opensave-app/frontend/src/views/Home.svelte
index 0287353d..fcb53894 100644
--- a/cmd/opensave-app/frontend/src/views/Home.svelte
+++ b/cmd/opensave-app/frontend/src/views/Home.svelte
@@ -112,7 +112,7 @@
-
+