diff --git a/src/i18n/resolver.test.ts b/src/i18n/resolver.test.ts index f47f9910..2c0fcf0f 100644 --- a/src/i18n/resolver.test.ts +++ b/src/i18n/resolver.test.ts @@ -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", }); }); diff --git a/src/i18n/resolver.ts b/src/i18n/resolver.ts index 105385b2..498c06e0 100644 --- a/src/i18n/resolver.ts +++ b/src/i18n/resolver.ts @@ -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 = new Set(LANGUAGES); const localeCodeSet: ReadonlySet = 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 = { 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 { diff --git a/src/i18n/useLocale.ts b/src/i18n/useLocale.ts index 7d282b15..3f459829 100644 --- a/src/i18n/useLocale.ts +++ b/src/i18n/useLocale.ts @@ -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 diff --git a/src/routes/[lang]/layout.tsx b/src/routes/[lang]/layout.tsx index 3e1cf2e7..9da80f97 100644 --- a/src/routes/[lang]/layout.tsx +++ b/src/routes/[lang]/layout.tsx @@ -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 (

404 — Unknown locale: {rawLang}

-

Supported: ru-ru, en-us, es-es, fr-fr, it-it, ja-jp, ko-kr, zh-cn, de-de

+

Supported: ru-ru, en-en, es-es, fr-fr, it-it, ja-ja, ko-ko, zh-zh, de-de

); } diff --git a/src/shared/seo/hreflang.test.ts b/src/shared/seo/hreflang.test.ts index 60078ed0..ade33d8c 100644 --- a/src/shared/seo/hreflang.test.ts +++ b/src/shared/seo/hreflang.test.ts @@ -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", () => { diff --git a/src/shared/seo/hreflang.ts b/src/shared/seo/hreflang.ts index 45a80ca5..1ffcf1c5 100644 --- a/src/shared/seo/hreflang.ts +++ b/src/shared/seo/hreflang.ts @@ -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;