fix(i18n): deep-merge en.json as fallback for locales missing keys

D12 of master-plan-21 assumed next-intl had a built-in fallback to EN
already configured. It did not — request.ts loaded only
messages/${locale}.json and no getMessageFallback was defined, so any
key absent in the user's locale rendered the key path literally
(for example "memory.concept.title").

Plan 21 added 156 memory.* keys to en and pt-BR but the other 39
locales kept only the pre-existing 36 memory.* keys, so users on those
locales saw raw key paths across the new Memory studio.

Fix: load en.json as the base and deep-merge the locale-specific
messages on top. Existing translations are untouched; only missing
keys fall back to English. Satisfies §7 "i18n 41 locales".
This commit is contained in:
diegosouzapw
2026-05-28 21:46:54 -03:00
parent feb0e983eb
commit 5a32e42508

View File

@@ -3,6 +3,34 @@ import { cookies, headers } from "next/headers";
import { LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE } from "./config";
import type { Locale } from "./config";
// Deep-merge fallback messages so locales that lack newer keys (e.g. memory.*
// added by plan 21) silently fall back to EN instead of rendering the key path.
function mergeMessages(
fallback: Record<string, unknown>,
override: Record<string, unknown>
): Record<string, unknown> {
const out: Record<string, unknown> = { ...fallback };
for (const [k, v] of Object.entries(override)) {
const baseVal = out[k];
if (
v !== null &&
typeof v === "object" &&
!Array.isArray(v) &&
baseVal !== null &&
typeof baseVal === "object" &&
!Array.isArray(baseVal)
) {
out[k] = mergeMessages(
baseVal as Record<string, unknown>,
v as Record<string, unknown>
);
} else {
out[k] = v;
}
}
return out;
}
export default getRequestConfig(async () => {
// 1. Try cookie
const cookieStore = await cookies();
@@ -19,10 +47,16 @@ export default getRequestConfig(async () => {
locale = DEFAULT_LOCALE;
}
const messages = (await import(`./messages/${locale}.json`)).default;
const enMessages = (await import("./messages/en.json"))
.default as Record<string, unknown>;
if (locale === "en") {
return { locale, messages: enMessages };
}
const localeMessages = (await import(`./messages/${locale}.json`))
.default as Record<string, unknown>;
return {
locale,
messages,
messages: mergeMessages(enMessages, localeMessages),
};
});