feat(i18n): deep-merge EN fallback to fill missing locale keys (B/G1)

Export deepMergeFallback and apply it in getRequestConfig so all 39
non-EN locales receive EN text for any key absent in their JSON file.
Locale-specific translations always win; EN fills gaps only.
This commit is contained in:
diegosouzapw
2026-05-28 12:13:59 -03:00
parent d899a30777
commit bff01d6fe3

View File

@@ -3,23 +3,57 @@ import { cookies, headers } from "next/headers";
import { LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE } from "./config";
import type { Locale } from "./config";
const FALLBACK_LOCALE = "en";
/**
* Deep merge that mutates `target` with values from `source`.
* If both have an object at the same key, recurse.
* Otherwise prefer the existing value in `target` (locale-specific wins).
*/
export function deepMergeFallback(
target: Record<string, unknown>,
source: Record<string, unknown>
): Record<string, unknown> {
for (const [key, sourceValue] of Object.entries(source)) {
const targetValue = target[key];
if (
sourceValue !== null &&
typeof sourceValue === "object" &&
!Array.isArray(sourceValue) &&
targetValue !== null &&
typeof targetValue === "object" &&
!Array.isArray(targetValue)
) {
deepMergeFallback(targetValue as Record<string, unknown>, sourceValue as Record<string, unknown>);
} else if (targetValue === undefined) {
target[key] = sourceValue;
}
}
return target;
}
export default getRequestConfig(async () => {
// 1. Try cookie
const cookieStore = await cookies();
let locale: string = cookieStore.get(LOCALE_COOKIE)?.value || "";
// 2. Try custom header (set by middleware)
if (!locale) {
const headerStore = await headers();
locale = headerStore.get("x-locale") || "";
}
// 3. Validate & fallback
if (!LOCALES.includes(locale as Locale)) {
locale = DEFAULT_LOCALE;
}
const messages = (await import(`./messages/${locale}.json`)).default;
const localeMessages = (await import(`./messages/${locale}.json`)).default;
// G1: fall back to EN for any missing key. EN is loaded only once per request
// and only when the active locale is not EN itself (no-op).
let messages = localeMessages as Record<string, unknown>;
if (locale !== FALLBACK_LOCALE) {
const fallbackMessages = (await import(`./messages/${FALLBACK_LOCALE}.json`)).default as Record<string, unknown>;
messages = deepMergeFallback({ ...localeMessages }, fallbackMessages);
}
return {
locale,