diff --git a/src/lib/i18n/context.tsx b/src/lib/i18n/context.tsx index 478e1ea11..06d80c3fa 100644 --- a/src/lib/i18n/context.tsx +++ b/src/lib/i18n/context.tsx @@ -11,41 +11,7 @@ import { } from "react"; import { locales, defaultLocale, type Locale } from "./config"; import { makeFormatters, type Formatters } from "../format-locale"; - -import deMessages from "../../../messages/de.json"; -import enMessages from "../../../messages/en.json"; -import frMessages from "../../../messages/fr.json"; -import esMessages from "../../../messages/es.json"; -import itMessages from "../../../messages/it.json"; -import plMessages from "../../../messages/pl.json"; - -const allMessages: Record> = { - de: deMessages, - en: enMessages, - fr: frMessages, - es: esMessages, - it: itMessages, - pl: plMessages, -}; - -// Resolve nested key like "nav.dashboard" from a message object -function resolveKey( - messages: Record, - key: string, -): string | undefined { - const parts = key.split("."); - let current: unknown = messages; - - for (const part of parts) { - if (current && typeof current === "object" && part in current) { - current = (current as Record)[part]; - } else { - return undefined; - } - } - - return typeof current === "string" ? current : undefined; -} +import { allMessages, resolveKey } from "./shared-resolve"; interface I18nContextValue { locale: Locale; diff --git a/src/lib/i18n/server-translator.ts b/src/lib/i18n/server-translator.ts index dbd2d486b..950e4d17f 100644 --- a/src/lib/i18n/server-translator.ts +++ b/src/lib/i18n/server-translator.ts @@ -1,35 +1,5 @@ -import deMessages from "../../../messages/de.json"; -import enMessages from "../../../messages/en.json"; -import frMessages from "../../../messages/fr.json"; -import esMessages from "../../../messages/es.json"; -import itMessages from "../../../messages/it.json"; -import plMessages from "../../../messages/pl.json"; import { defaultLocale, type Locale } from "./config"; - -const allMessages: Record> = { - de: deMessages, - en: enMessages, - fr: frMessages, - es: esMessages, - it: itMessages, - pl: plMessages, -}; - -function resolveKey( - messages: Record, - key: string, -): string | undefined { - const parts = key.split("."); - let current: unknown = messages; - for (const part of parts) { - if (current && typeof current === "object" && part in current) { - current = (current as Record)[part]; - } else { - return undefined; - } - } - return typeof current === "string" ? current : undefined; -} +import { allMessages, resolveKey } from "./shared-resolve"; export interface ServerTranslator { locale: Locale; diff --git a/src/lib/i18n/shared-resolve.ts b/src/lib/i18n/shared-resolve.ts new file mode 100644 index 000000000..e0d262e24 --- /dev/null +++ b/src/lib/i18n/shared-resolve.ts @@ -0,0 +1,59 @@ +/** + * Shared message-bundle map and nested-key resolver used by both the + * client-side `I18nProvider` in `context.tsx` and the server-side + * `getServerTranslator()` in `server-translator.ts`. + * + * v1.4.27 B7 / BL-P4-11-S10 — the two surfaces previously each carried + * their own private copy of `allMessages` plus a near-identical + * `resolveKey` helper. The maps drifted twice during v1.4.18 / v1.4.25 + * polishing passes when a new locale was added; pinning both call-sites + * to the same import keeps client and server fallback semantics in + * lockstep and shrinks the surface a future locale addition has to + * touch from two to one. + */ +import deMessages from "../../../messages/de.json"; +import enMessages from "../../../messages/en.json"; +import frMessages from "../../../messages/fr.json"; +import esMessages from "../../../messages/es.json"; +import itMessages from "../../../messages/it.json"; +import plMessages from "../../../messages/pl.json"; +import { type Locale } from "./config"; + +/** + * Single source of truth for the per-locale message bundle. + * + * Both the client provider and the server translator import this map. + * The bundles ship as static JSON imports so they tree-shake into the + * client chunk that needs them and into the server chunk that needs + * them without duplicating. + */ +export const allMessages: Record> = { + de: deMessages, + en: enMessages, + fr: frMessages, + es: esMessages, + it: itMessages, + pl: plMessages, +}; + +/** + * Resolve a dotted key (`nav.dashboard`) inside a flat-or-nested + * message object. Returns `undefined` when the key path doesn't land + * on a string leaf so the caller can chain a fallback bundle (English, + * raw key) without ambiguity. + */ +export function resolveKey( + messages: Record, + key: string, +): string | undefined { + const parts = key.split("."); + let current: unknown = messages; + for (const part of parts) { + if (current && typeof current === "object" && part in current) { + current = (current as Record)[part]; + } else { + return undefined; + } + } + return typeof current === "string" ? current : undefined; +}