mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 11:52:26 +03:00
fix(i18n): detect uk/fil browsers via aliases and base-language → regional locale match
This commit is contained in:
@@ -6,15 +6,22 @@
|
||||
* 1. Exact match against `navigator.languages` entries (case-insensitive).
|
||||
* 2. `zh-HK` / `zh-MO` are treated as `zh-TW` (Traditional Chinese) since
|
||||
* OmniRoute does not ship a dedicated Hong-Kong/Macau locale.
|
||||
* 3. Language-prefix match — e.g. `en-US` matches a supported `en` locale.
|
||||
* 4. No match → `null` (caller should keep the existing default).
|
||||
* 3. Declared alias — `aliases` has the shape of `LOCALE_ALIASES` from
|
||||
* `@/i18n/config` (locale code → lower-case BCP-47 tags) and is matched on
|
||||
* the full tag or on its base language, e.g. `fil`, `fil-PH`, `tl` → `phi`
|
||||
* and `uk` → `uk-UA`. Aliases of locales not in `locales` are ignored.
|
||||
* 4. Language-prefix match — e.g. `en-US` matches a supported `en` locale.
|
||||
* 5. Bare base language → first supported regional locale of that language,
|
||||
* in `locales` (config) order — e.g. `uk` → `uk-UA`, `zh` → `zh-CN`.
|
||||
* 6. No match → `null` (caller should keep the existing default).
|
||||
*
|
||||
* Kept dependency-free (no DOM/`navigator` access) so it is trivially unit
|
||||
* testable and reusable from both client components and future server code.
|
||||
*/
|
||||
export function detectBrowserLocale(
|
||||
languages: readonly string[],
|
||||
locales: readonly string[]
|
||||
locales: readonly string[],
|
||||
aliases: Readonly<Record<string, readonly string[]>> = {}
|
||||
): string | null {
|
||||
if (!languages || languages.length === 0 || !locales || locales.length === 0) {
|
||||
return null;
|
||||
@@ -22,9 +29,18 @@ export function detectBrowserLocale(
|
||||
|
||||
const normalizedLocales = locales.map((locale) => locale.toLowerCase());
|
||||
|
||||
// alias tag (lower-case) → supported locale, only for locales actually offered.
|
||||
const aliasIndex = new Map<string, string>();
|
||||
for (const [code, tags] of Object.entries(aliases)) {
|
||||
const index = normalizedLocales.indexOf(code.toLowerCase());
|
||||
if (index === -1) continue;
|
||||
for (const tag of tags) aliasIndex.set(tag.toLowerCase(), locales[index]);
|
||||
}
|
||||
|
||||
for (const rawLanguage of languages) {
|
||||
if (!rawLanguage) continue;
|
||||
const language = rawLanguage.toLowerCase();
|
||||
const prefix = language.split("-")[0];
|
||||
|
||||
// 1. Exact match.
|
||||
const exactIndex = normalizedLocales.indexOf(language);
|
||||
@@ -32,7 +48,8 @@ export function detectBrowserLocale(
|
||||
return locales[exactIndex];
|
||||
}
|
||||
|
||||
// 2. zh-HK / zh-MO fold to zh-TW when zh-TW is supported.
|
||||
// 2. zh-HK / zh-MO fold to zh-TW when zh-TW is supported (kept for callers
|
||||
// that do not pass aliases).
|
||||
if (language === "zh-hk" || language === "zh-mo") {
|
||||
const zhTwIndex = normalizedLocales.indexOf("zh-tw");
|
||||
if (zhTwIndex !== -1) {
|
||||
@@ -40,12 +57,26 @@ export function detectBrowserLocale(
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Language-prefix match (e.g. "en-US" -> "en").
|
||||
const prefix = language.split("-")[0];
|
||||
// 3. Declared alias, on the full tag or on its base language (fil-PH → phi).
|
||||
const aliased = aliasIndex.get(language) ?? aliasIndex.get(prefix);
|
||||
if (aliased) {
|
||||
return aliased;
|
||||
}
|
||||
|
||||
// 4. Language-prefix match (e.g. "en-US" -> "en").
|
||||
const prefixIndex = normalizedLocales.indexOf(prefix);
|
||||
if (prefixIndex !== -1) {
|
||||
return locales[prefixIndex];
|
||||
}
|
||||
|
||||
// 5. Bare base language → first supported regional locale of that language
|
||||
// ("uk" -> "uk-UA", "zh" -> "zh-CN"). Config order decides the tie.
|
||||
const regionalIndex = normalizedLocales.findIndex(
|
||||
(locale) => locale.includes("-") && locale.split("-")[0] === prefix
|
||||
);
|
||||
if (regionalIndex !== -1) {
|
||||
return locales[regionalIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LOCALES, LOCALE_COOKIE } from "@/i18n/config";
|
||||
import { LOCALES, LOCALE_ALIASES, LOCALE_COOKIE } from "@/i18n/config";
|
||||
import type { Locale } from "@/i18n/config";
|
||||
import { detectBrowserLocale } from "@/i18n/detectBrowserLocale";
|
||||
import { persistLocale } from "@/shared/lib/persistLocale";
|
||||
@@ -23,7 +23,11 @@ export function LocaleAutoDetect() {
|
||||
useEffect(() => {
|
||||
if (typeof navigator === "undefined" || hasLocaleCookie()) return;
|
||||
|
||||
const detected = detectBrowserLocale(navigator.languages ?? [navigator.language], LOCALES);
|
||||
const detected = detectBrowserLocale(
|
||||
navigator.languages ?? [navigator.language],
|
||||
LOCALES,
|
||||
LOCALE_ALIASES
|
||||
);
|
||||
if (!detected) return;
|
||||
|
||||
persistLocale(detected as Locale);
|
||||
|
||||
@@ -40,4 +40,35 @@ describe("detectBrowserLocale", () => {
|
||||
it("is case-insensitive", () => {
|
||||
assert.equal(detectBrowserLocale(["PT-br"], SUPPORTED_LOCALES), "pt-BR");
|
||||
});
|
||||
|
||||
// Regional-code locales (`uk-UA`, `phi`) plus the alias map shape exported by
|
||||
// `@/i18n/config` (`LOCALE_ALIASES`) — browsers send `uk`, `fil`/`tl`.
|
||||
const REGIONAL_LOCALES = ["en", "uk-UA", "phi", "pt", "pt-BR", "zh-CN", "zh-TW"] as const;
|
||||
const ALIASES = { "uk-UA": ["uk"], phi: ["fil", "tl"] } as const;
|
||||
|
||||
it("resolves a declared alias (fil → phi)", () => {
|
||||
assert.equal(detectBrowserLocale(["fil"], REGIONAL_LOCALES, ALIASES), "phi");
|
||||
});
|
||||
|
||||
it("resolves an alias carried by a regional tag (fil-PH → phi, TL → phi)", () => {
|
||||
assert.equal(detectBrowserLocale(["fil-PH"], REGIONAL_LOCALES, ALIASES), "phi");
|
||||
assert.equal(detectBrowserLocale(["TL"], REGIONAL_LOCALES, ALIASES), "phi");
|
||||
});
|
||||
|
||||
it("matches a bare base language to a regional locale of that language (uk → uk-UA) without aliases", () => {
|
||||
assert.equal(detectBrowserLocale(["uk"], REGIONAL_LOCALES), "uk-UA");
|
||||
});
|
||||
|
||||
it("picks the first regional locale in config order for a bare base language (zh → zh-CN)", () => {
|
||||
assert.equal(detectBrowserLocale(["zh"], REGIONAL_LOCALES), "zh-CN");
|
||||
});
|
||||
|
||||
it("keeps the exact/prefix precedence: pt-PT → pt, pt-BR → pt-BR", () => {
|
||||
assert.equal(detectBrowserLocale(["pt-PT"], REGIONAL_LOCALES, ALIASES), "pt");
|
||||
assert.equal(detectBrowserLocale(["pt-BR"], REGIONAL_LOCALES, ALIASES), "pt-BR");
|
||||
});
|
||||
|
||||
it("still returns null when nothing matches, even with aliases", () => {
|
||||
assert.equal(detectBrowserLocale(["ja-JP"], REGIONAL_LOCALES, ALIASES), null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user