diff --git a/bin/cli/i18n.mjs b/bin/cli/i18n.mjs index 70cae65232..9ec435dcc9 100644 --- a/bin/cli/i18n.mjs +++ b/bin/cli/i18n.mjs @@ -5,6 +5,25 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const LOCALES_DIR = join(__dirname, "locales"); const FALLBACK_LOCALE = "en"; +const I18N_CONFIG_PATH = join(__dirname, "..", "..", "config", "i18n.json"); +let aliasMap = null; // lower-case tag → canonical locale code + +function loadAliasMap() { + if (aliasMap) return aliasMap; + aliasMap = new Map(); + try { + const { locales } = JSON.parse(readFileSync(I18N_CONFIG_PATH, "utf8")); + for (const entry of locales) { + aliasMap.set(entry.code.toLowerCase(), entry.code); + for (const alias of entry.aliases || []) { + aliasMap.set(String(alias).toLowerCase(), entry.code); + } + } + } catch { + // config absent (trimmed package): keep file-based detection only + } + return aliasMap; +} const cache = new Map(); let activeLocale = null; @@ -24,8 +43,16 @@ function normalize(raw) { const stripped = String(raw).split(".")[0].replaceAll("_", "-"); if (!stripped || !/^[a-zA-Z0-9-]+$/.test(stripped)) return FALLBACK_LOCALE; if (hasCatalog(stripped)) return stripped; - const base = stripped.split("-")[0]; + const lower = stripped.toLowerCase(); + const base = lower.split("-")[0]; + const aliases = loadAliasMap(); + const viaAlias = aliases.get(lower) ?? aliases.get(base); + if (viaAlias && hasCatalog(viaAlias)) return viaAlias; if (hasCatalog(base)) return base; + const regional = [...new Set(aliases.values())].find( + (code) => code.includes("-") && code.toLowerCase().split("-")[0] === base + ); + if (regional && hasCatalog(regional)) return regional; return FALLBACK_LOCALE; } @@ -103,4 +130,5 @@ export function resetForTests() { cache.clear(); activeLocale = null; fallbackCatalog = null; + aliasMap = null; } diff --git a/tests/unit/cli-i18n-catalog.test.ts b/tests/unit/cli-i18n-catalog.test.ts index 7b996dfc55..af83f12fcf 100644 --- a/tests/unit/cli-i18n-catalog.test.ts +++ b/tests/unit/cli-i18n-catalog.test.ts @@ -74,7 +74,10 @@ test("pt-BR.json tem todas as seções top-level de en.json", () => { assert.deepEqual(missing, [], `Seções top-level faltando em pt-BR.json: ${missing.join(", ")}`); }); -for (const [name, cat] of [["zh-CN", zhCN], ["zh-TW", zhTW]] as const) { +for (const [name, cat] of [ + ["zh-CN", zhCN], + ["zh-TW", zhTW], +] as const) { test(name + ".json tem paridade total de chaves com en.json", () => { const catKeys = flattenKeys(cat as Record); const missing = [...enKeys].filter((k) => !catKeys.has(k)); @@ -106,6 +109,23 @@ test("i18n.mjs usa fallback en quando locale não existe", async () => { resetForTests(); }); +test("i18n.mjs resolve alias do config: OMNIROUTE_LANG=uk → uk-UA, fil_PH.UTF-8 → phi", async () => { + const { resetForTests, detectLocale } = await import("../../bin/cli/i18n.mjs"); + const orig = process.env.OMNIROUTE_LANG; + process.env.OMNIROUTE_LANG = "uk"; + resetForTests(); + assert.equal(detectLocale(), "uk-UA"); + process.env.OMNIROUTE_LANG = "fil_PH.UTF-8"; + resetForTests(); + assert.equal(detectLocale(), "phi"); + process.env.OMNIROUTE_LANG = "uk_UA.UTF-8"; + resetForTests(); + assert.equal(detectLocale(), "uk-UA"); + if (orig === undefined) delete process.env.OMNIROUTE_LANG; + else process.env.OMNIROUTE_LANG = orig; + resetForTests(); +}); + test("t() interpola variáveis {var}", async () => { const { resetForTests, t, setLocale } = await import("../../bin/cli/i18n.mjs"); resetForTests();