mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(i18n): match browser locale by language subtag instead of exact code
The extension ships en-US and zh-CN resources, but only en-US was ever
selected: every other browser language fell through to fallbackLng
("zh-CN"), so users on en, en-GB, en-CN, en-AU, ja-JP, fr-FR saw Chinese.
Match on the BCP 47 language subtag and normalise it to a resource key:
en* -> en-US
zh-Hans* / zh-CN / zh-SG / zh -> zh-CN
zh-Hant* / zh-TW / zh-HK / zh-MO -> zh-TW
anything else -> unchanged (fallbackLng default: en-US)
Simplified vs Traditional is decided by the script subtag (Hans/Hant) when
present, then by the region subtag, since Chrome reports zh-CN / zh-TW
rather than script subtags. The zh-TW branch is written now so adding a
Traditional translation later needs no code change.
chrome.i18n.getUILanguage() is now preferred over navigator.language, which
is unreliable on the chrome-extension:// origin the popup runs on.
Adds a locale-resolution regression matrix (28 cases).
Closes #168
This commit is contained in:
@@ -24,6 +24,9 @@ declare const chrome:
|
||||
) => void;
|
||||
};
|
||||
};
|
||||
i18n?: {
|
||||
getUILanguage(): string;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -31,14 +34,86 @@ function getChromeLocal(): ChromeStorageLocal | undefined {
|
||||
return typeof chrome !== "undefined" ? chrome.storage?.local : undefined;
|
||||
}
|
||||
|
||||
/** Regions whose written Chinese defaults to Traditional. */
|
||||
const TRADITIONAL_CHINESE_REGIONS = new Set(["tw", "hk", "mo"]);
|
||||
|
||||
/**
|
||||
* Normalize a BCP 47 language tag to one of the shipped resource keys.
|
||||
*
|
||||
* | detected tag | resource | rule |
|
||||
* | ----------------------- | -------- | --------------------------- |
|
||||
* | `en`, `en-GB`, `en-CN` | `en-US` | every English variant |
|
||||
* | `zh-Hans`, `zh-Hans-CN` | `zh-CN` | script subtag wins |
|
||||
* | `zh-Hant`, `zh-Hant-TW` | `zh-TW` | script subtag wins |
|
||||
* | `zh-CN`, `zh-SG` | `zh-CN` | region subtag |
|
||||
* | `zh-TW`, `zh-HK`, `zh-MO`| `zh-TW` | region subtag |
|
||||
* | `zh` (bare) | `zh-CN` | default: Simplified |
|
||||
* | `ja`, `fr`, `de`, … | unchanged| no resource → `fallbackLng` |
|
||||
*
|
||||
* Simplified vs Traditional is decided by the script subtag (`Hans` / `Hant`)
|
||||
* when present, then by the region subtag — Chrome reports `zh-CN` / `zh-TW`
|
||||
* rather than script subtags in practice.
|
||||
*
|
||||
* The `zh-TW` branch is written now so adding a Traditional translation later
|
||||
* needs no code change; until that resource exists i18next falls back to `zh-CN`.
|
||||
*/
|
||||
export function normalizeLanguageCode(code: string): string {
|
||||
const parts = code.split(/[-_]/).map((part) => part.toLowerCase());
|
||||
const primary = parts[0];
|
||||
|
||||
// English — every variant resolves to the single `en-US` resource.
|
||||
if (primary === "en") {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
if (primary === "zh") {
|
||||
// 1. Script subtag is the most accurate signal (zh-Hans / zh-Hant).
|
||||
if (parts.includes("hans")) {
|
||||
return "zh-CN";
|
||||
}
|
||||
if (parts.includes("hant")) {
|
||||
return "zh-TW";
|
||||
}
|
||||
// 2. Fall back to the region subtag, which is what Chrome actually reports.
|
||||
// Skip parts[0] — the primary subtag `zh` is also two characters long.
|
||||
const region = parts.slice(1).find((part) => part.length === 2);
|
||||
if (region && TRADITIONAL_CHINESE_REGIONS.has(region)) {
|
||||
return "zh-TW";
|
||||
}
|
||||
// 3. Bare `zh` and any other region default to Simplified.
|
||||
return "zh-CN";
|
||||
}
|
||||
|
||||
// No translation shipped: leave untouched so i18next applies `fallbackLng`.
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* i18next detector that prefers `chrome.i18n.getUILanguage()`, Chrome's
|
||||
* authoritative source for the extension UI language. The popup runs on a
|
||||
* `chrome-extension://` origin where `navigator.language` can disagree with it.
|
||||
*/
|
||||
export const chromeUiLanguageDetector = {
|
||||
name: "chromeUILanguage",
|
||||
lookup(): string | undefined {
|
||||
if (typeof chrome !== "undefined" && chrome.i18n?.getUILanguage) {
|
||||
return chrome.i18n.getUILanguage();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
|
||||
/** Avoid page-origin localStorage; use extension storage + navigator only. */
|
||||
export function getLanguageDetectionOptions(): {
|
||||
order: string[];
|
||||
caches: string[];
|
||||
convertDetectedLanguage: (lng: string) => string;
|
||||
} {
|
||||
return {
|
||||
order: ["navigator"],
|
||||
order: ["chromeUILanguage", "navigator"],
|
||||
caches: [],
|
||||
// Normalise the detected tag before i18next resolves it against `resources`.
|
||||
convertDetectedLanguage: (lng: string) => normalizeLanguageCode(lng),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import { bindChromeStorageLanguageSync, getLanguageDetectionOptions } from "./chrome-storage-sync";
|
||||
import {
|
||||
bindChromeStorageLanguageSync,
|
||||
chromeUiLanguageDetector,
|
||||
getLanguageDetectionOptions,
|
||||
} from "./chrome-storage-sync";
|
||||
import enUSCommon from "./locales/en-US/common.json";
|
||||
import enUSExtension from "./locales/en-US/extension.json";
|
||||
import zhCNCommon from "./locales/zh-CN/common.json";
|
||||
@@ -19,12 +23,17 @@ const resources = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector(chromeUiLanguageDetector);
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "zh-CN",
|
||||
// English is the international default; Chinese users still get Chinese.
|
||||
// `zh` → zh-CN also covers zh-TW/zh-HK until a Traditional resource exists.
|
||||
fallbackLng: { en: ["en-US"], zh: ["zh-CN"], default: ["en-US"] },
|
||||
defaultNS: "common",
|
||||
ns: ["common", "extension"],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user