mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Per review feedback on #174: the hardcoded if/else in normalizeLanguageCode required touching the function for every new language, so a bare `ko` would miss the `ko-KR` resource #161 is about to add and Korean users would land on English despite the fix. Replace it with createLanguageNormalizer(resourceKeys), a factory that derives its mapping from the resource keys i18next is configured with: 1. exact resource key -> unchanged 2. one resource for the language -> that key (ko -> ko-KR) 3. several (zh-CN / zh-TW) -> script subtag, then region 4. nothing shipped -> unchanged, fallbackLng applies getLanguageDetectionOptions now takes the resource keys and i18n.ts passes Object.keys(resources). Chinese disambiguation stays as the only special case - one language, two scripts; every other mapping derives from the index automatically. Test expectations for the zh-TW family change from "zh-TW" to "zh-CN": only zh-CN ships today and the factory maps to shipped keys only, so Traditional resolves directly to Simplified instead of leaking into the fallback chain. The rendered UI is identical either way, and the new extensibility cases lock in the automatic disambiguation once a zh-TW bundle registers - plus the auto-resolution of a newly registered language like ko-KR. 28 -> 31 tests; extension suite 832/832; tsc, biome and ext:build clean.
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
chromeUiLanguageDetector,
|
|
createLanguageNormalizer,
|
|
getLanguageDetectionOptions,
|
|
} from "../src/chrome-storage-sync";
|
|
|
|
/** What `packages/i18n` ships today — kept in sync with `resources` in i18n.ts. */
|
|
const SHIPPED_RESOURCE_KEYS = ["zh-CN", "en-US"];
|
|
|
|
/**
|
|
* Locks the locale-resolution table from issue #168: the UI must follow the
|
|
* browser language instead of falling through to Chinese for every locale
|
|
* other than `en-US`.
|
|
*/
|
|
describe("createLanguageNormalizer", () => {
|
|
const normalize = createLanguageNormalizer(SHIPPED_RESOURCE_KEYS);
|
|
|
|
const cases: Array<[detected: string, resource: string]> = [
|
|
// English — every variant maps to the single en-US resource.
|
|
["en", "en-US"],
|
|
["en-US", "en-US"],
|
|
["en-GB", "en-US"],
|
|
["en-CN", "en-US"],
|
|
["en-AU", "en-US"],
|
|
|
|
// Chinese — only `zh-CN` ships today, so every variant resolves to it
|
|
// directly rather than leaking into the fallback chain. Traditional picks
|
|
// up `zh-TW` automatically once that bundle is registered (see below).
|
|
["zh", "zh-CN"],
|
|
["zh-CN", "zh-CN"],
|
|
["zh-SG", "zh-CN"],
|
|
["zh-Hans", "zh-CN"],
|
|
["zh-Hans-CN", "zh-CN"],
|
|
["zh-Hans-SG", "zh-CN"],
|
|
["zh-TW", "zh-CN"],
|
|
["zh-HK", "zh-CN"],
|
|
["zh-MO", "zh-CN"],
|
|
["zh-Hant", "zh-CN"],
|
|
["zh-Hant-TW", "zh-CN"],
|
|
["zh-Hant-HK", "zh-CN"],
|
|
|
|
// No translation shipped — returned unchanged so i18next applies the
|
|
// `default` fallback (en-US) rather than Chinese.
|
|
["ja", "ja"],
|
|
["ja-JP", "ja-JP"],
|
|
["fr-FR", "fr-FR"],
|
|
["de-DE", "de-DE"],
|
|
];
|
|
|
|
it.each(cases)("maps %s to %s", (detected, resource) => {
|
|
expect(normalize(detected)).toBe(resource);
|
|
});
|
|
|
|
it("is case-insensitive", () => {
|
|
expect(normalize("EN-us")).toBe("en-US");
|
|
expect(normalize("zh-hant-tw")).toBe("zh-CN");
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The point of the factory: supporting a new language means registering its
|
|
* bundle in `resources` — never touching the normalizer.
|
|
*/
|
|
describe("createLanguageNormalizer extensibility", () => {
|
|
it("auto-resolves a newly registered language", () => {
|
|
const normalize = createLanguageNormalizer(["en-US", "zh-CN", "ko-KR"]);
|
|
expect(normalize("ko")).toBe("ko-KR");
|
|
expect(normalize("ko-KR")).toBe("ko-KR");
|
|
});
|
|
|
|
it("disambiguates zh once a Traditional bundle ships", () => {
|
|
const normalize = createLanguageNormalizer(["en-US", "zh-CN", "zh-TW"]);
|
|
expect(normalize("zh-Hant")).toBe("zh-TW");
|
|
expect(normalize("zh-Hant-TW")).toBe("zh-TW");
|
|
expect(normalize("zh-TW")).toBe("zh-TW");
|
|
expect(normalize("zh-HK")).toBe("zh-TW");
|
|
expect(normalize("zh-MO")).toBe("zh-TW");
|
|
expect(normalize("zh-Hans")).toBe("zh-CN");
|
|
expect(normalize("zh-CN")).toBe("zh-CN");
|
|
expect(normalize("zh")).toBe("zh-CN");
|
|
});
|
|
|
|
it("leaves unregistered languages to fallbackLng", () => {
|
|
const normalize = createLanguageNormalizer(["en-US", "zh-CN"]);
|
|
expect(normalize("pt-BR")).toBe("pt-BR");
|
|
expect(normalize("ru")).toBe("ru");
|
|
});
|
|
});
|
|
|
|
describe("chromeUiLanguageDetector", () => {
|
|
it("prefers chrome.i18n.getUILanguage() when available", () => {
|
|
vi.stubGlobal("chrome", { i18n: { getUILanguage: () => "en-GB" } });
|
|
expect(chromeUiLanguageDetector.lookup()).toBe("en-GB");
|
|
});
|
|
|
|
it("returns undefined outside a Chrome extension context", () => {
|
|
vi.stubGlobal("chrome", undefined);
|
|
expect(chromeUiLanguageDetector.lookup()).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined when the i18n API is unavailable", () => {
|
|
vi.stubGlobal("chrome", {});
|
|
expect(chromeUiLanguageDetector.lookup()).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("getLanguageDetectionOptions", () => {
|
|
it("orders the Chrome UI language ahead of navigator", () => {
|
|
expect(getLanguageDetectionOptions(SHIPPED_RESOURCE_KEYS).order).toEqual([
|
|
"chromeUILanguage",
|
|
"navigator",
|
|
]);
|
|
});
|
|
|
|
it("normalises whatever the detectors report", () => {
|
|
const { convertDetectedLanguage } = getLanguageDetectionOptions(SHIPPED_RESOURCE_KEYS);
|
|
expect(convertDetectedLanguage("en-GB")).toBe("en-US");
|
|
expect(convertDetectedLanguage("zh-TW")).toBe("zh-CN");
|
|
expect(convertDetectedLanguage("ja-JP")).toBe("ja-JP");
|
|
});
|
|
|
|
it("does not write to page-origin storage", () => {
|
|
expect(getLanguageDetectionOptions(SHIPPED_RESOURCE_KEYS).caches).toEqual([]);
|
|
});
|
|
});
|