From 5a32e42508c11083bbeadc33eb92dcf6a71207d7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 21:46:54 -0300 Subject: [PATCH] fix(i18n): deep-merge en.json as fallback for locales missing keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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". --- src/i18n/request.ts | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/i18n/request.ts b/src/i18n/request.ts index e5eb20991c..0e647263f3 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -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, + override: Record +): Record { + const out: Record = { ...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, + v as Record + ); + } 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; + if (locale === "en") { + return { locale, messages: enMessages }; + } + const localeMessages = (await import(`./messages/${locale}.json`)) + .default as Record; return { locale, - messages, + messages: mergeMessages(enMessages, localeMessages), }; });