@open-mercato/shared 0.8.1-develop.7274.1.21a0083828 → 0.8.1-develop.7275.1.f772b944d4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/i18n/server.js
CHANGED
|
@@ -56,12 +56,12 @@ async function loadDictionary(locale) {
|
|
|
56
56
|
const needsDefaultLocaleBase = locale !== defaultLocale && !locales.includes(locale);
|
|
57
57
|
const merged = needsDefaultLocaleBase ? { ...await loadDictionary(defaultLocale) } : {};
|
|
58
58
|
const baseRaw = await loadAppDictionary(locale);
|
|
59
|
-
Object.assign(merged, flattenDictionary(baseRaw));
|
|
60
59
|
const modules = tryGetModules() ?? [];
|
|
61
60
|
for (const m of modules) {
|
|
62
61
|
const dict = m.translations?.[locale];
|
|
63
62
|
if (dict) Object.assign(merged, flattenDictionary(dict));
|
|
64
63
|
}
|
|
64
|
+
Object.assign(merged, flattenDictionary(baseRaw));
|
|
65
65
|
setCachedDictionary(locale, merged);
|
|
66
66
|
return merged;
|
|
67
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/server.ts"],
|
|
4
|
-
"sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport type { Dict } from './context'\nimport { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport { tryGetModules } from '../modules/registry'\nimport { loadAppDictionary } from './app-dictionaries'\nimport { getCachedDictionary, setCachedDictionary } from './dictionary-cache'\nimport { getSupportedLocales, resolveSupportedLocalesForRequest } from './locale-registry'\n\n// Re-export for backwards compatibility\nexport { registerModules, getModules } from '../modules/registry'\nexport { registerAppDictionaryLoader } from './app-dictionaries'\nexport { invalidateDictionaryCache } from './dictionary-cache'\nexport {\n registerLocales,\n getSupportedLocales,\n registerSupportedLocalesResolver,\n resolveSupportedLocalesForRequest,\n} from './locale-registry'\n\nfunction flattenDictionary(source: unknown, prefix = ''): Dict {\n if (!source || typeof source !== 'object' || Array.isArray(source)) return {}\n const result: Dict = {}\n for (const [key, value] of Object.entries(source as Record<string, unknown>)) {\n if (!key) continue\n const nextKey = prefix ? `${prefix}.${key}` : key\n if (typeof value === 'string') {\n result[nextKey] = value\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n Object.assign(result, flattenDictionary(value, nextKey))\n }\n }\n return result\n}\n\nexport type DetectLocaleOptions = {\n /**\n * Restrict detection to this set \u2014 typically the current tenant's selection,\n * resolved by the caller via `resolveSupportedLocalesForRequest()`. Omitted,\n * detection uses the process-wide supported set, which is the prior behaviour.\n */\n supportedLocales?: readonly Locale[]\n}\n\nexport async function detectLocale(options?: DetectLocaleOptions): Promise<Locale> {\n // Ops-level override: pin the whole app to one locale (default: unset).\n const forced = resolveForcedLocale(process.env)\n if (forced) return forced\n const supported = options?.supportedLocales ?? getSupportedLocales()\n // Dynamic import to avoid requiring Next.js in non-Next.js contexts (CLI, tests)\n try {\n const { cookies, headers } = await import('next/headers')\n try {\n const c = (await cookies()).get('locale')?.value\n if (c && supported.includes(c as Locale)) return c as Locale\n } catch {\n // cookies() may not be available outside request context (e.g., in tests)\n }\n try {\n const accept = (await headers()).get('accept-language') || ''\n // Matched against the served set rather than the process-wide one, so a\n // header like `de, en` on a tenant that serves only `en` picks `en`\n // instead of matching `de` first and then discarding the whole header.\n const match = resolveLocaleFromAcceptLanguage(accept, supported)\n if (match) return match\n } catch {\n // headers() may not be available outside request context (e.g., in tests)\n }\n } catch {\n // next/headers not available (CLI context)\n }\n // The caller may have narrowed the set past the default locale, and returning\n // a locale outside the served set would render a page whose own language\n // switcher does not offer the language it is written in.\n // `resolveSupportedLocalesForRequest` keeps `defaultLocale` in the set for\n // exactly this reason; the `supported[0]` arm covers a caller that narrowed by\n // hand and did not.\n if (supported.includes(defaultLocale)) return defaultLocale\n return supported[0] ?? defaultLocale\n}\n\nexport async function loadDictionary(locale: Locale): Promise<Dict> {\n // Locale dictionaries are immutable at runtime, so the flatten+merge below\n // only needs to run once per locale. The cache is invalidated whenever\n // modules or the app dictionary loader are (re)registered.\n const cached = getCachedDictionary(locale)\n if (cached) return cached\n // A locale the platform does not ship has no dictionaries of its own yet, and\n // roughly a quarter of `t()` call sites pass no inline fallback \u2014 without a\n // base layer those render as raw keys. Layering the default locale underneath\n // makes an app- or operator-added locale degrade to English instead, which is\n // what every comparable platform does. Shipped locales skip this entirely and\n // keep their previous merge semantics byte for byte.\n const needsDefaultLocaleBase =\n locale !== defaultLocale && !(locales as readonly string[]).includes(locale)\n const merged: Dict = needsDefaultLocaleBase ? { ...(await loadDictionary(defaultLocale)) } : {}\n // Load from registry instead of @/ import (works in standalone packages)\n
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe,eAA4B;AAEpD,SAAS,qBAAqB,uCAAuC;AACrE,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB,2BAA2B;AACzD,SAAS,2BAA8D;AAGvE,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,mCAAmC;AAC5C,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA,uBAAAA;AAAA,EACA;AAAA,EACA,qCAAAC;AAAA,OACK;AAEP,SAAS,kBAAkB,QAAiB,SAAS,IAAU;AAC7D,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAC5E,QAAM,SAAe,CAAC;AACtB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC5E,QAAI,CAAC,IAAK;AACV,UAAM,UAAU,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAC9C,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,OAAO,IAAI;AAAA,IACpB,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtE,aAAO,OAAO,QAAQ,kBAAkB,OAAO,OAAO,CAAC;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,aAAa,SAAgD;AAEjF,QAAM,SAAS,oBAAoB,QAAQ,GAAG;AAC9C,MAAI,OAAQ,QAAO;AACnB,QAAM,YAAY,SAAS,oBAAoB,oBAAoB;AAEnE,MAAI;AACF,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,OAAO,cAAc;AACxD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAC3C,UAAI,KAAK,UAAU,SAAS,CAAW,EAAG,QAAO;AAAA,IACnD,QAAQ;AAAA,IAER;AACA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,iBAAiB,KAAK;AAI3D,YAAM,QAAQ,gCAAgC,QAAQ,SAAS;AAC/D,UAAI,MAAO,QAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,UAAU,SAAS,aAAa,EAAG,QAAO;AAC9C,SAAO,UAAU,CAAC,KAAK;AACzB;AAEA,eAAsB,eAAe,QAA+B;AAIlE,QAAM,SAAS,oBAAoB,MAAM;AACzC,MAAI,OAAQ,QAAO;AAOnB,QAAM,yBACJ,WAAW,iBAAiB,CAAE,QAA8B,SAAS,MAAM;AAC7E,QAAM,SAAe,yBAAyB,EAAE,GAAI,MAAM,eAAe,aAAa,EAAG,IAAI,CAAC;
|
|
4
|
+
"sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport type { Dict } from './context'\nimport { resolveForcedLocale, resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport { tryGetModules } from '../modules/registry'\nimport { loadAppDictionary } from './app-dictionaries'\nimport { getCachedDictionary, setCachedDictionary } from './dictionary-cache'\nimport { getSupportedLocales, resolveSupportedLocalesForRequest } from './locale-registry'\n\n// Re-export for backwards compatibility\nexport { registerModules, getModules } from '../modules/registry'\nexport { registerAppDictionaryLoader } from './app-dictionaries'\nexport { invalidateDictionaryCache } from './dictionary-cache'\nexport {\n registerLocales,\n getSupportedLocales,\n registerSupportedLocalesResolver,\n resolveSupportedLocalesForRequest,\n} from './locale-registry'\n\nfunction flattenDictionary(source: unknown, prefix = ''): Dict {\n if (!source || typeof source !== 'object' || Array.isArray(source)) return {}\n const result: Dict = {}\n for (const [key, value] of Object.entries(source as Record<string, unknown>)) {\n if (!key) continue\n const nextKey = prefix ? `${prefix}.${key}` : key\n if (typeof value === 'string') {\n result[nextKey] = value\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n Object.assign(result, flattenDictionary(value, nextKey))\n }\n }\n return result\n}\n\nexport type DetectLocaleOptions = {\n /**\n * Restrict detection to this set \u2014 typically the current tenant's selection,\n * resolved by the caller via `resolveSupportedLocalesForRequest()`. Omitted,\n * detection uses the process-wide supported set, which is the prior behaviour.\n */\n supportedLocales?: readonly Locale[]\n}\n\nexport async function detectLocale(options?: DetectLocaleOptions): Promise<Locale> {\n // Ops-level override: pin the whole app to one locale (default: unset).\n const forced = resolveForcedLocale(process.env)\n if (forced) return forced\n const supported = options?.supportedLocales ?? getSupportedLocales()\n // Dynamic import to avoid requiring Next.js in non-Next.js contexts (CLI, tests)\n try {\n const { cookies, headers } = await import('next/headers')\n try {\n const c = (await cookies()).get('locale')?.value\n if (c && supported.includes(c as Locale)) return c as Locale\n } catch {\n // cookies() may not be available outside request context (e.g., in tests)\n }\n try {\n const accept = (await headers()).get('accept-language') || ''\n // Matched against the served set rather than the process-wide one, so a\n // header like `de, en` on a tenant that serves only `en` picks `en`\n // instead of matching `de` first and then discarding the whole header.\n const match = resolveLocaleFromAcceptLanguage(accept, supported)\n if (match) return match\n } catch {\n // headers() may not be available outside request context (e.g., in tests)\n }\n } catch {\n // next/headers not available (CLI context)\n }\n // The caller may have narrowed the set past the default locale, and returning\n // a locale outside the served set would render a page whose own language\n // switcher does not offer the language it is written in.\n // `resolveSupportedLocalesForRequest` keeps `defaultLocale` in the set for\n // exactly this reason; the `supported[0]` arm covers a caller that narrowed by\n // hand and did not.\n if (supported.includes(defaultLocale)) return defaultLocale\n return supported[0] ?? defaultLocale\n}\n\nexport async function loadDictionary(locale: Locale): Promise<Dict> {\n // Locale dictionaries are immutable at runtime, so the flatten+merge below\n // only needs to run once per locale. The cache is invalidated whenever\n // modules or the app dictionary loader are (re)registered.\n const cached = getCachedDictionary(locale)\n if (cached) return cached\n // A locale the platform does not ship has no dictionaries of its own yet, and\n // roughly a quarter of `t()` call sites pass no inline fallback \u2014 without a\n // base layer those render as raw keys. Layering the default locale underneath\n // makes an app- or operator-added locale degrade to English instead, which is\n // what every comparable platform does. Shipped locales skip this entirely and\n // keep their previous merge semantics byte for byte.\n const needsDefaultLocaleBase =\n locale !== defaultLocale && !(locales as readonly string[]).includes(locale)\n const merged: Dict = needsDefaultLocaleBase ? { ...(await loadDictionary(defaultLocale)) } : {}\n // Load from registry instead of @/ import (works in standalone packages).\n // Awaited BEFORE tryGetModules() below: the host app's dictionary loader\n // (see apps/mercato/src/lib/i18n/register-dictionary-loader.ts) lazily\n // imports this locale's per-module translation bundle and registers it via\n // `registerModules()` as a side effect of resolving \u2014 reading the module\n // registry first would race a locale's first-ever load and permanently\n // cache a dictionary missing every lazily-registered module's translations\n // for it (the registration event that would normally invalidate the cache\n // fires too early, before this call has anything cached to invalidate).\n const baseRaw = await loadAppDictionary(locale)\n // Route handlers translate their responses, so they resolve a dictionary even\n // when they are exercised in isolation without a bootstrapped registry. Module\n // dictionaries are layered first so the host app dictionary \u2014 merged last,\n // below \u2014 is always the final word and can override any module-defined key.\n const modules = tryGetModules() ?? []\n for (const m of modules) {\n const dict = m.translations?.[locale]\n if (dict) Object.assign(merged, flattenDictionary(dict))\n }\n Object.assign(merged, flattenDictionary(baseRaw))\n setCachedDictionary(locale, merged)\n return merged\n}\n\n/**\n * Detect the locale and load its dictionary in one step.\n *\n * `options` is forwarded to `detectLocale`, so a caller that has already\n * resolved the request's served set (a layout mounting its own `I18nProvider`)\n * detects against that set instead of the process-wide one. Omitted \u2014 which is\n * every route handler \u2014 behaviour is unchanged and no tenant lookup is made.\n */\nexport async function resolveTranslations(options?: DetectLocaleOptions) {\n const locale = await detectLocale(options)\n const dict = await loadDictionary(locale)\n const t = createTranslator(dict)\n const translate = createFallbackTranslator(dict)\n return { locale, dict, t, translate }\n}\n// Hint Next.js to keep this server-only; ignore if unavailable when running scripts outside Next.\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // noop: allows running generator scripts without Next's server-only package\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe,eAA4B;AAEpD,SAAS,qBAAqB,uCAAuC;AACrE,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB,2BAA2B;AACzD,SAAS,2BAA8D;AAGvE,SAAS,iBAAiB,kBAAkB;AAC5C,SAAS,mCAAmC;AAC5C,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA,uBAAAA;AAAA,EACA;AAAA,EACA,qCAAAC;AAAA,OACK;AAEP,SAAS,kBAAkB,QAAiB,SAAS,IAAU;AAC7D,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAC5E,QAAM,SAAe,CAAC;AACtB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC5E,QAAI,CAAC,IAAK;AACV,UAAM,UAAU,SAAS,GAAG,MAAM,IAAI,GAAG,KAAK;AAC9C,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,OAAO,IAAI;AAAA,IACpB,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtE,aAAO,OAAO,QAAQ,kBAAkB,OAAO,OAAO,CAAC;AAAA,IACzD;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,aAAa,SAAgD;AAEjF,QAAM,SAAS,oBAAoB,QAAQ,GAAG;AAC9C,MAAI,OAAQ,QAAO;AACnB,QAAM,YAAY,SAAS,oBAAoB,oBAAoB;AAEnE,MAAI;AACF,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,OAAO,cAAc;AACxD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAC3C,UAAI,KAAK,UAAU,SAAS,CAAW,EAAG,QAAO;AAAA,IACnD,QAAQ;AAAA,IAER;AACA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,iBAAiB,KAAK;AAI3D,YAAM,QAAQ,gCAAgC,QAAQ,SAAS;AAC/D,UAAI,MAAO,QAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF,QAAQ;AAAA,EAER;AAOA,MAAI,UAAU,SAAS,aAAa,EAAG,QAAO;AAC9C,SAAO,UAAU,CAAC,KAAK;AACzB;AAEA,eAAsB,eAAe,QAA+B;AAIlE,QAAM,SAAS,oBAAoB,MAAM;AACzC,MAAI,OAAQ,QAAO;AAOnB,QAAM,yBACJ,WAAW,iBAAiB,CAAE,QAA8B,SAAS,MAAM;AAC7E,QAAM,SAAe,yBAAyB,EAAE,GAAI,MAAM,eAAe,aAAa,EAAG,IAAI,CAAC;AAU9F,QAAM,UAAU,MAAM,kBAAkB,MAAM;AAK9C,QAAM,UAAU,cAAc,KAAK,CAAC;AACpC,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,EAAE,eAAe,MAAM;AACpC,QAAI,KAAM,QAAO,OAAO,QAAQ,kBAAkB,IAAI,CAAC;AAAA,EACzD;AACA,SAAO,OAAO,QAAQ,kBAAkB,OAAO,CAAC;AAChD,sBAAoB,QAAQ,MAAM;AAClC,SAAO;AACT;AAUA,eAAsB,oBAAoB,SAA+B;AACvE,QAAM,SAAS,MAAM,aAAa,OAAO;AACzC,QAAM,OAAO,MAAM,eAAe,MAAM;AACxC,QAAM,IAAI,iBAAiB,IAAI;AAC/B,QAAM,YAAY,yBAAyB,IAAI;AAC/C,SAAO,EAAE,QAAQ,MAAM,GAAG,UAAU;AACtC;AAEA,IAAI;AAEF,UAAQ,aAAa;AACvB,QAAQ;AAER;",
|
|
6
6
|
"names": ["getSupportedLocales", "resolveSupportedLocalesForRequest"]
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7275.1.f772b944d4';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7275.1.f772b944d4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@mikro-orm/core": "^7.1.14",
|
|
114
114
|
"@mikro-orm/decorators": "^7.1.14",
|
|
115
115
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
116
|
-
"@open-mercato/cache": "0.8.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.8.1-develop.7275.1.f772b944d4",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|
|
@@ -50,6 +50,55 @@ describe('dictionary fallback for locales the platform does not ship', () => {
|
|
|
50
50
|
})
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
+
describe('host app override precedence', () => {
|
|
54
|
+
it('lets the host app dictionary override a module-defined key', async () => {
|
|
55
|
+
registerModules([{ translations: { en: { greeting: 'Hello from module' } } }] as any)
|
|
56
|
+
|
|
57
|
+
const en = await loadDictionary('en')
|
|
58
|
+
|
|
59
|
+
// The host's own locale file must win — a module MUST NOT be able to
|
|
60
|
+
// silently shadow a key the host app explicitly set (issue #5995).
|
|
61
|
+
expect(en.greeting).toBe('Hello')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('still falls back to a module key the host app does not define', async () => {
|
|
65
|
+
registerModules([{ translations: { en: { moduleOnly: 'Module value' } } }] as any)
|
|
66
|
+
|
|
67
|
+
const en = await loadDictionary('en')
|
|
68
|
+
|
|
69
|
+
expect(en.moduleOnly).toBe('Module value')
|
|
70
|
+
expect(en.greeting).toBe('Hello')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('sees a module registered as a side effect of resolving the app loader on the first load of a locale', async () => {
|
|
74
|
+
// Mirrors apps/mercato/src/lib/i18n/register-dictionary-loader.ts: the
|
|
75
|
+
// registered loader lazily imports the locale's per-module translation
|
|
76
|
+
// bundle and calls registerModules() as a side effect, after an internal
|
|
77
|
+
// await. loadDictionary() MUST await loadAppDictionary() before reading
|
|
78
|
+
// tryGetModules() — reading the registry first would race the locale's
|
|
79
|
+
// first-ever load and permanently cache a dictionary missing this
|
|
80
|
+
// lazily-registered module's translations (regression fixed in
|
|
81
|
+
// 4f3a8e255).
|
|
82
|
+
registerAppDictionaryLoader(async (locale: Locale) => {
|
|
83
|
+
if (locale === 'pl') {
|
|
84
|
+
await Promise.resolve()
|
|
85
|
+
registerModules([
|
|
86
|
+
{ translations: { pl: { greeting: 'Cześć (module)', fromModule: 'Z modułu' } } },
|
|
87
|
+
] as any)
|
|
88
|
+
}
|
|
89
|
+
return APP_DICTIONARIES[locale] ?? {}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const pl = await loadDictionary('pl')
|
|
93
|
+
|
|
94
|
+
// The module key is present on the very first load, proving the module
|
|
95
|
+
// registered inside the loader was read after it resolved.
|
|
96
|
+
expect(pl.fromModule).toBe('Z modułu')
|
|
97
|
+
// The app dictionary still wins the collision (issue #5995).
|
|
98
|
+
expect(pl.greeting).toBe('Cześć')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
53
102
|
describe('an app-registered locale', () => {
|
|
54
103
|
it('falls back to the default locale instead of rendering raw keys', async () => {
|
|
55
104
|
registerLocales(['cs'])
|
package/src/lib/i18n/server.ts
CHANGED
|
@@ -94,18 +94,26 @@ export async function loadDictionary(locale: Locale): Promise<Dict> {
|
|
|
94
94
|
const needsDefaultLocaleBase =
|
|
95
95
|
locale !== defaultLocale && !(locales as readonly string[]).includes(locale)
|
|
96
96
|
const merged: Dict = needsDefaultLocaleBase ? { ...(await loadDictionary(defaultLocale)) } : {}
|
|
97
|
-
// Load from registry instead of @/ import (works in standalone packages)
|
|
97
|
+
// Load from registry instead of @/ import (works in standalone packages).
|
|
98
|
+
// Awaited BEFORE tryGetModules() below: the host app's dictionary loader
|
|
99
|
+
// (see apps/mercato/src/lib/i18n/register-dictionary-loader.ts) lazily
|
|
100
|
+
// imports this locale's per-module translation bundle and registers it via
|
|
101
|
+
// `registerModules()` as a side effect of resolving — reading the module
|
|
102
|
+
// registry first would race a locale's first-ever load and permanently
|
|
103
|
+
// cache a dictionary missing every lazily-registered module's translations
|
|
104
|
+
// for it (the registration event that would normally invalidate the cache
|
|
105
|
+
// fires too early, before this call has anything cached to invalidate).
|
|
98
106
|
const baseRaw = await loadAppDictionary(locale)
|
|
99
|
-
Object.assign(merged, flattenDictionary(baseRaw))
|
|
100
107
|
// Route handlers translate their responses, so they resolve a dictionary even
|
|
101
|
-
// when they are exercised in isolation without a bootstrapped registry.
|
|
102
|
-
//
|
|
103
|
-
//
|
|
108
|
+
// when they are exercised in isolation without a bootstrapped registry. Module
|
|
109
|
+
// dictionaries are layered first so the host app dictionary — merged last,
|
|
110
|
+
// below — is always the final word and can override any module-defined key.
|
|
104
111
|
const modules = tryGetModules() ?? []
|
|
105
112
|
for (const m of modules) {
|
|
106
113
|
const dict = m.translations?.[locale]
|
|
107
114
|
if (dict) Object.assign(merged, flattenDictionary(dict))
|
|
108
115
|
}
|
|
116
|
+
Object.assign(merged, flattenDictionary(baseRaw))
|
|
109
117
|
setCachedDictionary(locale, merged)
|
|
110
118
|
return merged
|
|
111
119
|
}
|