feat(i18n): declare locale aliases in config/i18n.json (uk, fil/tl, zh-hk/mo)

This commit is contained in:
Markus Hartung
2026-09-02 02:08:03 -03:00
parent cabbbe410a
commit b2edd7c0bb
4 changed files with 53 additions and 3 deletions

View File

@@ -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."
}
}
}

View File

@@ -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"]
}
]
}

View File

@@ -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<Record<string, readonly string[]>> = 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";

View File

@@ -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<string>();
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"]);
});