@open-mercato/shared 0.7.1-develop.7180.1.9717fbbb43 → 0.7.1-develop.7182.1.789943f937
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/context.js +14 -3
- package/dist/lib/i18n/context.js.map +2 -2
- package/dist/lib/i18n/locale-label.js +34 -0
- package/dist/lib/i18n/locale-label.js.map +7 -0
- package/dist/lib/i18n/locale-registry.js +73 -0
- package/dist/lib/i18n/locale-registry.js.map +7 -0
- package/dist/lib/i18n/locale-set.js +54 -0
- package/dist/lib/i18n/locale-set.js.map +7 -0
- package/dist/lib/i18n/locale.js +8 -8
- package/dist/lib/i18n/locale.js.map +2 -2
- package/dist/lib/i18n/server.js +22 -7
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/testing/renderWithProviders.js +2 -2
- package/dist/lib/testing/renderWithProviders.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/i18n/__tests__/__fixtures__/locale-augmentation-missing-key.ts +18 -0
- package/src/lib/i18n/__tests__/__fixtures__/locale-augmentation-ok.ts +28 -0
- package/src/lib/i18n/__tests__/__fixtures__/locale-unaugmented.ts +5 -0
- package/src/lib/i18n/__tests__/context-supported-locales.test.tsx +134 -0
- package/src/lib/i18n/__tests__/detect-locale-narrowed.test.ts +109 -0
- package/src/lib/i18n/__tests__/dictionary-locale-fallback.test.ts +110 -0
- package/src/lib/i18n/__tests__/locale-augmentation.test.ts +56 -0
- package/src/lib/i18n/__tests__/locale-label.test.ts +141 -0
- package/src/lib/i18n/__tests__/locale-registry.test.ts +267 -0
- package/src/lib/i18n/config.ts +38 -1
- package/src/lib/i18n/config.typecheck.tsx +60 -0
- package/src/lib/i18n/context.tsx +30 -3
- package/src/lib/i18n/locale-label.ts +80 -0
- package/src/lib/i18n/locale-registry.ts +142 -0
- package/src/lib/i18n/locale-set.ts +98 -0
- package/src/lib/i18n/locale.ts +22 -6
- package/src/lib/i18n/server.ts +51 -7
- package/src/lib/testing/renderWithProviders.tsx +4 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 284 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/config.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * The set of languages the platform ships dictionaries for.\n *\n * `Locale` is derived from `LocaleRegistry` rather than written as a closed\n * union so that a downstream application can serve a language the platform does\n * not ship, without patching or forking `@open-mercato/shared`. Augment the\n * interface from the app and the new code becomes a valid `Locale` everywhere:\n *\n * ```ts\n * declare module '@open-mercato/shared/lib/i18n/config' {\n * interface LocaleRegistry { cs: true }\n * }\n * ```\n *\n * Unaugmented, `Locale` resolves to exactly `'en' | 'pl' | 'es' | 'de' | 'ko'`,\n * so existing exhaustive `Record<Locale, T>` maps keep their drift-guard value \u2014\n * and an app that opts in keeps exhaustiveness over its own extended set. This\n * is the same `keyof SomeRegistry` + declaration-merging idiom TypeScript uses\n * on itself (`NumberFormatOptionsStyleRegistry` in `lib.es5.d.ts`, additively\n * merged with `unit` in `lib.es2020.intl.d.ts`).\n *\n * The type layer is advisory only: declaration merging applies when a package is\n * *installed*, not when it is *enabled*, so it can claim a locale the running app\n * never registered. `getSupportedLocales()` in `./locale-registry` is the single\n * runtime authority, and every entry point validates against it.\n */\nexport interface LocaleRegistry {\n en: true\n pl: true\n es: true\n de: true\n ko: true\n}\n\nexport type Locale = keyof LocaleRegistry & string\n\n// NOTE: `scripts/dev.mjs` reads the next two declarations by regex (it parses\n// this file as text to build the dev splash screen before the app compiles).\n// Keep them as literal `export const <name>: <Type> = <literal>` statements.\nexport const locales: Locale[] = ['en', 'pl', 'es', 'de', 'ko']\nexport const defaultLocale: Locale = 'en'\n"],
|
|
5
|
+
"mappings": "AAuCO,MAAM,UAAoB,CAAC,MAAM,MAAM,MAAM,MAAM,IAAI;AACvD,MAAM,gBAAwB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/i18n/context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useMemo } from "react";
|
|
4
|
+
import { getSupportedLocales } from "./locale-set.js";
|
|
4
5
|
const I18N_CONTEXT_KEY = "__openMercatoI18nContext";
|
|
5
6
|
function getI18nContext() {
|
|
6
7
|
const store = globalThis;
|
|
@@ -22,10 +23,15 @@ function format(template, params) {
|
|
|
22
23
|
return String(value);
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
|
-
function I18nProvider({ children, locale, dict, localeLocked
|
|
26
|
+
function I18nProvider({ children, locale, dict, localeLocked, supportedLocales }) {
|
|
27
|
+
const outer = useContext(I18nContext);
|
|
26
28
|
const value = useMemo(() => ({
|
|
27
29
|
locale,
|
|
28
|
-
localeLocked,
|
|
30
|
+
localeLocked: localeLocked ?? outer?.localeLocked ?? false,
|
|
31
|
+
// Falls back to the process-local registry so a provider mounted without the
|
|
32
|
+
// prop and without an enclosing one (tests, standalone renders) behaves
|
|
33
|
+
// exactly as it did before.
|
|
34
|
+
supportedLocales: supportedLocales ?? outer?.supportedLocales ?? getSupportedLocales(),
|
|
29
35
|
t: (key, fallbackOrParams, params) => {
|
|
30
36
|
let fallback;
|
|
31
37
|
let resolvedParams;
|
|
@@ -38,7 +44,7 @@ function I18nProvider({ children, locale, dict, localeLocked = false }) {
|
|
|
38
44
|
const template = dict[key] ?? fallback ?? key;
|
|
39
45
|
return format(template, resolvedParams);
|
|
40
46
|
}
|
|
41
|
-
}), [locale, dict, localeLocked]);
|
|
47
|
+
}), [locale, dict, localeLocked, supportedLocales, outer]);
|
|
42
48
|
return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
|
|
43
49
|
}
|
|
44
50
|
function useT() {
|
|
@@ -59,6 +65,10 @@ function useOptionalLocale() {
|
|
|
59
65
|
const ctx = useContext(I18nContext);
|
|
60
66
|
return ctx?.locale;
|
|
61
67
|
}
|
|
68
|
+
function useSupportedLocales() {
|
|
69
|
+
const ctx = useContext(I18nContext);
|
|
70
|
+
return ctx?.supportedLocales ?? getSupportedLocales();
|
|
71
|
+
}
|
|
62
72
|
function useLocaleLocked() {
|
|
63
73
|
const ctx = useContext(I18nContext);
|
|
64
74
|
return ctx?.localeLocked ?? false;
|
|
@@ -69,6 +79,7 @@ export {
|
|
|
69
79
|
useLocaleLocked,
|
|
70
80
|
useOptionalLocale,
|
|
71
81
|
useOptionalT,
|
|
82
|
+
useSupportedLocales,
|
|
72
83
|
useT
|
|
73
84
|
};
|
|
74
85
|
//# sourceMappingURL=context.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/context.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type PropsWithChildren } from 'react'\nimport type { Locale } from './config'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type PropsWithChildren } from 'react'\nimport type { Locale } from './config'\nimport { getSupportedLocales } from './locale-set'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n /**\n * Every locale this app serves. Resolved on the server (where the app\n * registry and any tenant configuration are readable) and handed to the\n * client, because a client bundle cannot see either.\n */\n supportedLocales: readonly Locale[]\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked, supportedLocales }: PropsWithChildren<{ locale: Locale; dict: Dict; localeLocked?: boolean; supportedLocales?: readonly Locale[] }>) {\n // A nested provider (the backend layout mounts one inside the root layout's)\n // shadows the whole subtree, so a prop it does not pass would otherwise be\n // silently downgraded to the registry default for every consumer below it.\n // Inheriting from the enclosing provider first makes an omitted prop mean\n // \"unchanged\" rather than \"reset\", which is what a nested mount intends.\n const outer = useContext(I18nContext)\n const value = useMemo<I18nContextValue>(() => ({\n locale,\n localeLocked: localeLocked ?? outer?.localeLocked ?? false,\n // Falls back to the process-local registry so a provider mounted without the\n // prop and without an enclosing one (tests, standalone renders) behaves\n // exactly as it did before.\n supportedLocales: supportedLocales ?? outer?.supportedLocales ?? getSupportedLocales(),\n t: (key, fallbackOrParams, params) => {\n let fallback: string | undefined\n let resolvedParams: TranslateParams | undefined\n\n if (typeof fallbackOrParams === 'string') {\n fallback = fallbackOrParams\n resolvedParams = params\n } else {\n resolvedParams = fallbackOrParams ?? params\n }\n\n const template = dict[key] ?? fallback ?? key\n return format(template, resolvedParams)\n },\n }), [locale, dict, localeLocked, supportedLocales, outer])\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>\n}\n\nexport function useT() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useT must be used within I18nProvider')\n return ctx.t\n}\n\n/**\n * Like `useT`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use where a translator is desirable but not\n * guaranteed (e.g. plumbing `t` into side-effect handlers that may run before\n * the provider mounts) \u2014 callers MUST provide a fallback.\n */\nexport function useOptionalT(): TranslateFn | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.t\n}\n\nexport function useLocale() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useLocale must be used within I18nProvider')\n return ctx.locale\n}\n\n/**\n * Like `useLocale`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use in shared components that receive their\n * translator as a prop and may render outside a provider (galleries, tests).\n */\nexport function useOptionalLocale(): Locale | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.locale\n}\n\n/**\n * Every locale this app serves, for rendering a language picker. Falls back to\n * the process-local registry outside a provider so callers can render\n * unconditionally.\n */\nexport function useSupportedLocales(): readonly Locale[] {\n const ctx = useContext(I18nContext)\n return ctx?.supportedLocales ?? getSupportedLocales()\n}\n\n/**\n * True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`\n * when no provider is in scope so callers can render unconditionally.\n */\nexport function useLocaleLocked() {\n const ctx = useContext(I18nContext)\n return ctx?.localeLocked ?? false\n}\n"],
|
|
5
|
+
"mappings": ";AAsFS;AArFT,SAAS,eAAe,YAAY,eAAuC;AAE3E,SAAS,2BAA2B;AAyBpC,MAAM,mBAAmB;AAMzB,SAAS,iBAAiB;AACxB,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,gBAAgB,GAAG;AAC5B,UAAM,gBAAgB,IAAI,cAAuC,IAAI;AAAA,EACvE;AACA,SAAO,MAAM,gBAAgB;AAC/B;AAEA,MAAM,cAAc,eAAe;AAEnC,SAAS,OAAO,UAAkB,QAA0B;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,4BAA4B,CAAC,GAAG,WAAW,cAAc;AAC/E,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,QAAW;AACvB,aAAO,YAAY,KAAK,GAAG,OAAO,IAAI,GAAG;AAAA,IAC3C;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAEO,SAAS,aAAa,EAAE,UAAU,QAAQ,MAAM,cAAc,iBAAiB,GAAoH;AAMxM,QAAM,QAAQ,WAAW,WAAW;AACpC,QAAM,QAAQ,QAA0B,OAAO;AAAA,IAC7C;AAAA,IACA,cAAc,gBAAgB,OAAO,gBAAgB;AAAA;AAAA;AAAA;AAAA,IAIrD,kBAAkB,oBAAoB,OAAO,oBAAoB,oBAAoB;AAAA,IACrF,GAAG,CAAC,KAAK,kBAAkB,WAAW;AACpC,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,mBAAW;AACX,yBAAiB;AAAA,MACnB,OAAO;AACL,yBAAiB,oBAAoB;AAAA,MACvC;AAEA,YAAM,WAAW,KAAK,GAAG,KAAK,YAAY;AAC1C,aAAO,OAAO,UAAU,cAAc;AAAA,IACxC;AAAA,EACF,IAAI,CAAC,QAAQ,MAAM,cAAc,kBAAkB,KAAK,CAAC;AACzD,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,OAAO;AACrB,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO,IAAI;AACb;AAQO,SAAS,eAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAEO,SAAS,YAAY;AAC1B,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO,IAAI;AACb;AAOO,SAAS,oBAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAOO,SAAS,sBAAyC;AACvD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,oBAAoB,oBAAoB;AACtD;AAMO,SAAS,kBAAkB;AAChC,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,gBAAgB;AAC9B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const SHIPPED_LOCALE_LABELS = {
|
|
2
|
+
en: { key: "common.languages.english", native: "English" },
|
|
3
|
+
pl: { key: "common.languages.polish", native: "Polski" },
|
|
4
|
+
es: { key: "common.languages.spanish", native: "Espa\xF1ol" },
|
|
5
|
+
de: { key: "common.languages.german", native: "Deutsch" },
|
|
6
|
+
ko: { key: "common.languages.korean", native: "\uD55C\uAD6D\uC5B4" }
|
|
7
|
+
};
|
|
8
|
+
const intlDisplayNames = /* @__PURE__ */ new Map();
|
|
9
|
+
function resolveIntlDisplayName(locale) {
|
|
10
|
+
if (intlDisplayNames.has(locale)) return intlDisplayNames.get(locale);
|
|
11
|
+
const resolved = computeIntlDisplayName(locale);
|
|
12
|
+
intlDisplayNames.set(locale, resolved);
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
function computeIntlDisplayName(locale) {
|
|
16
|
+
try {
|
|
17
|
+
const displayName = new Intl.DisplayNames([locale], { type: "language" }).of(locale);
|
|
18
|
+
if (!displayName || displayName.toLowerCase() === locale.toLowerCase()) return void 0;
|
|
19
|
+
return displayName;
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolveLocaleLabel(locale, t) {
|
|
25
|
+
const shipped = SHIPPED_LOCALE_LABELS[locale];
|
|
26
|
+
if (shipped) {
|
|
27
|
+
return t ? t(shipped.key, shipped.native) : shipped.native;
|
|
28
|
+
}
|
|
29
|
+
return resolveIntlDisplayName(locale) ?? locale.toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
resolveLocaleLabel
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=locale-label.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/i18n/locale-label.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TranslateFn } from './context'\n\ntype ShippedLocaleLabel = {\n /** Dictionary key, so the label itself is localized when a translator is given. */\n key: string\n /** Endonym \u2014 the language's name in its own language. */\n native: string\n}\n\n// The locales the platform ships. Kept as literal data rather than derived from\n// `Intl.DisplayNames` because `Intl` disagrees on casing for some of them\n// (`espa\u00F1ol`, `polski`), and these strings are already user-visible.\nconst SHIPPED_LOCALE_LABELS: Record<string, ShippedLocaleLabel> = {\n en: { key: 'common.languages.english', native: 'English' },\n pl: { key: 'common.languages.polish', native: 'Polski' },\n es: { key: 'common.languages.spanish', native: 'Espa\u00F1ol' },\n de: { key: 'common.languages.german', native: 'Deutsch' },\n ko: { key: 'common.languages.korean', native: '\uD55C\uAD6D\uC5B4' },\n}\n\n// `resolveLocaleLabel` is called once per option per render of every language\n// switcher, and constructing an `Intl.DisplayNames` is not free. The answer for\n// a given code never changes within a process.\nconst intlDisplayNames = new Map<string, string | undefined>()\n\nfunction resolveIntlDisplayName(locale: string): string | undefined {\n if (intlDisplayNames.has(locale)) return intlDisplayNames.get(locale)\n const resolved = computeIntlDisplayName(locale)\n intlDisplayNames.set(locale, resolved)\n return resolved\n}\n\nfunction computeIntlDisplayName(locale: string): string | undefined {\n try {\n // Ask for the language's name in its own language, so a switcher reads the\n // way a speaker of that language expects it to.\n const displayName = new Intl.DisplayNames([locale], { type: 'language' }).of(locale)\n // `Intl` echoes the input back when it has no data for the code.\n if (!displayName || displayName.toLowerCase() === locale.toLowerCase()) return undefined\n return displayName\n } catch {\n // Invalid or unsupported code \u2014 fall through to the uppercased code.\n return undefined\n }\n}\n\n/**\n * A human-readable name for any locale code, including ones the platform does\n * not ship dictionaries for.\n *\n * Resolution order:\n * 1. the shipped table \u2014 via `t` when given, so the label is itself localized\n * (a German UI shows \"Polnisch\"); otherwise the endonym (\"Polski\")\n * 2. `Intl.DisplayNames` \u2014 the endonym for an arbitrary code, no dependency\n * 3. the uppercased code \u2014 never blank\n *\n * Deliberately does **not** consult `./iso639`. Every caller of this function is\n * a client component \u2014 the admin `ProfileDropdown`, the storefront\n * `LanguageSwitcher`, the public checkout pay page \u2014 and `iso639.ts` is a\n * 186-entry table with a module-scope `Set` no bundler can tree-shake, so\n * importing it here would ship 7 KB of language catalogue to a conversion-\n * critical public route in order to render five labels that rung 1 already\n * answered. `Intl.DisplayNames` names essentially any code an app would plausibly\n * register, in every browser this app supports, so the catalogue was only ever a\n * fallback for a fallback. A code `Intl` cannot name degrades to its uppercased\n * form, which is never blank. Server and admin callers that genuinely need the\n * catalogue keep importing `getIso639Label` from `./iso639` directly.\n *\n * Pass `t` where the surrounding UI renders localized language names, and omit\n * it where it renders endonyms. Both conventions exist in the codebase and the\n * caller decides which one it wants.\n */\nexport function resolveLocaleLabel(locale: string, t?: TranslateFn): string {\n const shipped = SHIPPED_LOCALE_LABELS[locale]\n if (shipped) {\n return t ? t(shipped.key, shipped.native) : shipped.native\n }\n\n return resolveIntlDisplayName(locale) ?? locale.toUpperCase()\n}\n"],
|
|
5
|
+
"mappings": "AAYA,MAAM,wBAA4D;AAAA,EAChE,IAAI,EAAE,KAAK,4BAA4B,QAAQ,UAAU;AAAA,EACzD,IAAI,EAAE,KAAK,2BAA2B,QAAQ,SAAS;AAAA,EACvD,IAAI,EAAE,KAAK,4BAA4B,QAAQ,aAAU;AAAA,EACzD,IAAI,EAAE,KAAK,2BAA2B,QAAQ,UAAU;AAAA,EACxD,IAAI,EAAE,KAAK,2BAA2B,QAAQ,qBAAM;AACtD;AAKA,MAAM,mBAAmB,oBAAI,IAAgC;AAE7D,SAAS,uBAAuB,QAAoC;AAClE,MAAI,iBAAiB,IAAI,MAAM,EAAG,QAAO,iBAAiB,IAAI,MAAM;AACpE,QAAM,WAAW,uBAAuB,MAAM;AAC9C,mBAAiB,IAAI,QAAQ,QAAQ;AACrC,SAAO;AACT;AAEA,SAAS,uBAAuB,QAAoC;AAClE,MAAI;AAGF,UAAM,cAAc,IAAI,KAAK,aAAa,CAAC,MAAM,GAAG,EAAE,MAAM,WAAW,CAAC,EAAE,GAAG,MAAM;AAEnF,QAAI,CAAC,eAAe,YAAY,YAAY,MAAM,OAAO,YAAY,EAAG,QAAO;AAC/E,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AA4BO,SAAS,mBAAmB,QAAgB,GAAyB;AAC1E,QAAM,UAAU,sBAAsB,MAAM;AAC5C,MAAI,SAAS;AACX,WAAO,IAAI,EAAE,QAAQ,KAAK,QAAQ,MAAM,IAAI,QAAQ;AAAA,EACtD;AAEA,SAAO,uBAAuB,MAAM,KAAK,OAAO,YAAY;AAC9D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defaultLocale, locales } from "./config.js";
|
|
2
|
+
import { invalidateDictionaryCache } from "./dictionary-cache.js";
|
|
3
|
+
import { isValidIso639 } from "./iso639.js";
|
|
4
|
+
import { createLogger } from "../logger/index.js";
|
|
5
|
+
import {
|
|
6
|
+
addRegisteredLocale,
|
|
7
|
+
clearRegisteredLocaleSet,
|
|
8
|
+
getSupportedLocales,
|
|
9
|
+
normalizeLocaleCode
|
|
10
|
+
} from "./locale-set.js";
|
|
11
|
+
let cachedLogger = null;
|
|
12
|
+
function logger() {
|
|
13
|
+
if (!cachedLogger) cachedLogger = createLogger("shared").child({ component: "i18n-locale-registry" });
|
|
14
|
+
return cachedLogger;
|
|
15
|
+
}
|
|
16
|
+
import {
|
|
17
|
+
getSupportedLocales as getSupportedLocales2,
|
|
18
|
+
isSupportedLocale,
|
|
19
|
+
getRegisteredLocales,
|
|
20
|
+
normalizeLocaleCode as normalizeLocaleCode2
|
|
21
|
+
} from "./locale-set.js";
|
|
22
|
+
function registerLocales(codes) {
|
|
23
|
+
let added = false;
|
|
24
|
+
for (const code of codes) {
|
|
25
|
+
const normalized = normalizeLocaleCode(code);
|
|
26
|
+
if (!normalized) continue;
|
|
27
|
+
if (locales.includes(normalized)) continue;
|
|
28
|
+
if (!isValidIso639(normalized.split("-")[0] ?? normalized)) {
|
|
29
|
+
logger().warn("Ignoring unknown locale code", { code });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (addRegisteredLocale(normalized)) added = true;
|
|
33
|
+
}
|
|
34
|
+
if (added) invalidateDictionaryCache();
|
|
35
|
+
}
|
|
36
|
+
function clearRegisteredLocales() {
|
|
37
|
+
if (clearRegisteredLocaleSet()) invalidateDictionaryCache();
|
|
38
|
+
}
|
|
39
|
+
const RESOLVER_GLOBAL_KEY = "__openMercatoI18nSupportedLocalesResolver__";
|
|
40
|
+
function registerSupportedLocalesResolver(resolver) {
|
|
41
|
+
const scope = globalThis;
|
|
42
|
+
if (resolver && scope[RESOLVER_GLOBAL_KEY]) {
|
|
43
|
+
logger().warn("Replacing an already-registered supported-locales resolver");
|
|
44
|
+
}
|
|
45
|
+
scope[RESOLVER_GLOBAL_KEY] = resolver;
|
|
46
|
+
}
|
|
47
|
+
async function resolveSupportedLocalesForRequest() {
|
|
48
|
+
const available = getSupportedLocales();
|
|
49
|
+
const resolver = globalThis[RESOLVER_GLOBAL_KEY];
|
|
50
|
+
if (!resolver) return available;
|
|
51
|
+
try {
|
|
52
|
+
const configured = await resolver();
|
|
53
|
+
if (!configured || configured.length === 0) return available;
|
|
54
|
+
const selected = new Set(configured.map(normalizeLocaleCode));
|
|
55
|
+
if (!available.some((locale) => selected.has(locale))) return available;
|
|
56
|
+
selected.add(defaultLocale);
|
|
57
|
+
return available.filter((locale) => selected.has(locale));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
logger().warn("Failed to resolve tenant supported locales; serving the full set", { err });
|
|
60
|
+
return available;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
clearRegisteredLocales,
|
|
65
|
+
getRegisteredLocales,
|
|
66
|
+
getSupportedLocales2 as getSupportedLocales,
|
|
67
|
+
isSupportedLocale,
|
|
68
|
+
normalizeLocaleCode2 as normalizeLocaleCode,
|
|
69
|
+
registerLocales,
|
|
70
|
+
registerSupportedLocalesResolver,
|
|
71
|
+
resolveSupportedLocalesForRequest
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=locale-registry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/i18n/locale-registry.ts"],
|
|
4
|
+
"sourcesContent": ["import { defaultLocale, locales, type Locale } from './config'\nimport { invalidateDictionaryCache } from './dictionary-cache'\nimport { isValidIso639 } from './iso639'\nimport { createLogger } from '../logger'\nimport {\n addRegisteredLocale,\n clearRegisteredLocaleSet,\n getSupportedLocales,\n normalizeLocaleCode,\n} from './locale-set'\n\n// Constructed lazily rather than at module scope: a module-level factory call is\n// a side effect no bundler can drop, which would pin this whole module \u2014 and the\n// logger facade behind it \u2014 into any bundle that merely imports one of its\n// tree-shakeable exports.\nlet cachedLogger: ReturnType<typeof createLogger> | null = null\nfunction logger() {\n if (!cachedLogger) cachedLogger = createLogger('shared').child({ component: 'i18n-locale-registry' })\n return cachedLogger\n}\n\n// The read side lives in `./locale-set`, which has no dependencies beyond\n// `./config` so client bundles can import it without pulling in the logger, the\n// ISO 639 table or the dictionary cache. Re-exported here so `locale-registry`\n// remains the one import path callers need to know about.\nexport {\n getSupportedLocales,\n isSupportedLocale,\n getRegisteredLocales,\n normalizeLocaleCode,\n} from './locale-set'\n\n/**\n * Register additional locales this application serves on top of the ones the\n * platform ships in `locales`.\n *\n * Runtime half of the extension point; the compile-time half is augmenting\n * `LocaleRegistry` in `./config`. Both are needed for an app-defined locale to\n * be usable, and this one is the authority \u2014 the type layer cannot be trusted to\n * reflect what the running app actually registered.\n *\n * Codes are normalized (`pt_BR` \u2192 `pt-br`) and validated against ISO 639-1;\n * unknown codes are ignored with a warning rather than thrown, so one bad entry\n * in app config cannot take the app down at boot. Registering is idempotent, and\n * re-registering the shipped locales is a no-op.\n */\nexport function registerLocales(codes: readonly string[]): void {\n let added = false\n\n for (const code of codes) {\n const normalized = normalizeLocaleCode(code)\n if (!normalized) continue\n if ((locales as readonly string[]).includes(normalized)) continue\n // Region subtags (`pt-br`) are normalized but validated on their base code,\n // matching how `resolveSupportedLocale` folds a region down to its language.\n if (!isValidIso639(normalized.split('-')[0] ?? normalized)) {\n logger().warn('Ignoring unknown locale code', { code })\n continue\n }\n if (addRegisteredLocale(normalized)) added = true\n }\n\n // The dictionary a locale resolves to is derived from the supported set, so a\n // widened set invalidates everything built from the narrower one.\n if (added) invalidateDictionaryCache()\n}\n\n/** Drop every app-registered locale. Intended for tests. */\nexport function clearRegisteredLocales(): void {\n if (clearRegisteredLocaleSet()) invalidateDictionaryCache()\n}\n\n/**\n * Resolves the locale codes the current tenant has opted into, or `null` when\n * there is no tenant context or no stored selection.\n */\nexport type SupportedLocalesResolver = () => Promise<readonly string[] | null>\n\nconst RESOLVER_GLOBAL_KEY = '__openMercatoI18nSupportedLocalesResolver__'\n\ntype ResolverGlobalScope = typeof globalThis & {\n [RESOLVER_GLOBAL_KEY]?: SupportedLocalesResolver | null\n}\n\n/**\n * Register the source of per-tenant locale configuration.\n *\n * `@open-mercato/shared` cannot read tenant configuration itself \u2014 that needs a\n * DI container and a domain module \u2014 so the owning module registers a resolver\n * here, the same way `registerTranslationOverlayPlugin` inverts the dependency\n * for content translations. With nothing registered the served set is exactly\n * the process-local registry, which is today's behaviour.\n *\n * There is one slot: a second registration replaces the first. That is what\n * makes an enterprise overlay able to take over the tenant lookup, so it is not\n * an error, but it is warned about \u2014 silently losing the `translations` module's\n * resolver to an accidental second call is otherwise undiagnosable.\n */\nexport function registerSupportedLocalesResolver(resolver: SupportedLocalesResolver | null): void {\n const scope = globalThis as ResolverGlobalScope\n if (resolver && scope[RESOLVER_GLOBAL_KEY]) {\n logger().warn('Replacing an already-registered supported-locales resolver')\n }\n scope[RESOLVER_GLOBAL_KEY] = resolver\n}\n\n/**\n * The locales to offer for the current request: the tenant's selection narrowed\n * to those the app can actually serve.\n *\n * Intersecting rather than replacing means a code that was configured but has no\n * dictionary source behind it can never reach a language switcher, so a typo in\n * the settings screen cannot strand a tenant in a broken UI. An empty\n * intersection falls back to the full set for the same reason. Never throws \u2014\n * this runs in the root layout, where a failure would take down every page.\n */\nexport async function resolveSupportedLocalesForRequest(): Promise<readonly Locale[]> {\n const available = getSupportedLocales()\n const resolver = (globalThis as ResolverGlobalScope)[RESOLVER_GLOBAL_KEY]\n if (!resolver) return available\n\n try {\n const configured = await resolver()\n if (!configured || configured.length === 0) return available\n\n const selected = new Set(configured.map(normalizeLocaleCode))\n if (!available.some((locale) => selected.has(locale))) return available\n\n // `detectLocale` falls back to `defaultLocale` whenever neither the cookie\n // nor Accept-Language matches, so the default has to stay servable. Without\n // this, a tenant selecting only `['pl','de']` renders an English page whose\n // own switcher does not list English: a blank Select trigger, no checked row\n // in the profile menu, and no way for the user to get back.\n selected.add(defaultLocale)\n // Filtering `available` rather than mapping the selection preserves the\n // platform's locale ordering instead of the tenant's.\n return available.filter((locale) => selected.has(locale))\n } catch (err) {\n logger().warn('Failed to resolve tenant supported locales; serving the full set', { err })\n return available\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe,eAA4B;AACpD,SAAS,iCAAiC;AAC1C,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAMP,IAAI,eAAuD;AAC3D,SAAS,SAAS;AAChB,MAAI,CAAC,aAAc,gBAAe,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,uBAAuB,CAAC;AACpG,SAAO;AACT;AAMA;AAAA,EACE,uBAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA,uBAAAC;AAAA,OACK;AAgBA,SAAS,gBAAgB,OAAgC;AAC9D,MAAI,QAAQ;AAEZ,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,oBAAoB,IAAI;AAC3C,QAAI,CAAC,WAAY;AACjB,QAAK,QAA8B,SAAS,UAAU,EAAG;AAGzD,QAAI,CAAC,cAAc,WAAW,MAAM,GAAG,EAAE,CAAC,KAAK,UAAU,GAAG;AAC1D,aAAO,EAAE,KAAK,gCAAgC,EAAE,KAAK,CAAC;AACtD;AAAA,IACF;AACA,QAAI,oBAAoB,UAAU,EAAG,SAAQ;AAAA,EAC/C;AAIA,MAAI,MAAO,2BAA0B;AACvC;AAGO,SAAS,yBAA+B;AAC7C,MAAI,yBAAyB,EAAG,2BAA0B;AAC5D;AAQA,MAAM,sBAAsB;AAoBrB,SAAS,iCAAiC,UAAiD;AAChG,QAAM,QAAQ;AACd,MAAI,YAAY,MAAM,mBAAmB,GAAG;AAC1C,WAAO,EAAE,KAAK,4DAA4D;AAAA,EAC5E;AACA,QAAM,mBAAmB,IAAI;AAC/B;AAYA,eAAsB,oCAAgE;AACpF,QAAM,YAAY,oBAAoB;AACtC,QAAM,WAAY,WAAmC,mBAAmB;AACxE,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI;AACF,UAAM,aAAa,MAAM,SAAS;AAClC,QAAI,CAAC,cAAc,WAAW,WAAW,EAAG,QAAO;AAEnD,UAAM,WAAW,IAAI,IAAI,WAAW,IAAI,mBAAmB,CAAC;AAC5D,QAAI,CAAC,UAAU,KAAK,CAAC,WAAW,SAAS,IAAI,MAAM,CAAC,EAAG,QAAO;AAO9D,aAAS,IAAI,aAAa;AAG1B,WAAO,UAAU,OAAO,CAAC,WAAW,SAAS,IAAI,MAAM,CAAC;AAAA,EAC1D,SAAS,KAAK;AACZ,WAAO,EAAE,KAAK,oEAAoE,EAAE,IAAI,CAAC;AACzF,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": ["getSupportedLocales", "normalizeLocaleCode"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { locales } from "./config.js";
|
|
2
|
+
const GLOBAL_KEY = "__openMercatoI18nLocaleRegistry__";
|
|
3
|
+
const CACHE_KEY = "__openMercatoI18nSupportedLocales__";
|
|
4
|
+
function globalScope() {
|
|
5
|
+
return globalThis;
|
|
6
|
+
}
|
|
7
|
+
function getRegistered() {
|
|
8
|
+
const scope = globalScope();
|
|
9
|
+
if (!scope[GLOBAL_KEY]) {
|
|
10
|
+
scope[GLOBAL_KEY] = /* @__PURE__ */ new Set();
|
|
11
|
+
}
|
|
12
|
+
return scope[GLOBAL_KEY];
|
|
13
|
+
}
|
|
14
|
+
function normalizeLocaleCode(value) {
|
|
15
|
+
return value.trim().toLowerCase().replace(/_/g, "-");
|
|
16
|
+
}
|
|
17
|
+
function getSupportedLocales() {
|
|
18
|
+
const scope = globalScope();
|
|
19
|
+
const cached = scope[CACHE_KEY];
|
|
20
|
+
if (cached) return cached;
|
|
21
|
+
const registered = getRegistered();
|
|
22
|
+
const resolved = registered.size === 0 ? locales : [...locales, ...registered];
|
|
23
|
+
scope[CACHE_KEY] = resolved;
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
function isSupportedLocale(code) {
|
|
27
|
+
return getSupportedLocales().includes(normalizeLocaleCode(code));
|
|
28
|
+
}
|
|
29
|
+
function getRegisteredLocales() {
|
|
30
|
+
return [...getRegistered()];
|
|
31
|
+
}
|
|
32
|
+
function addRegisteredLocale(normalized) {
|
|
33
|
+
const registered = getRegistered();
|
|
34
|
+
if (registered.has(normalized)) return false;
|
|
35
|
+
registered.add(normalized);
|
|
36
|
+
globalScope()[CACHE_KEY] = null;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function clearRegisteredLocaleSet() {
|
|
40
|
+
const registered = getRegistered();
|
|
41
|
+
if (registered.size === 0) return false;
|
|
42
|
+
registered.clear();
|
|
43
|
+
globalScope()[CACHE_KEY] = null;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
addRegisteredLocale,
|
|
48
|
+
clearRegisteredLocaleSet,
|
|
49
|
+
getRegisteredLocales,
|
|
50
|
+
getSupportedLocales,
|
|
51
|
+
isSupportedLocale,
|
|
52
|
+
normalizeLocaleCode
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=locale-set.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/i18n/locale-set.ts"],
|
|
4
|
+
"sourcesContent": ["import { locales, type Locale } from './config'\n\n// The read side of the locale registry, kept in its own module with no imports\n// beyond `./config` so it is safe \u2014 and cheap \u2014 to pull into a client bundle.\n// `context.tsx` is a \"use client\" module and needs `getSupportedLocales()`; if it\n// reached for `./locale-registry` instead it would drag the logger facade, the\n// dictionary cache and the ISO 639 table into every route that mounts\n// `I18nProvider`, because `./locale-registry` has module-level side effects that\n// no bundler can tree-shake away.\n//\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same file\n// can be loaded as multiple module instances when mixing dynamic and static\n// imports. The registry and the dictionary cache it invalidates must stay\n// coherent across those instances. Mirrors `../modules/registry.ts` and\n// `./dictionary-cache.ts`.\nconst GLOBAL_KEY = '__openMercatoI18nLocaleRegistry__'\nconst CACHE_KEY = '__openMercatoI18nSupportedLocales__'\n\ntype LocaleSetGlobalScope = typeof globalThis & {\n [GLOBAL_KEY]?: Set<string>\n [CACHE_KEY]?: readonly Locale[] | null\n}\n\nfunction globalScope(): LocaleSetGlobalScope {\n return globalThis as LocaleSetGlobalScope\n}\n\nfunction getRegistered(): Set<string> {\n const scope = globalScope()\n if (!scope[GLOBAL_KEY]) {\n scope[GLOBAL_KEY] = new Set<string>()\n }\n return scope[GLOBAL_KEY]\n}\n\n/** `pt_BR` \u2192 `pt-br`. Applied on both sides of every comparison. */\nexport function normalizeLocaleCode(value: string): string {\n return value.trim().toLowerCase().replace(/_/g, '-')\n}\n\n/**\n * Every locale this application can serve: the platform baseline plus anything\n * registered by the app. The single runtime authority on the locale set \u2014 prefer\n * it over importing `locales` directly anywhere a user-supplied value is being\n * validated or a locale list is being rendered.\n *\n * Memoized so the returned array keeps a stable identity between mutations.\n * `useSupportedLocales()` hands this straight to callers, and a fresh array on\n * every render would re-fire any `useEffect`/`useMemo` that depends on it.\n */\nexport function getSupportedLocales(): readonly Locale[] {\n const scope = globalScope()\n const cached = scope[CACHE_KEY]\n if (cached) return cached\n\n const registered = getRegistered()\n // With nothing registered this is `locales` itself, not a copy, so the common\n // case allocates nothing and callers can compare by identity.\n const resolved = registered.size === 0 ? locales : ([...locales, ...registered] as Locale[])\n scope[CACHE_KEY] = resolved\n return resolved\n}\n\n/** True when `code` is a locale this application serves. */\nexport function isSupportedLocale(code: string): boolean {\n return (getSupportedLocales() as readonly string[]).includes(normalizeLocaleCode(code))\n}\n\n/** Locales registered by the app, excluding the platform baseline. Test seam. */\nexport function getRegisteredLocales(): readonly string[] {\n return [...getRegistered()]\n}\n\n/**\n * Add one already-normalized, already-validated code. Returns whether the set\n * actually changed, so the caller knows when to invalidate what it derived.\n * Internal to `./locale-registry` \u2014 applications call `registerLocales`.\n */\nexport function addRegisteredLocale(normalized: string): boolean {\n const registered = getRegistered()\n if (registered.has(normalized)) return false\n registered.add(normalized)\n globalScope()[CACHE_KEY] = null\n return true\n}\n\n/**\n * Drop every app-registered locale. Returns whether the set actually changed.\n * Internal to `./locale-registry` \u2014 tests call `clearRegisteredLocales`.\n */\nexport function clearRegisteredLocaleSet(): boolean {\n const registered = getRegistered()\n if (registered.size === 0) return false\n registered.clear()\n globalScope()[CACHE_KEY] = null\n return true\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAA4B;AAgBrC,MAAM,aAAa;AACnB,MAAM,YAAY;AAOlB,SAAS,cAAoC;AAC3C,SAAO;AACT;AAEA,SAAS,gBAA6B;AACpC,QAAM,QAAQ,YAAY;AAC1B,MAAI,CAAC,MAAM,UAAU,GAAG;AACtB,UAAM,UAAU,IAAI,oBAAI,IAAY;AAAA,EACtC;AACA,SAAO,MAAM,UAAU;AACzB;AAGO,SAAS,oBAAoB,OAAuB;AACzD,SAAO,MAAM,KAAK,EAAE,YAAY,EAAE,QAAQ,MAAM,GAAG;AACrD;AAYO,SAAS,sBAAyC;AACvD,QAAM,QAAQ,YAAY;AAC1B,QAAM,SAAS,MAAM,SAAS;AAC9B,MAAI,OAAQ,QAAO;AAEnB,QAAM,aAAa,cAAc;AAGjC,QAAM,WAAW,WAAW,SAAS,IAAI,UAAW,CAAC,GAAG,SAAS,GAAG,UAAU;AAC9E,QAAM,SAAS,IAAI;AACnB,SAAO;AACT;AAGO,SAAS,kBAAkB,MAAuB;AACvD,SAAQ,oBAAoB,EAAwB,SAAS,oBAAoB,IAAI,CAAC;AACxF;AAGO,SAAS,uBAA0C;AACxD,SAAO,CAAC,GAAG,cAAc,CAAC;AAC5B;AAOO,SAAS,oBAAoB,YAA6B;AAC/D,QAAM,aAAa,cAAc;AACjC,MAAI,WAAW,IAAI,UAAU,EAAG,QAAO;AACvC,aAAW,IAAI,UAAU;AACzB,cAAY,EAAE,SAAS,IAAI;AAC3B,SAAO;AACT;AAMO,SAAS,2BAAoC;AAClD,QAAM,aAAa,cAAc;AACjC,MAAI,WAAW,SAAS,EAAG,QAAO;AAClC,aAAW,MAAM;AACjB,cAAY,EAAE,SAAS,IAAI;AAC3B,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/i18n/locale.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSupportedLocales } from "./locale-set.js";
|
|
2
2
|
function normalizeLocaleToken(value) {
|
|
3
3
|
return value.trim().toLowerCase().replace(/_/g, "-");
|
|
4
4
|
}
|
|
5
|
-
function resolveSupportedLocale(value) {
|
|
5
|
+
function resolveSupportedLocale(value, supported = getSupportedLocales()) {
|
|
6
6
|
if (typeof value !== "string") return null;
|
|
7
7
|
const normalized = normalizeLocaleToken(value);
|
|
8
8
|
if (!normalized) return null;
|
|
9
|
-
if (
|
|
9
|
+
if (supported.includes(normalized)) {
|
|
10
10
|
return normalized;
|
|
11
11
|
}
|
|
12
12
|
const baseLocale = normalized.split("-")[0];
|
|
13
|
-
if (baseLocale &&
|
|
13
|
+
if (baseLocale && supported.includes(baseLocale)) {
|
|
14
14
|
return baseLocale;
|
|
15
15
|
}
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
-
function resolveLocaleFromCandidates(candidates) {
|
|
18
|
+
function resolveLocaleFromCandidates(candidates, supported) {
|
|
19
19
|
for (const candidate of candidates) {
|
|
20
|
-
const resolved = resolveSupportedLocale(candidate);
|
|
20
|
+
const resolved = resolveSupportedLocale(candidate, supported);
|
|
21
21
|
if (resolved) return resolved;
|
|
22
22
|
}
|
|
23
23
|
return null;
|
|
@@ -25,7 +25,7 @@ function resolveLocaleFromCandidates(candidates) {
|
|
|
25
25
|
function resolveForcedLocale(env) {
|
|
26
26
|
return resolveSupportedLocale(env.OM_FORCE_LOCALE);
|
|
27
27
|
}
|
|
28
|
-
function resolveLocaleFromAcceptLanguage(acceptLanguage) {
|
|
28
|
+
function resolveLocaleFromAcceptLanguage(acceptLanguage, supported) {
|
|
29
29
|
if (typeof acceptLanguage !== "string" || acceptLanguage.trim().length === 0) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
@@ -42,7 +42,7 @@ function resolveLocaleFromAcceptLanguage(acceptLanguage) {
|
|
|
42
42
|
}
|
|
43
43
|
return left.index - right.index;
|
|
44
44
|
});
|
|
45
|
-
return resolveLocaleFromCandidates(rankedCandidates.map((entry) => entry.locale));
|
|
45
|
+
return resolveLocaleFromCandidates(rankedCandidates.map((entry) => entry.locale), supported);
|
|
46
46
|
}
|
|
47
47
|
export {
|
|
48
48
|
resolveForcedLocale,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/i18n/locale.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Locale } from './config'\nimport { getSupportedLocales } from './locale-set'\n\nfunction normalizeLocaleToken(value: string): string {\n return value.trim().toLowerCase().replace(/_/g, '-')\n}\n\n/**\n * Canonicalize a user-supplied locale token against the set of locales that may\n * be served, folding a region subtag down to its base language (`de-AT` \u2192 `de`).\n *\n * `supported` defaults to the process-wide set. Pass the request's served set \u2014\n * from `resolveSupportedLocalesForRequest()` \u2014 anywhere the answer is written\n * somewhere durable, such as the `locale` cookie: the process-wide set is wider\n * than a tenant's selection, so validating against it would accept a locale that\n * every later render then discards, and report success while nothing changes.\n */\nexport function resolveSupportedLocale(\n value: string | null | undefined,\n supported: readonly Locale[] = getSupportedLocales(),\n): Locale | null {\n if (typeof value !== 'string') return null\n\n const normalized = normalizeLocaleToken(value)\n if (!normalized) return null\n\n if (supported.includes(normalized as Locale)) {\n return normalized as Locale\n }\n\n const baseLocale = normalized.split('-')[0]\n if (baseLocale && supported.includes(baseLocale as Locale)) {\n return baseLocale as Locale\n }\n\n return null\n}\n\nexport function resolveLocaleFromCandidates(\n candidates: Iterable<string | null | undefined>,\n supported?: readonly Locale[],\n): Locale | null {\n for (const candidate of candidates) {\n const resolved = resolveSupportedLocale(candidate, supported)\n if (resolved) return resolved\n }\n return null\n}\n\n/**\n * Reads the optional `OM_FORCE_LOCALE` env override. When set to a supported\n * locale (e.g. `pl`), the whole app is pinned to it and cookie/Accept-Language\n * detection is bypassed. Unset (the default) \u2192 `null` \u2192 normal detection.\n * Pure: pass the env bag so it stays testable and safe to call server-side only.\n */\nexport function resolveForcedLocale(\n env: Record<string, string | undefined>,\n): Locale | null {\n return resolveSupportedLocale(env.OM_FORCE_LOCALE)\n}\n\nexport function resolveLocaleFromAcceptLanguage(\n acceptLanguage: string | null | undefined,\n supported?: readonly Locale[],\n): Locale | null {\n if (typeof acceptLanguage !== 'string' || acceptLanguage.trim().length === 0) {\n return null\n }\n\n const rankedCandidates = acceptLanguage\n .split(',')\n .map((entry, index) => {\n const [rawLocale, ...rawParams] = entry.split(';')\n const locale = rawLocale?.trim() ?? ''\n const qParam = rawParams.find((param) => param.trim().startsWith('q='))\n const parsedQ = qParam ? Number.parseFloat(qParam.trim().slice(2)) : 1\n const quality = Number.isFinite(parsedQ) ? Math.min(Math.max(parsedQ, 0), 1) : 1\n\n return { locale, quality, index }\n })\n .filter((entry) => entry.locale.length > 0 && entry.quality > 0)\n .sort((left, right) => {\n if (right.quality !== left.quality) {\n return right.quality - left.quality\n }\n return left.index - right.index\n })\n\n return resolveLocaleFromCandidates(rankedCandidates.map((entry) => entry.locale), supported)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,2BAA2B;AAEpC,SAAS,qBAAqB,OAAuB;AACnD,SAAO,MAAM,KAAK,EAAE,YAAY,EAAE,QAAQ,MAAM,GAAG;AACrD;AAYO,SAAS,uBACd,OACA,YAA+B,oBAAoB,GACpC;AACf,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,aAAa,qBAAqB,KAAK;AAC7C,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,UAAU,SAAS,UAAoB,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,WAAW,MAAM,GAAG,EAAE,CAAC;AAC1C,MAAI,cAAc,UAAU,SAAS,UAAoB,GAAG;AAC1D,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,4BACd,YACA,WACe;AACf,aAAW,aAAa,YAAY;AAClC,UAAM,WAAW,uBAAuB,WAAW,SAAS;AAC5D,QAAI,SAAU,QAAO;AAAA,EACvB;AACA,SAAO;AACT;AAQO,SAAS,oBACd,KACe;AACf,SAAO,uBAAuB,IAAI,eAAe;AACnD;AAEO,SAAS,gCACd,gBACA,WACe;AACf,MAAI,OAAO,mBAAmB,YAAY,eAAe,KAAK,EAAE,WAAW,GAAG;AAC5E,WAAO;AAAA,EACT;AAEA,QAAM,mBAAmB,eACtB,MAAM,GAAG,EACT,IAAI,CAAC,OAAO,UAAU;AACrB,UAAM,CAAC,WAAW,GAAG,SAAS,IAAI,MAAM,MAAM,GAAG;AACjD,UAAM,SAAS,WAAW,KAAK,KAAK;AACpC,UAAM,SAAS,UAAU,KAAK,CAAC,UAAU,MAAM,KAAK,EAAE,WAAW,IAAI,CAAC;AACtE,UAAM,UAAU,SAAS,OAAO,WAAW,OAAO,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI;AACrE,UAAM,UAAU,OAAO,SAAS,OAAO,IAAI,KAAK,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI;AAE/E,WAAO,EAAE,QAAQ,SAAS,MAAM;AAAA,EAClC,CAAC,EACA,OAAO,CAAC,UAAU,MAAM,OAAO,SAAS,KAAK,MAAM,UAAU,CAAC,EAC9D,KAAK,CAAC,MAAM,UAAU;AACrB,QAAI,MAAM,YAAY,KAAK,SAAS;AAClC,aAAO,MAAM,UAAU,KAAK;AAAA,IAC9B;AACA,WAAO,KAAK,QAAQ,MAAM;AAAA,EAC5B,CAAC;AAEH,SAAO,4BAA4B,iBAAiB,IAAI,CAAC,UAAU,MAAM,MAAM,GAAG,SAAS;AAC7F;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/i18n/server.js
CHANGED
|
@@ -4,9 +4,16 @@ import { createFallbackTranslator, createTranslator } from "./translate.js";
|
|
|
4
4
|
import { tryGetModules } from "../modules/registry.js";
|
|
5
5
|
import { loadAppDictionary } from "./app-dictionaries.js";
|
|
6
6
|
import { getCachedDictionary, setCachedDictionary } from "./dictionary-cache.js";
|
|
7
|
+
import { getSupportedLocales } from "./locale-registry.js";
|
|
7
8
|
import { registerModules, getModules } from "../modules/registry.js";
|
|
8
9
|
import { registerAppDictionaryLoader } from "./app-dictionaries.js";
|
|
9
10
|
import { invalidateDictionaryCache } from "./dictionary-cache.js";
|
|
11
|
+
import {
|
|
12
|
+
registerLocales,
|
|
13
|
+
getSupportedLocales as getSupportedLocales2,
|
|
14
|
+
registerSupportedLocalesResolver,
|
|
15
|
+
resolveSupportedLocalesForRequest as resolveSupportedLocalesForRequest2
|
|
16
|
+
} from "./locale-registry.js";
|
|
10
17
|
function flattenDictionary(source, prefix = "") {
|
|
11
18
|
if (!source || typeof source !== "object" || Array.isArray(source)) return {};
|
|
12
19
|
const result = {};
|
|
@@ -21,31 +28,35 @@ function flattenDictionary(source, prefix = "") {
|
|
|
21
28
|
}
|
|
22
29
|
return result;
|
|
23
30
|
}
|
|
24
|
-
async function detectLocale() {
|
|
31
|
+
async function detectLocale(options) {
|
|
25
32
|
const forced = resolveForcedLocale(process.env);
|
|
26
33
|
if (forced) return forced;
|
|
34
|
+
const supported = options?.supportedLocales ?? getSupportedLocales();
|
|
27
35
|
try {
|
|
28
36
|
const { cookies, headers } = await import("next/headers");
|
|
29
37
|
try {
|
|
30
38
|
const c = (await cookies()).get("locale")?.value;
|
|
31
|
-
if (c &&
|
|
39
|
+
if (c && supported.includes(c)) return c;
|
|
32
40
|
} catch {
|
|
33
41
|
}
|
|
34
42
|
try {
|
|
35
43
|
const accept = (await headers()).get("accept-language") || "";
|
|
36
|
-
const match = resolveLocaleFromAcceptLanguage(accept);
|
|
44
|
+
const match = resolveLocaleFromAcceptLanguage(accept, supported);
|
|
37
45
|
if (match) return match;
|
|
38
46
|
} catch {
|
|
39
47
|
}
|
|
40
48
|
} catch {
|
|
41
49
|
}
|
|
42
|
-
return defaultLocale;
|
|
50
|
+
if (supported.includes(defaultLocale)) return defaultLocale;
|
|
51
|
+
return supported[0] ?? defaultLocale;
|
|
43
52
|
}
|
|
44
53
|
async function loadDictionary(locale) {
|
|
45
54
|
const cached = getCachedDictionary(locale);
|
|
46
55
|
if (cached) return cached;
|
|
56
|
+
const needsDefaultLocaleBase = locale !== defaultLocale && !locales.includes(locale);
|
|
57
|
+
const merged = needsDefaultLocaleBase ? { ...await loadDictionary(defaultLocale) } : {};
|
|
47
58
|
const baseRaw = await loadAppDictionary(locale);
|
|
48
|
-
|
|
59
|
+
Object.assign(merged, flattenDictionary(baseRaw));
|
|
49
60
|
const modules = tryGetModules() ?? [];
|
|
50
61
|
for (const m of modules) {
|
|
51
62
|
const dict = m.translations?.[locale];
|
|
@@ -54,8 +65,8 @@ async function loadDictionary(locale) {
|
|
|
54
65
|
setCachedDictionary(locale, merged);
|
|
55
66
|
return merged;
|
|
56
67
|
}
|
|
57
|
-
async function resolveTranslations() {
|
|
58
|
-
const locale = await detectLocale();
|
|
68
|
+
async function resolveTranslations(options) {
|
|
69
|
+
const locale = await detectLocale(options);
|
|
59
70
|
const dict = await loadDictionary(locale);
|
|
60
71
|
const t = createTranslator(dict);
|
|
61
72
|
const translate = createFallbackTranslator(dict);
|
|
@@ -68,10 +79,14 @@ try {
|
|
|
68
79
|
export {
|
|
69
80
|
detectLocale,
|
|
70
81
|
getModules,
|
|
82
|
+
getSupportedLocales2 as getSupportedLocales,
|
|
71
83
|
invalidateDictionaryCache,
|
|
72
84
|
loadDictionary,
|
|
73
85
|
registerAppDictionaryLoader,
|
|
86
|
+
registerLocales,
|
|
74
87
|
registerModules,
|
|
88
|
+
registerSupportedLocalesResolver,
|
|
89
|
+
resolveSupportedLocalesForRequest2 as resolveSupportedLocalesForRequest,
|
|
75
90
|
resolveTranslations
|
|
76
91
|
};
|
|
77
92
|
//# sourceMappingURL=server.js.map
|
|
@@ -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'\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 // Ops-level override: pin the whole app to one locale (default: unset).\n const forced = resolveForcedLocale(process.env)\n if (forced) return forced\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 &&
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe,eAA4B;AAEpD,SAAS,qBAAqB,uCAAuC;AACrE,SAAS,0BAA0B,wBAAwB;AAC3D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,qBAAqB,2BAA2B;
|
|
6
|
-
"names": []
|
|
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 const baseRaw = await loadAppDictionary(locale)\n Object.assign(merged, flattenDictionary(baseRaw))\n // Route handlers translate their responses, so they resolve a dictionary even\n // when they are exercised in isolation without a bootstrapped registry. The\n // app dictionary alone is the right degraded answer there \u2014 `registerModules`\n // invalidates this cache, so a later bootstrap still gets the merged result.\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 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;AAE9F,QAAM,UAAU,MAAM,kBAAkB,MAAM;AAC9C,SAAO,OAAO,QAAQ,kBAAkB,OAAO,CAAC;AAKhD,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,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
|
+
"names": ["getSupportedLocales", "resolveSupportedLocalesForRequest"]
|
|
7
7
|
}
|
|
@@ -3,9 +3,9 @@ import { render } from "@testing-library/react";
|
|
|
3
3
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
4
|
import { I18nProvider } from "@open-mercato/shared/lib/i18n/context";
|
|
5
5
|
function renderWithProviders(ui, options) {
|
|
6
|
-
const { locale = "en", dict = {}, queryClient = new QueryClient(), ...rest } = options ?? {};
|
|
6
|
+
const { locale = "en", dict = {}, queryClient = new QueryClient(), supportedLocales, ...rest } = options ?? {};
|
|
7
7
|
function Wrapper({ children }) {
|
|
8
|
-
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(I18nProvider, { locale, dict, children }) });
|
|
8
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(I18nProvider, { locale, dict, supportedLocales, children }) });
|
|
9
9
|
}
|
|
10
10
|
return render(ui, { wrapper: Wrapper, ...rest });
|
|
11
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/testing/renderWithProviders.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport type { RenderOptions } from '@testing-library/react'\nimport { render } from '@testing-library/react'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { I18nProvider } from '@open-mercato/shared/lib/i18n/context'\n\ntype ProviderOptions = {\n locale?: string\n dict?: Record<string, unknown>\n queryClient?: QueryClient\n}\n\nexport function renderWithProviders(\n ui: React.ReactElement,\n options?: RenderOptions & ProviderOptions,\n) {\n const { locale = 'en', dict = {}, queryClient = new QueryClient(), ...rest } = options ?? {}\n\n function Wrapper({ children }: { children: React.ReactNode }) {\n return (\n <QueryClientProvider client={queryClient}>\n {/* @ts-expect-error shared provider accepts loose dict shape */}\n <I18nProvider locale={locale} dict={dict}>\n {children}\n </I18nProvider>\n </QueryClientProvider>\n )\n }\n\n return render(ui, { wrapper: Wrapper, ...rest })\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport type { RenderOptions } from '@testing-library/react'\nimport { render } from '@testing-library/react'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport { I18nProvider } from '@open-mercato/shared/lib/i18n/context'\n\ntype ProviderOptions = {\n locale?: string\n dict?: Record<string, unknown>\n queryClient?: QueryClient\n /** Narrow the served locale set, as the server does for a tenant selection. */\n supportedLocales?: readonly string[]\n}\n\nexport function renderWithProviders(\n ui: React.ReactElement,\n options?: RenderOptions & ProviderOptions,\n) {\n const { locale = 'en', dict = {}, queryClient = new QueryClient(), supportedLocales, ...rest } = options ?? {}\n\n function Wrapper({ children }: { children: React.ReactNode }) {\n return (\n <QueryClientProvider client={queryClient}>\n {/* @ts-expect-error shared provider accepts loose dict shape */}\n <I18nProvider locale={locale} dict={dict} supportedLocales={supportedLocales}>\n {children}\n </I18nProvider>\n </QueryClientProvider>\n )\n }\n\n return render(ui, { wrapper: Wrapper, ...rest })\n}\n"],
|
|
5
|
+
"mappings": "AAwBQ;AAtBR,SAAS,cAAc;AACvB,SAAS,aAAa,2BAA2B;AACjD,SAAS,oBAAoB;AAUtB,SAAS,oBACd,IACA,SACA;AACA,QAAM,EAAE,SAAS,MAAM,OAAO,CAAC,GAAG,cAAc,IAAI,YAAY,GAAG,kBAAkB,GAAG,KAAK,IAAI,WAAW,CAAC;AAE7G,WAAS,QAAQ,EAAE,SAAS,GAAkC;AAC5D,WACE,oBAAC,uBAAoB,QAAQ,aAE3B,8BAAC,gBAAa,QAAgB,MAAY,kBACvC,UACH,GACF;AAAA,EAEJ;AAEA,SAAO,OAAO,IAAI,EAAE,SAAS,SAAS,GAAG,KAAK,CAAC;AACjD;",
|
|
6
6
|
"names": []
|
|
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.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7182.1.789943f937';\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.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7182.1.789943f937",
|
|
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.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7182.1.789943f937",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Fixture: proves an app that opts in KEEPS its exhaustiveness guarantee — a
|
|
2
|
+
// `Record<Locale, …>` that forgets the app's own locale must still fail to
|
|
3
|
+
// compile. Expected to produce a diagnostic; see `locale-augmentation.test.ts`.
|
|
4
|
+
import type { Locale } from '../../config'
|
|
5
|
+
|
|
6
|
+
declare module '../../config' {
|
|
7
|
+
interface LocaleRegistry {
|
|
8
|
+
cs: true
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const labels: Record<Locale, string> = {
|
|
13
|
+
en: 'English',
|
|
14
|
+
pl: 'Polski',
|
|
15
|
+
es: 'Español',
|
|
16
|
+
de: 'Deutsch',
|
|
17
|
+
ko: '한국어',
|
|
18
|
+
}
|