@open-mercato/shared 0.6.4-develop.4106.1.12c205a613 → 0.6.4-develop.4113.1.5e87922616

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 213 entry points
1
+ [build:shared] found 214 entry points
2
2
  [build:shared] built successfully
@@ -1,6 +1,8 @@
1
+ import { invalidateDictionaryCache } from "./dictionary-cache.js";
1
2
  let _appDictionaryLoader = null;
2
3
  function registerAppDictionaryLoader(loader) {
3
4
  _appDictionaryLoader = loader;
5
+ invalidateDictionaryCache();
4
6
  }
5
7
  async function loadAppDictionary(locale) {
6
8
  if (!_appDictionaryLoader) return {};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/i18n/app-dictionaries.ts"],
4
- "sourcesContent": ["import type { Locale } from './config'\n\ntype DictionaryLoader = (locale: Locale) => Promise<Record<string, unknown>>\n\nlet _appDictionaryLoader: DictionaryLoader | null = null\n\nexport function registerAppDictionaryLoader(loader: DictionaryLoader): void {\n _appDictionaryLoader = loader\n}\n\nexport async function loadAppDictionary(locale: Locale): Promise<Record<string, unknown>> {\n if (!_appDictionaryLoader) return {}\n try {\n return await _appDictionaryLoader(locale)\n } catch {\n return {}\n }\n}\n"],
5
- "mappings": "AAIA,IAAI,uBAAgD;AAE7C,SAAS,4BAA4B,QAAgC;AAC1E,yBAAuB;AACzB;AAEA,eAAsB,kBAAkB,QAAkD;AACxF,MAAI,CAAC,qBAAsB,QAAO,CAAC;AACnC,MAAI;AACF,WAAO,MAAM,qBAAqB,MAAM;AAAA,EAC1C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;",
4
+ "sourcesContent": ["import type { Locale } from './config'\nimport { invalidateDictionaryCache } from './dictionary-cache'\n\ntype DictionaryLoader = (locale: Locale) => Promise<Record<string, unknown>>\n\nlet _appDictionaryLoader: DictionaryLoader | null = null\n\nexport function registerAppDictionaryLoader(loader: DictionaryLoader): void {\n _appDictionaryLoader = loader\n invalidateDictionaryCache()\n}\n\nexport async function loadAppDictionary(locale: Locale): Promise<Record<string, unknown>> {\n if (!_appDictionaryLoader) return {}\n try {\n return await _appDictionaryLoader(locale)\n } catch {\n return {}\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,iCAAiC;AAI1C,IAAI,uBAAgD;AAE7C,SAAS,4BAA4B,QAAgC;AAC1E,yBAAuB;AACvB,4BAA0B;AAC5B;AAEA,eAAsB,kBAAkB,QAAkD;AACxF,MAAI,CAAC,qBAAsB,QAAO,CAAC;AACnC,MAAI;AACF,WAAO,MAAM,qBAAqB,MAAM;AAAA,EAC1C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,23 @@
1
+ const GLOBAL_KEY = "__openMercatoI18nDictionaryCache__";
2
+ function getCache() {
3
+ const globalScope = globalThis;
4
+ if (!globalScope[GLOBAL_KEY]) {
5
+ globalScope[GLOBAL_KEY] = /* @__PURE__ */ new Map();
6
+ }
7
+ return globalScope[GLOBAL_KEY];
8
+ }
9
+ function getCachedDictionary(locale) {
10
+ return getCache().get(locale);
11
+ }
12
+ function setCachedDictionary(locale, dict) {
13
+ getCache().set(locale, dict);
14
+ }
15
+ function invalidateDictionaryCache() {
16
+ getCache().clear();
17
+ }
18
+ export {
19
+ getCachedDictionary,
20
+ invalidateDictionaryCache,
21
+ setCachedDictionary
22
+ };
23
+ //# sourceMappingURL=dictionary-cache.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/i18n/dictionary-cache.ts"],
4
+ "sourcesContent": ["import type { Locale } from './config'\nimport type { Dict } from './context'\n\n// Process-level cache of flattened, module-merged translation dictionaries\n// keyed by locale. Locale dictionaries are immutable at runtime, so the\n// flatten+merge work in loadDictionary() only needs to run once per locale.\n//\n// Stored on globalThis for the same reason the module registry is: tsx/esbuild\n// can load this file as multiple module instances when mixing dynamic and\n// static imports, and the cache plus its invalidation must stay coherent across\n// those instances. See ../modules/registry.ts for the original workaround.\nconst GLOBAL_KEY = '__openMercatoI18nDictionaryCache__'\n\nfunction getCache(): Map<Locale, Dict> {\n const globalScope = globalThis as any\n if (!globalScope[GLOBAL_KEY]) {\n globalScope[GLOBAL_KEY] = new Map<Locale, Dict>()\n }\n return globalScope[GLOBAL_KEY]\n}\n\nexport function getCachedDictionary(locale: Locale): Dict | undefined {\n return getCache().get(locale)\n}\n\nexport function setCachedDictionary(locale: Locale, dict: Dict): void {\n getCache().set(locale, dict)\n}\n\n// Invalidate every cached locale dictionary. Called whenever the inputs to a\n// dictionary build change: module registration (module translations) and the\n// app dictionary loader registration (base translations).\nexport function invalidateDictionaryCache(): void {\n getCache().clear()\n}\n"],
5
+ "mappings": "AAWA,MAAM,aAAa;AAEnB,SAAS,WAA8B;AACrC,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,UAAU,GAAG;AAC5B,gBAAY,UAAU,IAAI,oBAAI,IAAkB;AAAA,EAClD;AACA,SAAO,YAAY,UAAU;AAC/B;AAEO,SAAS,oBAAoB,QAAkC;AACpE,SAAO,SAAS,EAAE,IAAI,MAAM;AAC9B;AAEO,SAAS,oBAAoB,QAAgB,MAAkB;AACpE,WAAS,EAAE,IAAI,QAAQ,IAAI;AAC7B;AAKO,SAAS,4BAAkC;AAChD,WAAS,EAAE,MAAM;AACnB;",
6
+ "names": []
7
+ }
@@ -3,8 +3,10 @@ import { resolveLocaleFromAcceptLanguage } from "./locale.js";
3
3
  import { createFallbackTranslator, createTranslator } from "./translate.js";
4
4
  import { getModules } from "../modules/registry.js";
5
5
  import { loadAppDictionary } from "./app-dictionaries.js";
6
+ import { getCachedDictionary, setCachedDictionary } from "./dictionary-cache.js";
6
7
  import { registerModules, getModules as getModules2 } from "../modules/registry.js";
7
8
  import { registerAppDictionaryLoader } from "./app-dictionaries.js";
9
+ import { invalidateDictionaryCache } from "./dictionary-cache.js";
8
10
  function flattenDictionary(source, prefix = "") {
9
11
  if (!source || typeof source !== "object" || Array.isArray(source)) return {};
10
12
  const result = {};
@@ -38,6 +40,8 @@ async function detectLocale() {
38
40
  return defaultLocale;
39
41
  }
40
42
  async function loadDictionary(locale) {
43
+ const cached = getCachedDictionary(locale);
44
+ if (cached) return cached;
41
45
  const baseRaw = await loadAppDictionary(locale);
42
46
  const merged = { ...flattenDictionary(baseRaw) };
43
47
  const modules = getModules();
@@ -45,6 +49,7 @@ async function loadDictionary(locale) {
45
49
  const dict = m.translations?.[locale];
46
50
  if (dict) Object.assign(merged, flattenDictionary(dict));
47
51
  }
52
+ setCachedDictionary(locale, merged);
48
53
  return merged;
49
54
  }
50
55
  async function resolveTranslations() {
@@ -61,6 +66,7 @@ try {
61
66
  export {
62
67
  detectLocale,
63
68
  getModules2 as getModules,
69
+ invalidateDictionaryCache,
64
70
  loadDictionary,
65
71
  registerAppDictionaryLoader,
66
72
  registerModules,
@@ -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 { resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport { getModules } from '../modules/registry'\nimport { loadAppDictionary } from './app-dictionaries'\n\n// Re-export for backwards compatibility\nexport { registerModules, getModules } from '../modules/registry'\nexport { registerAppDictionaryLoader } from './app-dictionaries'\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 async function detectLocale(): Promise<Locale> {\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 && locales.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 const match = resolveLocaleFromAcceptLanguage(accept)\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 return defaultLocale\n}\n\nexport async function loadDictionary(locale: Locale): Promise<Dict> {\n // Load from registry instead of @/ import (works in standalone packages)\n const baseRaw = await loadAppDictionary(locale)\n const merged: Dict = { ...flattenDictionary(baseRaw) }\n const modules = getModules()\n for (const m of modules) {\n const dict = m.translations?.[locale]\n if (dict) Object.assign(merged, flattenDictionary(dict))\n }\n return merged\n}\n\nexport async function resolveTranslations() {\n const locale = await detectLocale()\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,uCAAuC;AAChD,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAGlC,SAAS,iBAAiB,cAAAA,mBAAkB;AAC5C,SAAS,mCAAmC;AAE5C,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;AAEA,eAAsB,eAAgC;AAEpD,MAAI;AACF,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,OAAO,cAAc;AACxD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAC3C,UAAI,KAAK,QAAQ,SAAS,CAAW,EAAG,QAAO;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,iBAAiB,KAAK;AAC3D,YAAM,QAAQ,gCAAgC,MAAM;AACpD,UAAI,MAAO,QAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,eAAsB,eAAe,QAA+B;AAElE,QAAM,UAAU,MAAM,kBAAkB,MAAM;AAC9C,QAAM,SAAe,EAAE,GAAG,kBAAkB,OAAO,EAAE;AACrD,QAAM,UAAU,WAAW;AAC3B,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,EAAE,eAAe,MAAM;AACpC,QAAI,KAAM,QAAO,OAAO,QAAQ,kBAAkB,IAAI,CAAC;AAAA,EACzD;AACA,SAAO;AACT;AAEA,eAAsB,sBAAsB;AAC1C,QAAM,SAAS,MAAM,aAAa;AAClC,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;",
4
+ "sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport type { Dict } from './context'\nimport { resolveLocaleFromAcceptLanguage } from './locale'\nimport { createFallbackTranslator, createTranslator } from './translate'\nimport { getModules } from '../modules/registry'\nimport { loadAppDictionary } from './app-dictionaries'\nimport { getCachedDictionary, setCachedDictionary } from './dictionary-cache'\n\n// Re-export for backwards compatibility\nexport { registerModules, getModules } from '../modules/registry'\nexport { registerAppDictionaryLoader } from './app-dictionaries'\nexport { invalidateDictionaryCache } from './dictionary-cache'\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 async function detectLocale(): Promise<Locale> {\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 && locales.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 const match = resolveLocaleFromAcceptLanguage(accept)\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 return 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 // Load from registry instead of @/ import (works in standalone packages)\n const baseRaw = await loadAppDictionary(locale)\n const merged: Dict = { ...flattenDictionary(baseRaw) }\n const modules = getModules()\n for (const m of modules) {\n const dict = m.translations?.[locale]\n if (dict) Object.assign(merged, flattenDictionary(dict))\n }\n setCachedDictionary(locale, merged)\n return merged\n}\n\nexport async function resolveTranslations() {\n const locale = await detectLocale()\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,uCAAuC;AAChD,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB,2BAA2B;AAGzD,SAAS,iBAAiB,cAAAA,mBAAkB;AAC5C,SAAS,mCAAmC;AAC5C,SAAS,iCAAiC;AAE1C,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;AAEA,eAAsB,eAAgC;AAEpD,MAAI;AACF,UAAM,EAAE,SAAS,QAAQ,IAAI,MAAM,OAAO,cAAc;AACxD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,QAAQ,GAAG;AAC3C,UAAI,KAAK,QAAQ,SAAS,CAAW,EAAG,QAAO;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,GAAG,IAAI,iBAAiB,KAAK;AAC3D,YAAM,QAAQ,gCAAgC,MAAM;AACpD,UAAI,MAAO,QAAO;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEA,eAAsB,eAAe,QAA+B;AAIlE,QAAM,SAAS,oBAAoB,MAAM;AACzC,MAAI,OAAQ,QAAO;AAEnB,QAAM,UAAU,MAAM,kBAAkB,MAAM;AAC9C,QAAM,SAAe,EAAE,GAAG,kBAAkB,OAAO,EAAE;AACrD,QAAM,UAAU,WAAW;AAC3B,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,EAAE,eAAe,MAAM;AACpC,QAAI,KAAM,QAAO,OAAO,QAAQ,kBAAkB,IAAI,CAAC;AAAA,EACzD;AACA,sBAAoB,QAAQ,MAAM;AAClC,SAAO;AACT;AAEA,eAAsB,sBAAsB;AAC1C,QAAM,SAAS,MAAM,aAAa;AAClC,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": ["getModules"]
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { applyModuleOverridesToModules } from "@open-mercato/shared/modules/overrides";
2
+ import { invalidateDictionaryCache } from "../i18n/dictionary-cache.js";
2
3
  const GLOBAL_KEY = "__openMercatoModulesRegistry__";
3
4
  function getGlobalModules() {
4
5
  return globalThis[GLOBAL_KEY] ?? null;
@@ -13,6 +14,7 @@ function registerModules(modules) {
13
14
  console.debug("[Bootstrap] Modules re-registered (this may occur during HMR)");
14
15
  }
15
16
  setGlobalModules(applyModuleOverridesToModules(modules));
17
+ invalidateDictionaryCache();
16
18
  }
17
19
  function getModules() {
18
20
  const modules = getGlobalModules();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/modules/registry.ts"],
4
- "sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Modules re-registered (this may occur during HMR)')\n }\n setGlobalModules(applyModuleOverridesToModules(modules))\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n"],
5
- "mappings": "AACA,SAAS,qCAAqC;AAS9C,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,YAAQ,MAAM,+DAA+D;AAAA,EAC/E;AACA,mBAAiB,8BAA8B,OAAO,CAAC;AACzD;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\nimport { invalidateDictionaryCache } from '../i18n/dictionary-cache'\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] Modules re-registered (this may occur during HMR)')\n }\n setGlobalModules(applyModuleOverridesToModules(modules))\n invalidateDictionaryCache()\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n"],
5
+ "mappings": "AACA,SAAS,qCAAqC;AAC9C,SAAS,iCAAiC;AAS1C,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,YAAQ,MAAM,+DAA+D;AAAA,EAC/E;AACA,mBAAiB,8BAA8B,OAAO,CAAC;AACvD,4BAA0B;AAC5B;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.4-develop.4106.1.12c205a613";
1
+ const APP_VERSION = "0.6.4-develop.4113.1.5e87922616";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -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.6.4-develop.4106.1.12c205a613'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.4-develop.4113.1.5e87922616'\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.6.4-develop.4106.1.12c205a613",
3
+ "version": "0.6.4-develop.4113.1.5e87922616",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.1",
93
93
  "@mikro-orm/decorators": "^7.1.1",
94
94
  "@mikro-orm/postgresql": "^7.1.1",
95
- "@open-mercato/cache": "0.6.4-develop.4106.1.12c205a613",
95
+ "@open-mercato/cache": "0.6.4-develop.4113.1.5e87922616",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -0,0 +1,82 @@
1
+ import {
2
+ loadDictionary,
3
+ registerModules,
4
+ registerAppDictionaryLoader,
5
+ invalidateDictionaryCache,
6
+ } from '../server'
7
+ import type { Locale } from '../config'
8
+
9
+ describe('i18n dictionary memoization', () => {
10
+ beforeEach(() => {
11
+ registerModules([] as any)
12
+ registerAppDictionaryLoader(async () => ({}))
13
+ invalidateDictionaryCache()
14
+ })
15
+
16
+ it('builds the flattened dictionary once per locale and reuses it', async () => {
17
+ const loader = jest.fn(async (_locale: Locale) => ({ greeting: 'hello' }))
18
+ registerAppDictionaryLoader(loader)
19
+ registerModules([{ translations: { en: { module: { title: 'Module' } } } }] as any)
20
+
21
+ const first = await loadDictionary('en')
22
+ const second = await loadDictionary('en')
23
+
24
+ expect(first).toBe(second)
25
+ expect(loader).toHaveBeenCalledTimes(1)
26
+ expect(first).toEqual({ greeting: 'hello', 'module.title': 'Module' })
27
+ })
28
+
29
+ it('caches each locale independently', async () => {
30
+ const loader = jest.fn(async (locale: Locale) => ({ greeting: locale }))
31
+ registerAppDictionaryLoader(loader)
32
+ registerModules([] as any)
33
+
34
+ const en = await loadDictionary('en')
35
+ const pl = await loadDictionary('pl')
36
+ await loadDictionary('en')
37
+
38
+ expect(en).not.toBe(pl)
39
+ expect(en).toEqual({ greeting: 'en' })
40
+ expect(pl).toEqual({ greeting: 'pl' })
41
+ expect(loader).toHaveBeenCalledTimes(2)
42
+ })
43
+
44
+ it('rebuilds after modules are re-registered', async () => {
45
+ registerAppDictionaryLoader(async () => ({}))
46
+ registerModules([{ translations: { en: { a: 'one' } } }] as any)
47
+ const before = await loadDictionary('en')
48
+ expect(before).toEqual({ a: 'one' })
49
+
50
+ registerModules([{ translations: { en: { b: 'two' } } }] as any)
51
+ const after = await loadDictionary('en')
52
+
53
+ expect(after).not.toBe(before)
54
+ expect(after).toEqual({ b: 'two' })
55
+ })
56
+
57
+ it('rebuilds after the app dictionary loader is re-registered', async () => {
58
+ registerModules([] as any)
59
+ registerAppDictionaryLoader(async () => ({ greeting: 'old' }))
60
+ const before = await loadDictionary('en')
61
+ expect(before).toEqual({ greeting: 'old' })
62
+
63
+ registerAppDictionaryLoader(async () => ({ greeting: 'new' }))
64
+ const after = await loadDictionary('en')
65
+
66
+ expect(after).not.toBe(before)
67
+ expect(after).toEqual({ greeting: 'new' })
68
+ })
69
+
70
+ it('rebuilds after explicit cache invalidation', async () => {
71
+ const loader = jest.fn(async () => ({ greeting: 'hello' }))
72
+ registerAppDictionaryLoader(loader)
73
+ registerModules([] as any)
74
+
75
+ await loadDictionary('en')
76
+ expect(loader).toHaveBeenCalledTimes(1)
77
+
78
+ invalidateDictionaryCache()
79
+ await loadDictionary('en')
80
+ expect(loader).toHaveBeenCalledTimes(2)
81
+ })
82
+ })
@@ -1,4 +1,5 @@
1
1
  import type { Locale } from './config'
2
+ import { invalidateDictionaryCache } from './dictionary-cache'
2
3
 
3
4
  type DictionaryLoader = (locale: Locale) => Promise<Record<string, unknown>>
4
5
 
@@ -6,6 +7,7 @@ let _appDictionaryLoader: DictionaryLoader | null = null
6
7
 
7
8
  export function registerAppDictionaryLoader(loader: DictionaryLoader): void {
8
9
  _appDictionaryLoader = loader
10
+ invalidateDictionaryCache()
9
11
  }
10
12
 
11
13
  export async function loadAppDictionary(locale: Locale): Promise<Record<string, unknown>> {
@@ -0,0 +1,35 @@
1
+ import type { Locale } from './config'
2
+ import type { Dict } from './context'
3
+
4
+ // Process-level cache of flattened, module-merged translation dictionaries
5
+ // keyed by locale. Locale dictionaries are immutable at runtime, so the
6
+ // flatten+merge work in loadDictionary() only needs to run once per locale.
7
+ //
8
+ // Stored on globalThis for the same reason the module registry is: tsx/esbuild
9
+ // can load this file as multiple module instances when mixing dynamic and
10
+ // static imports, and the cache plus its invalidation must stay coherent across
11
+ // those instances. See ../modules/registry.ts for the original workaround.
12
+ const GLOBAL_KEY = '__openMercatoI18nDictionaryCache__'
13
+
14
+ function getCache(): Map<Locale, Dict> {
15
+ const globalScope = globalThis as any
16
+ if (!globalScope[GLOBAL_KEY]) {
17
+ globalScope[GLOBAL_KEY] = new Map<Locale, Dict>()
18
+ }
19
+ return globalScope[GLOBAL_KEY]
20
+ }
21
+
22
+ export function getCachedDictionary(locale: Locale): Dict | undefined {
23
+ return getCache().get(locale)
24
+ }
25
+
26
+ export function setCachedDictionary(locale: Locale, dict: Dict): void {
27
+ getCache().set(locale, dict)
28
+ }
29
+
30
+ // Invalidate every cached locale dictionary. Called whenever the inputs to a
31
+ // dictionary build change: module registration (module translations) and the
32
+ // app dictionary loader registration (base translations).
33
+ export function invalidateDictionaryCache(): void {
34
+ getCache().clear()
35
+ }
@@ -4,10 +4,12 @@ import { resolveLocaleFromAcceptLanguage } from './locale'
4
4
  import { createFallbackTranslator, createTranslator } from './translate'
5
5
  import { getModules } from '../modules/registry'
6
6
  import { loadAppDictionary } from './app-dictionaries'
7
+ import { getCachedDictionary, setCachedDictionary } from './dictionary-cache'
7
8
 
8
9
  // Re-export for backwards compatibility
9
10
  export { registerModules, getModules } from '../modules/registry'
10
11
  export { registerAppDictionaryLoader } from './app-dictionaries'
12
+ export { invalidateDictionaryCache } from './dictionary-cache'
11
13
 
12
14
  function flattenDictionary(source: unknown, prefix = ''): Dict {
13
15
  if (!source || typeof source !== 'object' || Array.isArray(source)) return {}
@@ -48,6 +50,11 @@ export async function detectLocale(): Promise<Locale> {
48
50
  }
49
51
 
50
52
  export async function loadDictionary(locale: Locale): Promise<Dict> {
53
+ // Locale dictionaries are immutable at runtime, so the flatten+merge below
54
+ // only needs to run once per locale. The cache is invalidated whenever
55
+ // modules or the app dictionary loader are (re)registered.
56
+ const cached = getCachedDictionary(locale)
57
+ if (cached) return cached
51
58
  // Load from registry instead of @/ import (works in standalone packages)
52
59
  const baseRaw = await loadAppDictionary(locale)
53
60
  const merged: Dict = { ...flattenDictionary(baseRaw) }
@@ -56,6 +63,7 @@ export async function loadDictionary(locale: Locale): Promise<Dict> {
56
63
  const dict = m.translations?.[locale]
57
64
  if (dict) Object.assign(merged, flattenDictionary(dict))
58
65
  }
66
+ setCachedDictionary(locale, merged)
59
67
  return merged
60
68
  }
61
69
 
@@ -1,5 +1,6 @@
1
1
  import type { Module } from '@open-mercato/shared/modules/registry'
2
2
  import { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'
3
+ import { invalidateDictionaryCache } from '../i18n/dictionary-cache'
3
4
 
4
5
  // Registration pattern for publishable packages.
5
6
  // Use globalThis to survive tsx/esbuild module duplication where the same
@@ -24,6 +25,7 @@ export function registerModules(modules: Module[]) {
24
25
  console.debug('[Bootstrap] Modules re-registered (this may occur during HMR)')
25
26
  }
26
27
  setGlobalModules(applyModuleOverridesToModules(modules))
28
+ invalidateDictionaryCache()
27
29
  }
28
30
 
29
31
  export function getModules(): Module[] {