From b2edd7c0bb43af770610e28812947df5f3718230 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Wed, 2 Sep 2026 02:08:03 -0300 Subject: [PATCH] feat(i18n): declare locale aliases in config/i18n.json (uk, fil/tl, zh-hk/mo) --- config/i18n-schema.json | 10 ++++++++++ config/i18n.json | 9 ++++++--- src/i18n/config.ts | 16 ++++++++++++++++ tests/unit/i18n-config.test.ts | 21 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/config/i18n-schema.json b/config/i18n-schema.json index fb5ee16664..7c4ac5ea00 100644 --- a/config/i18n-schema.json +++ b/config/i18n-schema.json @@ -57,6 +57,16 @@ "flag": { "type": "string", "description": "Flag emoji shown next to the locale." + }, + "aliases": { + "type": "array", + "description": "Browser/OS language tags (lower-case BCP-47) that resolve to this locale, e.g. `uk` → `uk-UA`, `fil`/`tl` → `phi`.", + "items": { "type": "string", "pattern": "^[a-z]{2,3}(-[a-z0-9]{2,8})*$" }, + "uniqueItems": true + }, + "script": { + "type": "string", + "description": "ISO 15924 script code (e.g. `Mymr`, `Khmr`). Informational only — the visual-QA report uses it to prioritise tall scripts." } } } diff --git a/config/i18n.json b/config/i18n.json index 2b44ee7aa8..fc48aec286 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -211,7 +211,8 @@ "name": "Filipino", "native": "Filipino", "english": "Filipino", - "flag": "🇵🇭" + "flag": "🇵🇭", + "aliases": ["fil", "tl"] }, { "code": "pl", @@ -315,7 +316,8 @@ "name": "Українська", "native": "Українська", "english": "Ukrainian", - "flag": "🇺🇦" + "flag": "🇺🇦", + "aliases": ["uk"] }, { "code": "ur", @@ -347,7 +349,8 @@ "name": "中文 (繁體)", "native": "中文 (繁體)", "english": "Chinese (Traditional)", - "flag": "🇹🇼" + "flag": "🇹🇼", + "aliases": ["zh-hk", "zh-mo"] } ] } diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 70f35db9ac..aa6ab82d35 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -11,6 +11,8 @@ type RawLocaleEntry = { native?: string; english?: string; flag: string; + aliases?: readonly string[]; + script?: string; }; type RawI18nConfig = { @@ -51,4 +53,18 @@ export const LANGUAGES: readonly { export const RTL_LOCALES: readonly Locale[] = config.rtl as readonly Locale[]; +/** + * Browser / OS language tags that resolve to a configured locale. Read by + * `detectBrowserLocale` (client), `resolveRequestedLocale` (server cookie) and + * mirrored by the CLI (`bin/cli/i18n.mjs`). Keys are locale codes, values are + * lower-case BCP-47 tags. Only locales that declare `aliases` appear here. + */ +export const LOCALE_ALIASES: Readonly> = Object.freeze( + Object.fromEntries( + config.locales + .filter((entry) => Array.isArray(entry.aliases) && entry.aliases.length > 0) + .map((entry) => [entry.code, Object.freeze([...(entry.aliases as string[])])]) + ) +); + export const LOCALE_COOKIE = "NEXT_LOCALE"; diff --git a/tests/unit/i18n-config.test.ts b/tests/unit/i18n-config.test.ts index 80bdd9d9d4..5811e0f045 100644 --- a/tests/unit/i18n-config.test.ts +++ b/tests/unit/i18n-config.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_LOCALE, LANGUAGES, LOCALES, + LOCALE_ALIASES, LOCALE_COOKIE, RTL_LOCALES, } from "../../src/i18n/config.ts"; @@ -34,3 +35,23 @@ test("i18n language metadata preserves native and English names", () => { flag: englishConfig?.flag, }); }); + +test("locale aliases are lower-case, unique and never collide with a locale code", () => { + const codes = new Set(LOCALES.map((code) => code.toLowerCase())); + const seen = new Set(); + for (const [code, aliases] of Object.entries(LOCALE_ALIASES)) { + assert.ok(codes.has(code.toLowerCase()), `${code} is not a configured locale`); + for (const alias of aliases) { + assert.equal(alias, alias.toLowerCase(), `${alias} must be lower-case`); + assert.ok(!codes.has(alias), `${alias} collides with a locale code`); + assert.ok(!seen.has(alias), `${alias} is declared for two locales`); + seen.add(alias); + } + } +}); + +test("Ukrainian, Filipino and Hong-Kong/Macau browsers resolve through declared aliases", () => { + assert.deepEqual(LOCALE_ALIASES["uk-UA"], ["uk"]); + assert.deepEqual(LOCALE_ALIASES["phi"], ["fil", "tl"]); + assert.deepEqual(LOCALE_ALIASES["zh-TW"], ["zh-hk", "zh-mo"]); +});