diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 986ec37..0718c25 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -14,7 +14,6 @@ "settingsTitle": { "message": "Settings" }, "language": { "message": "Language" }, - "langFollowBrowser": { "message": "Follow browser" }, "openSourced": { "message": "It's open sourced!" }, "viewOnGithub": { "message": "View on GitHub" }, "tokenLabel": { "message": "GitHub Personal Access Token" }, diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index 57c124f..1e29a18 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -14,7 +14,6 @@ "settingsTitle": { "message": "设置" }, "language": { "message": "语言" }, - "langFollowBrowser": { "message": "跟随浏览器" }, "openSourced": { "message": "本项目已开源!" }, "viewOnGithub": { "message": "在 GitHub 上查看" }, "tokenLabel": { "message": "GitHub 个人访问令牌" }, @@ -34,7 +33,7 @@ "groupHome": { "message": "首页" }, "groupPRsIssues": { "message": "PR 和 Issue" }, "groupPRDetails": { "message": "PR 详情" }, - "groupCommits": { "message": "提交" }, + "groupCommits": { "message": "Commits" }, "groupRepository": { "message": "仓库" }, "featBetterTopReposName": { "message": "增强的热门仓库" }, @@ -67,17 +66,17 @@ }, "featPrCollapseExpandName": { "message": "折叠/展开所有文件" }, "featPrCollapseExpandDesc": { - "message": "在 PR、提交和对比页面添加一个折叠或展开所有文件差异的按钮。" + "message": "在 PR、Commit 和对比页面添加一个折叠或展开所有文件差异的按钮。" }, - "featCommitTagsName": { "message": "提交标签" }, + "featCommitTagsName": { "message": "Commit Tags" }, "featCommitTagsDesc": { - "message": "在提交列表页显示 git 标签,便于识别。" + "message": "在 Commits 列表页显示 git tags,便于识别。" }, - "featCommitDiffStatsName": { "message": "提交差异统计" }, + "featCommitDiffStatsName": { "message": "Commit 差异统计" }, "featCommitDiffStatsDesc": { - "message": "在提交列表页显示新增、删除行数和文件数(如 +223 −114 · 5 files)。需要令牌。" + "message": "在 Commits 列表页显示新增、删除行数和文件数(如 +223 −114 · 5 files)。需要令牌。" }, - "featReleaseTabName": { "message": "发布标签页" }, + "featReleaseTabName": { "message": "Releases 标签页" }, "featReleaseTabDesc": { "message": "在仓库导航栏添加一个 Releases 标签页,方便快速访问。" }, @@ -100,7 +99,7 @@ "saveFailed": { "message": "保存失败" }, "saved": { "message": "已保存!" }, - "releases": { "message": "发布" }, + "releases": { "message": "Releases" }, "approveNow": { "message": "立即批准" }, "approveDialogTitle": { "message": "批准此 Pull Request?" }, "approveCommentPlaceholder": { "message": "添加评论(可选)" }, @@ -113,7 +112,7 @@ }, "commitTagTitle": { - "message": "标签:$name$", + "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, "copied": { "message": "已复制!" }, @@ -122,13 +121,13 @@ "pinRepository": { "message": "置顶仓库" }, "unpinRepository": { "message": "取消置顶" }, - "watchers": { "message": "关注者" }, - "forks": { "message": "复刻" }, - "stargazers": { "message": "星标用户" }, + "watchers": { "message": "Watchers" }, + "forks": { "message": "Forks" }, + "stargazers": { "message": "Stargazers" }, "viewAll": { "message": "查看全部" }, - "noWatchers": { "message": "暂无关注者" }, - "noStargazers": { "message": "暂无星标用户" }, - "noForks": { "message": "暂无复刻" }, + "noWatchers": { "message": "暂无 Watchers" }, + "noStargazers": { "message": "暂无 Stargazers" }, + "noForks": { "message": "暂无 Forks" }, "failedToLoad": { "message": "加载失败" }, "collapseTree": { "message": "折叠目录树" }, diff --git a/src/_locales/zh_TW/messages.json b/src/_locales/zh_TW/messages.json index 12e2575..ab573c6 100644 --- a/src/_locales/zh_TW/messages.json +++ b/src/_locales/zh_TW/messages.json @@ -14,7 +14,6 @@ "settingsTitle": { "message": "設定" }, "language": { "message": "語言" }, - "langFollowBrowser": { "message": "跟隨瀏覽器" }, "openSourced": { "message": "本專案已開源!" }, "viewOnGithub": { "message": "在 GitHub 上查看" }, "tokenLabel": { "message": "GitHub 個人存取權杖" }, @@ -34,7 +33,7 @@ "groupHome": { "message": "首頁" }, "groupPRsIssues": { "message": "PR 和 Issue" }, "groupPRDetails": { "message": "PR 詳情" }, - "groupCommits": { "message": "提交" }, + "groupCommits": { "message": "Commits" }, "groupRepository": { "message": "儲存庫" }, "featBetterTopReposName": { "message": "增強的熱門儲存庫" }, @@ -67,17 +66,17 @@ }, "featPrCollapseExpandName": { "message": "摺疊/展開所有檔案" }, "featPrCollapseExpandDesc": { - "message": "在 PR、提交和比較頁面新增一個摺疊或展開所有檔案差異的按鈕。" + "message": "在 PR、Commit 和比較頁面新增一個摺疊或展開所有檔案差異的按鈕。" }, - "featCommitTagsName": { "message": "提交標籤" }, + "featCommitTagsName": { "message": "Commit Tags" }, "featCommitTagsDesc": { - "message": "在提交清單頁顯示 git 標籤,便於識別。" + "message": "在 Commits 清單頁顯示 git tags,便於識別。" }, - "featCommitDiffStatsName": { "message": "提交差異統計" }, + "featCommitDiffStatsName": { "message": "Commit 差異統計" }, "featCommitDiffStatsDesc": { - "message": "在提交清單頁顯示新增、刪除行數和檔案數(如 +223 −114 · 5 files)。需要權杖。" + "message": "在 Commits 清單頁顯示新增、刪除行數和檔案數(如 +223 −114 · 5 files)。需要權杖。" }, - "featReleaseTabName": { "message": "發佈分頁" }, + "featReleaseTabName": { "message": "Releases 分頁" }, "featReleaseTabDesc": { "message": "在儲存庫導覽列新增一個 Releases 分頁,方便快速存取。" }, @@ -100,7 +99,7 @@ "saveFailed": { "message": "儲存失敗" }, "saved": { "message": "已儲存!" }, - "releases": { "message": "發佈" }, + "releases": { "message": "Releases" }, "approveNow": { "message": "立即核准" }, "approveDialogTitle": { "message": "核准此 Pull Request?" }, "approveCommentPlaceholder": { "message": "新增留言(選填)" }, @@ -113,7 +112,7 @@ }, "commitTagTitle": { - "message": "標籤:$name$", + "message": "Tag: $name$", "placeholders": { "name": { "content": "$1", "example": "v1.0.0" } } }, "copied": { "message": "已複製!" }, @@ -122,13 +121,13 @@ "pinRepository": { "message": "置頂儲存庫" }, "unpinRepository": { "message": "取消置頂" }, - "watchers": { "message": "關注者" }, - "forks": { "message": "復刻" }, - "stargazers": { "message": "星標使用者" }, + "watchers": { "message": "Watchers" }, + "forks": { "message": "Forks" }, + "stargazers": { "message": "Stargazers" }, "viewAll": { "message": "查看全部" }, - "noWatchers": { "message": "尚無關注者" }, - "noStargazers": { "message": "尚無星標使用者" }, - "noForks": { "message": "尚無復刻" }, + "noWatchers": { "message": "尚無 Watchers" }, + "noStargazers": { "message": "尚無 Stargazers" }, + "noForks": { "message": "尚無 Forks" }, "failedToLoad": { "message": "載入失敗" }, "collapseTree": { "message": "摺疊目錄樹" }, diff --git a/src/content.ts b/src/content.ts index 133f7c4..3a44014 100644 --- a/src/content.ts +++ b/src/content.ts @@ -128,7 +128,7 @@ if (isExtensionValid()) { if (area !== "local") return; // Picked-up on the next injection/navigation; a refresh re-renders all text. if (LOCALE_KEY in changes) { - setLocale((changes[LOCALE_KEY].newValue as LocalePref) ?? "auto"); + setLocale((changes[LOCALE_KEY].newValue as LocalePref) ?? "en"); } for (const key of FEATURE_KEYS) { if (!(key in changes)) continue; diff --git a/src/lib/i18n.test.ts b/src/lib/i18n.test.ts index 1749791..ac08dcc 100644 --- a/src/lib/i18n.test.ts +++ b/src/lib/i18n.test.ts @@ -15,7 +15,6 @@ describe("i18n", () => { setLocale("zh_CN"); expect(t("approveNow")).toBe("立即批准"); expect(t("settingsTitle")).toBe("设置"); - expect(t("langFollowBrowser")).toBe("跟随浏览器"); }); it("switches to Traditional Chinese via setLocale", () => { @@ -32,7 +31,15 @@ describe("i18n", () => { "1,234 additions, 56 deletions across 3 files", ); setLocale("zh_CN"); - expect(t("commitTagTitle", "v1.0.0")).toBe("标签:v1.0.0"); + expect(t("commitTagTitle", "v1.0.0")).toBe("Tag: v1.0.0"); + }); + + it("falls back to English for a legacy/unknown preference", () => { + // "auto" was a valid preference before we dropped browser auto-detection; + // an existing user may still have it stored. It must resolve to English. + setLocale("auto" as unknown as Parameters[0]); + expect(t("settingsTitle")).toBe("Settings"); + expect(t("approveNow")).toBe("approve now"); }); it("returns empty string for an unknown key", () => { diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 928389c..9c829f1 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -1,8 +1,9 @@ // Lightweight i18n that, unlike chrome.i18n (which is locked to the browser UI // locale), supports an in-extension manual language override stored in -// chrome.storage.local. Catalogs are bundled so t() stays synchronous; the -// active locale is resolved once from the stored preference (default: follow -// the browser locale) and can be changed at runtime via setLocale(). +// chrome.storage.local. Catalogs are bundled so t() stays synchronous. The +// active locale defaults to English for everyone; we deliberately do NOT +// auto-detect the browser locale. Users opt into another language explicitly +// on the options page, and the choice can be applied at runtime via setLocale(). import enMessages from "../_locales/en/messages.json"; import zhCNMessages from "../_locales/zh_CN/messages.json"; import zhTWMessages from "../_locales/zh_TW/messages.json"; @@ -19,52 +20,43 @@ const CATALOGS: Record = { zh_TW: zhTWMessages as unknown as Catalog, }; -/** Locale preference: "auto" follows the browser, or a concrete catalog id. */ -export type LocalePref = "auto" | "en" | "zh_CN" | "zh_TW"; +/** A concrete catalog id. English is the default; other locales are opt-in. */ +export type LocalePref = "en" | "zh_CN" | "zh_TW"; /** Order shown in the language picker. */ -export const LOCALE_OPTIONS: LocalePref[] = ["auto", "en", "zh_CN", "zh_TW"]; +export const LOCALE_OPTIONS: LocalePref[] = ["en", "zh_CN", "zh_TW"]; /** chrome.storage.local key holding the LocalePref. */ export const LOCALE_KEY = "locale"; +/** Locale used when the user hasn't explicitly picked one. */ +export const DEFAULT_LOCALE: LocalePref = "en"; -/** Map the browser UI language to one of our catalogs (en fallback). */ -function resolveAuto(): string { - let ui = "en"; - try { - const g = globalThis as unknown as { chrome?: { i18n?: { getUILanguage?: () => string } } }; - ui = g.chrome?.i18n?.getUILanguage?.() || "en"; - } catch { - ui = "en"; - } - const lc = ui.toLowerCase(); - if (lc.startsWith("zh")) { - // Traditional for Taiwan / Hong Kong / Macau / explicit Hant script. - return /hant|-tw|-hk|-mo/.test(lc) ? "zh_TW" : "zh_CN"; - } - return "en"; +/** Normalize an arbitrary stored value to a known locale (en fallback). Also + * absorbs the legacy "auto" preference, which no longer exists. */ +function normalize(pref: unknown): LocalePref { + return typeof pref === "string" && pref in CATALOGS ? (pref as LocalePref) : DEFAULT_LOCALE; } -let currentLocale = resolveAuto(); +let currentLocale: LocalePref = DEFAULT_LOCALE; /** Apply a preference immediately (synchronous). */ export function setLocale(pref: LocalePref): void { - currentLocale = pref === "auto" ? resolveAuto() : pref; + currentLocale = normalize(pref); } /** - * Load the stored preference and apply it. Resolves to the stored pref - * ("auto" when unset or storage is unavailable) so callers can reflect it in a - * picker. Safe to call repeatedly. + * Load the stored preference and apply it. Resolves to the effective locale + * (DEFAULT_LOCALE when unset, unrecognized, or storage is unavailable) so + * callers can reflect it in a picker. Safe to call repeatedly. */ export function initLocale(): Promise { return new Promise((resolve) => { try { chrome.storage.local.get([LOCALE_KEY], (result) => { - const pref = (result?.[LOCALE_KEY] as LocalePref) || "auto"; + const pref = normalize(result?.[LOCALE_KEY]); setLocale(pref); resolve(pref); }); } catch { - resolve("auto"); + resolve(DEFAULT_LOCALE); } }); } diff --git a/static/options.html b/static/options.html index a2371e0..6759d2b 100644 --- a/static/options.html +++ b/static/options.html @@ -460,7 +460,6 @@

Settings

Language