i18n: switch URL locale codes to xx-xx (Angular contract)
Angular's LocalizationService reads `Country = baseHref[1..3]` and `Language = baseHref[4..6]` — both halves are the same 2-letter language code (`/ru-ru/`, `/en-en/`, `/zh-zh/`, …), confirmed by the spec fixtures using `/en-en/onlineboard/...`. The previous shipping codes mixed in IETF region codes (`en-us`, `ja-jp`, `ko-kr`, `zh-cn`) which do not match the customer's URL surface. Renamed: en-us → en-en ja-jp → ja-ja ko-kr → ko-ko zh-cn → zh-zh The `LANGUAGE_TO_LOCALE_CODE` table now mirrors Angular exactly. Resolver/hreflang tests + layout 404 message updated.
This commit is contained in:
@@ -33,13 +33,13 @@ describe("isLanguage", () => {
|
||||
describe("resolveLocaleFromPath", () => {
|
||||
it("extracts BCP-47 locale from the first path segment", () => {
|
||||
expect(resolveLocaleFromPath("/ru-ru/onlineboard")).toBe("ru-ru");
|
||||
expect(resolveLocaleFromPath("/en-us/onlineboard/flight/SU100")).toBe("en-us");
|
||||
expect(resolveLocaleFromPath("/en-en/onlineboard/flight/SU100")).toBe("en-en");
|
||||
expect(resolveLocaleFromPath("/de-de/schedule")).toBe("de-de");
|
||||
});
|
||||
|
||||
it("auto-promotes a bare short language code to its BCP-47 cousin", () => {
|
||||
expect(resolveLocaleFromPath("/ru/onlineboard")).toBe("ru-ru");
|
||||
expect(resolveLocaleFromPath("/en/onlineboard/flight/SU100")).toBe("en-us");
|
||||
expect(resolveLocaleFromPath("/en/onlineboard/flight/SU100")).toBe("en-en");
|
||||
expect(resolveLocaleFromPath("/de/schedule")).toBe("de-de");
|
||||
});
|
||||
|
||||
@@ -63,8 +63,8 @@ describe("stripLocaleFromPath", () => {
|
||||
locale: "ru-ru",
|
||||
rest: "/onlineboard",
|
||||
});
|
||||
expect(stripLocaleFromPath("/en-us/onlineboard/flight/SU100")).toEqual({
|
||||
locale: "en-us",
|
||||
expect(stripLocaleFromPath("/en-en/onlineboard/flight/SU100")).toEqual({
|
||||
locale: "en-en",
|
||||
rest: "/onlineboard/flight/SU100",
|
||||
});
|
||||
});
|
||||
|
||||
+17
-12
@@ -3,7 +3,7 @@
|
||||
* file lookup, API path segment, and `Accept-Language` header.
|
||||
*
|
||||
* Mirrors Angular's `LocalizationService`: URL is BCP-47 (`/ru-ru/`,
|
||||
* `/en-us/`, `/zh-cn/`...), backend + translation files use the short
|
||||
* `/en-en/`, `/zh-zh/`...), backend + translation files use the short
|
||||
* 2-letter language part only (`ru`, `en`, `zh`...). This split keeps
|
||||
* the customer's URL contract while reusing a single set of locale
|
||||
* resources.
|
||||
@@ -13,13 +13,13 @@ export type Language = "ru" | "en" | "es" | "fr" | "it" | "ja" | "ko" | "zh" | "
|
||||
|
||||
export type LocaleCode =
|
||||
| "ru-ru"
|
||||
| "en-us"
|
||||
| "en-en"
|
||||
| "es-es"
|
||||
| "fr-fr"
|
||||
| "it-it"
|
||||
| "ja-jp"
|
||||
| "ko-kr"
|
||||
| "zh-cn"
|
||||
| "ja-ja"
|
||||
| "ko-ko"
|
||||
| "zh-zh"
|
||||
| "de-de";
|
||||
|
||||
export const LANGUAGES: readonly Language[] = [
|
||||
@@ -27,7 +27,7 @@ export const LANGUAGES: readonly Language[] = [
|
||||
] as const;
|
||||
|
||||
export const LOCALE_CODES: readonly LocaleCode[] = [
|
||||
"ru-ru", "en-us", "es-es", "fr-fr", "it-it", "ja-jp", "ko-kr", "zh-cn", "de-de",
|
||||
"ru-ru", "en-en", "es-es", "fr-fr", "it-it", "ja-ja", "ko-ko", "zh-zh", "de-de",
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_LOCALE_CODE: LocaleCode = "ru-ru";
|
||||
@@ -36,15 +36,20 @@ export const DEFAULT_LANGUAGE: Language = "ru";
|
||||
const languageSet: ReadonlySet<string> = new Set(LANGUAGES);
|
||||
const localeCodeSet: ReadonlySet<string> = new Set(LOCALE_CODES);
|
||||
|
||||
// Angular's URL contract is `/{country}-{language}` where the two
|
||||
// halves repeat the same 2-letter language code (see
|
||||
// `ClientApp/src/app/shared/services/localization.service.ts` —
|
||||
// `Country = baseHref[1..3]`, `Language = baseHref[4..6]`). We mirror
|
||||
// that exact shape so the React MF remote shares Angular's URL surface.
|
||||
const LANGUAGE_TO_LOCALE_CODE: Record<Language, LocaleCode> = {
|
||||
ru: "ru-ru",
|
||||
en: "en-us",
|
||||
en: "en-en",
|
||||
es: "es-es",
|
||||
fr: "fr-fr",
|
||||
it: "it-it",
|
||||
ja: "ja-jp",
|
||||
ko: "ko-kr",
|
||||
zh: "zh-cn",
|
||||
ja: "ja-ja",
|
||||
ko: "ko-ko",
|
||||
zh: "zh-zh",
|
||||
de: "de-de",
|
||||
};
|
||||
|
||||
@@ -58,7 +63,7 @@ export function isLocaleCode(x: string): x is LocaleCode {
|
||||
|
||||
/**
|
||||
* Extract the language part from a BCP-47 locale code.
|
||||
* `localeToLanguage("en-us")` → `"en"`.
|
||||
* `localeToLanguage("en-en")` → `"en"`.
|
||||
*/
|
||||
export function localeToLanguage(code: LocaleCode): Language {
|
||||
return code.slice(0, 2) as Language;
|
||||
@@ -66,7 +71,7 @@ export function localeToLanguage(code: LocaleCode): Language {
|
||||
|
||||
/**
|
||||
* Promote a short language code to its canonical URL locale code.
|
||||
* `languageToLocale("en")` → `"en-us"`. Used when the URL needs the
|
||||
* `languageToLocale("en")` → `"en-en"`. Used when the URL needs the
|
||||
* BCP-47 form but only the short language is in hand.
|
||||
*/
|
||||
export function languageToLocale(lang: Language): LocaleCode {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* `useLocale()` — central hook that resolves the current page's
|
||||
* locale from `useParams<{lang}>` and exposes both forms:
|
||||
*
|
||||
* - `locale` — BCP-47 URL code (`"ru-ru"`, `"en-us"`, …) for
|
||||
* - `locale` — BCP-47 URL code (`"ru-ru"`, `"en-en"`, …) for
|
||||
* building outgoing links and reading from
|
||||
* `window.location`.
|
||||
* - `language` — short code (`"ru"`, `"en"`, …) for the i18n file
|
||||
|
||||
@@ -15,12 +15,12 @@ import type i18next from "i18next";
|
||||
|
||||
// Register all PrimeReact locales once at module load. The active
|
||||
// locale is selected per-render via setPrimeLocale() so that switching
|
||||
// between /ru-ru and /en-us swaps Calendar/AutoComplete labels too.
|
||||
// between /ru-ru and /en-en swaps Calendar/AutoComplete labels too.
|
||||
registerPrimeLocales(addLocale);
|
||||
|
||||
/**
|
||||
* Locale-scoped layout. Validates the `lang` URL segment (BCP-47:
|
||||
* `ru-ru`, `en-us`, …; legacy short codes like `ru` are auto-promoted
|
||||
* `ru-ru`, `en-en`, …; legacy short codes like `ru` are auto-promoted
|
||||
* to their canonical BCP-47 cousin), creates an i18n instance for
|
||||
* the matching language file, and updates the shared ApiClient's
|
||||
* locale so backend responses come back in the right language.
|
||||
@@ -64,7 +64,7 @@ export default function LangLayout(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h2>404 — Unknown locale: {rawLang}</h2>
|
||||
<p>Supported: ru-ru, en-us, es-es, fr-fr, it-it, ja-jp, ko-kr, zh-cn, de-de</p>
|
||||
<p>Supported: ru-ru, en-en, es-es, fr-fr, it-it, ja-ja, ko-ko, zh-zh, de-de</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ describe("buildHreflangSet", () => {
|
||||
});
|
||||
|
||||
const en = result.find((entry) => entry.lang === "en");
|
||||
expect(en?.href).toBe("https://www.aeroflot.ru/en-us/onlineboard");
|
||||
expect(en?.href).toBe("https://www.aeroflot.ru/en-en/onlineboard");
|
||||
|
||||
const ja = result.find((entry) => entry.lang === "ja");
|
||||
expect(ja?.href).toBe("https://www.aeroflot.ru/ja-jp/onlineboard");
|
||||
expect(ja?.href).toBe("https://www.aeroflot.ru/ja-ja/onlineboard");
|
||||
});
|
||||
|
||||
it("preserves paths with nested segments", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface HreflangEntry {
|
||||
* Returns 9 language entries + 1 x-default entry (pointing to the
|
||||
* default language). Hreflang attribute uses the short language code
|
||||
* (`hreflang="en"`); the URL itself uses the BCP-47 locale code
|
||||
* (`/en-us/...`) to match the customer's URL contract.
|
||||
* (`/en-en/...`) to match the customer's URL contract.
|
||||
*/
|
||||
export function buildHreflangSet(args: {
|
||||
canonicalOrigin: string;
|
||||
|
||||
Reference in New Issue
Block a user