@burojs/i18n-core 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -35,6 +35,29 @@ resources.<resource>.fields.<field>.label → fields.<field>.label → <
35
35
 
36
36
  `resolveKey` implements that chain and is exported for callers that need to know *which* key won.
37
37
 
38
+ ## Counts
39
+
40
+ Text that shows a number is a **count family**: one key per CLDR plural
41
+ category of the locale, `<key>_one`, `<key>_few`, `<key>_many`, `<key>_other`.
42
+ Ask for it with `count` — the facade picks the form with the locale's own
43
+ rules and interpolates `{{count}}`:
44
+
45
+ ```ts
46
+ // ru: count_one «{{count}} документ», count_few «{{count}} документа», count_many «{{count}} документов»
47
+ i18n.t('resources.document.count', { count: 2 }); // «2 документа»
48
+ i18n.t('resources.document.count', { count: 5 }); // «5 документов»
49
+ i18n.has('resources.document.count', { count: 5 }); // true
50
+ i18n.has('resources.document.count'); // false — a family is only present for a count
51
+ ```
52
+
53
+ A count family is for counted text only. A collection title is not a count
54
+ form — Russian «Документы» is none of `one`, `few` or `many` — so a resource's
55
+ title is its own key, `labelPlural`.
56
+
57
+ An empty-string translation counts as missing: the adapter walks on to the
58
+ fallback locale, then the caller's default in code. Which key each element of
59
+ an app reads is in one table in [`docs/i18n-guide.md`](../../docs/i18n-guide.md).
60
+
38
61
  ## Locale registry
39
62
 
40
63
  Most apps assemble their `resources` from more than one place: the framework
@@ -51,7 +74,9 @@ import {
51
74
  } from '@burojs/i18n-core';
52
75
  import { en as frameworkEn } from '@burojs/react';
53
76
 
54
- const registry = createLocaleRegistry();
77
+ // `primary` is the locale whose text is the default in code. It is required, and it is the only
78
+ // place the app names it: everything else reads `registry.primary()`.
79
+ const registry = createLocaleRegistry({ primary: 'en' });
55
80
  registry.register({ locale: 'en', priority: 0, source: '@burojs/react', bundle: frameworkEn });
56
81
 
57
82
  // One file per locale, globbed by the bundler — dropping in `es.ts` is the
@@ -61,12 +86,19 @@ for (const c of localeContributionsFromGlob(glob, { source: 'app', priority: 10
61
86
  registry.register(c);
62
87
  }
63
88
 
64
- const i18n = await createI18nFromRegistry(registry, {
65
- defaultLocale: 'en',
66
- fallbackLocale: 'en', // or a per-locale chain: { fr: ['en'], default: ['en'] }
67
- });
89
+ // Starts in, and falls back to, registry.primary(). Override either when needed:
90
+ // `defaultLocale` for a stored choice, `fallbackLocale` for a per-locale chain
91
+ // such as { fr: ['es'], default: ['en'] }.
92
+ const i18n = await createI18nFromRegistry(registry);
68
93
  ```
69
94
 
95
+ `buildResources()` — and so `createI18nFromRegistry` — throws when no
96
+ contribution is registered for the primary locale: the fallback chain would
97
+ end at a locale with no bundle, and a language switcher built from
98
+ `registry.locales()` would not offer it. That is almost always a typo or a
99
+ glob that missed its file; an app with genuinely no strings of its own for the
100
+ primary locale registers an empty bundle for it.
101
+
70
102
  A key collision within one (locale, namespace) resolves by the higher
71
103
  DECLARED `priority`, never by which contribution happened to register last.
72
104
  `localeContributionsFromGlob` is bundler-agnostic (same reasoning as
@@ -80,7 +112,7 @@ this package — is the one that actually calls the bundler API.
80
112
  - **`createI18nextAdapter`** — the default backend; swap it to use another i18n runtime.
81
113
  - **`createResourceI18n` / `ResourceI18n`** — a resource-scoped view of the facade.
82
114
  - **`resolveKey`** — the fallback-chain resolver, usable standalone.
83
- - **`createLocaleRegistry` / `LocaleRegistry`** — merges per-source locale contributions by declared priority.
115
+ - **`createLocaleRegistry({ primary })` / `LocaleRegistry`** — merges per-source locale contributions by declared priority, and declares the primary locale (`registry.primary()`).
84
116
  - **`localeContributionsFromGlob`** — turns a bundler glob of per-locale files into contributions.
85
117
  - **`createI18nFromRegistry`** — wires a `LocaleRegistry` straight into `createI18nextAdapter` + `createI18n`.
86
118
 
package/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ i18next = __toESM(i18next, 1);
26
26
  //#region src/lookup.ts
27
27
  function resolveKey(key, resource) {
28
28
  if (!key) return [];
29
- if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.")) return [key];
29
+ if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.") || key.startsWith("blocks.")) return [key];
30
30
  const candidates = [];
31
31
  if (resource) candidates.push(`resources.${resource}.${key}`);
32
32
  candidates.push(`resources._.${key}`);
@@ -36,6 +36,7 @@ function resolveKey(key, resource) {
36
36
  }
37
37
  //#endregion
38
38
  //#region src/i18next-adapter.ts
39
+ const MISSING = "\0";
39
40
  async function createI18nextAdapter(config) {
40
41
  const instance = i18next.default.createInstance();
41
42
  await instance.init({
@@ -47,7 +48,8 @@ async function createI18nextAdapter(config) {
47
48
  nsSeparator: config.nsSeparator ?? false,
48
49
  defaultNS: "common",
49
50
  interpolation: { escapeValue: false },
50
- returnNull: false
51
+ returnNull: false,
52
+ returnEmptyString: false
51
53
  });
52
54
  return {
53
55
  t(key, params) {
@@ -59,7 +61,11 @@ async function createI18nextAdapter(config) {
59
61
  ns,
60
62
  ...params ?? {}
61
63
  });
62
- if (result === subKey && !instance.exists(subKey, { ns })) return key;
64
+ const count = typeof params?.count === "number" ? params.count : void 0;
65
+ if (result === subKey && !instance.exists(subKey, count === void 0 ? { ns } : {
66
+ ns,
67
+ count
68
+ })) return key;
63
69
  return result;
64
70
  },
65
71
  async changeLocale(locale) {
@@ -75,33 +81,42 @@ async function createI18nextAdapter(config) {
75
81
  instance.on("languageChanged", cb);
76
82
  return () => instance.off("languageChanged", cb);
77
83
  },
78
- hasKey(key) {
84
+ hasKey(key, options) {
79
85
  const dotIdx = key.indexOf(".");
80
86
  const ns = dotIdx === -1 ? void 0 : key.substring(0, dotIdx);
81
87
  const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);
82
- const opts = ns === void 0 ? void 0 : { ns };
83
- if (!instance.exists(subKey, opts)) return false;
88
+ const lookup = {
89
+ ...ns === void 0 ? {} : { ns },
90
+ ...options?.count === void 0 ? {} : { count: options.count }
91
+ };
84
92
  const value = instance.t(subKey, {
85
- ...opts ?? {},
86
- returnObjects: true
93
+ ...lookup,
94
+ returnObjects: true,
95
+ defaultValue: MISSING
87
96
  });
88
- return typeof value !== "object" || value === null;
97
+ return value !== MISSING && (typeof value !== "object" || value === null);
89
98
  }
90
99
  };
91
100
  }
92
101
  //#endregion
93
102
  //#region src/i18n.ts
94
103
  function createI18n(config) {
95
- const { adapter, warnOnMissing = true } = config;
104
+ const { adapter, warnOnMissing = true, locales } = config;
96
105
  return {
97
106
  t(key, options) {
107
+ const count = options?.count;
108
+ const params = count === void 0 ? options?.params : {
109
+ ...options?.params,
110
+ count
111
+ };
98
112
  const candidates = resolveKey(key, options?.resource);
99
- for (const candidate of candidates) if (adapter.hasKey(candidate)) return adapter.t(candidate, options?.params);
113
+ for (const candidate of candidates) if (count === void 0 ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count })) return adapter.t(candidate, params);
100
114
  if (warnOnMissing && typeof console !== "undefined") console.warn(`[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ""}`);
101
115
  return key;
102
116
  },
103
117
  has(key, options) {
104
- return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));
118
+ const count = options?.count;
119
+ return resolveKey(key, options?.resource).some((candidate) => count === void 0 ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count }));
105
120
  },
106
121
  async changeLocale(locale) {
107
122
  await adapter.changeLocale(locale);
@@ -115,6 +130,7 @@ function createI18n(config) {
115
130
  async loadNamespace(namespace, locale) {
116
131
  await adapter.loadNamespace(namespace, locale);
117
132
  },
133
+ ...locales === void 0 ? {} : { locales },
118
134
  adapter
119
135
  };
120
136
  }
@@ -125,7 +141,8 @@ function createResourceI18n(i18n, resource) {
125
141
  t(key, options) {
126
142
  return i18n.t(key, {
127
143
  resource: options?.resource ?? resource,
128
- params: options?.params
144
+ params: options?.params,
145
+ ...options?.count === void 0 ? {} : { count: options.count }
129
146
  });
130
147
  },
131
148
  changeLocale: i18n.changeLocale,
@@ -147,9 +164,14 @@ function mergeBundle(base, overlay) {
147
164
  }
148
165
  return out;
149
166
  }
150
- function createLocaleRegistry() {
167
+ function createLocaleRegistry(options) {
168
+ const primary = options?.primary;
169
+ if (typeof primary !== "string" || primary === "") throw new TypeError(`createLocaleRegistry needs { primary }, the locale whose text is the default in code; received ${JSON.stringify(primary)}.`);
151
170
  const registered = [];
152
171
  return {
172
+ primary() {
173
+ return primary;
174
+ },
153
175
  register(contribution) {
154
176
  registered.push(contribution);
155
177
  },
@@ -163,6 +185,10 @@ function createLocaleRegistry() {
163
185
  return registered;
164
186
  },
165
187
  buildResources() {
188
+ if (!registered.some((c) => c.locale === primary)) {
189
+ const known = Array.from(new Set(registered.map((c) => c.locale))).sort();
190
+ throw new Error(`The locale registry's primary locale ${JSON.stringify(primary)} has no contribution (registered: ${known.length === 0 ? "none" : known.join(", ")}). Register its bundle, an empty one if the app has no strings of its own, or declare the primary it does register.`);
191
+ }
166
192
  const byLocale = new Map();
167
193
  for (const contribution of registered) {
168
194
  const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;
@@ -213,17 +239,20 @@ function localeContributionsFromGlob(glob, options) {
213
239
  }
214
240
  //#endregion
215
241
  //#region src/create-i18n-from-registry.ts
216
- async function createI18nFromRegistry(registry, config) {
242
+ async function createI18nFromRegistry(registry, config = {}) {
243
+ const resources = registry.buildResources();
244
+ const primary = registry.primary();
217
245
  return createI18n({
218
246
  adapter: await createI18nextAdapter({
219
- defaultLocale: config.defaultLocale,
247
+ defaultLocale: config.defaultLocale ?? primary,
220
248
  locales: registry.locales(),
221
- resources: registry.buildResources(),
222
- ...config.fallbackLocale !== void 0 ? { fallbackLocale: config.fallbackLocale } : {},
249
+ resources,
250
+ fallbackLocale: config.fallbackLocale ?? primary,
223
251
  ...config.keySeparator !== void 0 ? { keySeparator: config.keySeparator } : {},
224
252
  ...config.nsSeparator !== void 0 ? { nsSeparator: config.nsSeparator } : {}
225
253
  }),
226
- warnOnMissing: config.warnOnMissing
254
+ warnOnMissing: config.warnOnMissing,
255
+ locales: () => registry.locales()
227
256
  });
228
257
  }
229
258
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/lookup.ts","../src/i18next-adapter.ts","../src/i18n.ts","../src/resource-context.ts","../src/locale-registry.ts","../src/create-i18n-from-registry.ts","../src/index.ts"],"sourcesContent":["/**\n * Build candidate keys for fallback lookup.\n *\n * Lookup order (first match wins):\n * 1. resources.<name>.<key> — resource-specific\n * 2. resources._.<key> — base for all resources\n * 3. common.<key> — global common\n * 4. <key> — raw fallback (debug only)\n *\n * If `key` is already namespaced (starts with `resources.`, `enums.` or\n * `common.`), return it as-is without further scoping.\n */\nexport function resolveKey(key: string, resource?: string): string[] {\n if (!key) return [];\n\n // Already namespaced — return as-is. `enums.` is a first-class top-level\n // namespace (global enum labels), like `resources.` and `common.`.\n if (key.startsWith('resources.') || key.startsWith('enums.') || key.startsWith('common.')) {\n return [key];\n }\n\n const candidates: string[] = [];\n if (resource) {\n candidates.push(`resources.${resource}.${key}`);\n }\n candidates.push(`resources._.${key}`);\n candidates.push(`common.${key}`);\n candidates.push(key); // raw fallback for warnings\n return candidates;\n}\n","import i18next, { type FallbackLng, type i18n } from 'i18next';\nimport type { I18nAdapter, TranslationBundle } from './types/index.js';\n\nexport interface I18nextAdapterConfig {\n defaultLocale: string;\n locales: string[];\n resources?: Record<string, Record<string, TranslationBundle>>;\n /**\n * A single fallback locale (`'en'`) applies uniformly to every locale. A\n * `fr → en` fallback for one locale but not another needs i18next's own\n * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so\n * this accepts i18next's full `FallbackLng` type rather than just `string`.\n * The chain is entirely CONFIG: nothing in this adapter special-cases any\n * particular locale code, here or anywhere else.\n */\n fallbackLocale?: FallbackLng;\n keySeparator?: string | false;\n nsSeparator?: string | false;\n}\n\n/**\n * Default I18nAdapter implementation backed by i18next.\n * Each top-level key in resources becomes a separate i18next namespace\n * (e.g. 'common', 'resources').\n */\nexport async function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter> {\n const instance: i18n = i18next.createInstance();\n\n await instance.init({\n lng: config.defaultLocale,\n fallbackLng: config.fallbackLocale ?? config.defaultLocale,\n supportedLngs: config.locales,\n resources: config.resources,\n keySeparator: config.keySeparator ?? '.',\n // Qualified Buro entity refs use `namespace:id`. Resource keys flow\n // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace\n // separator would split a valid ref before our explicit `ns` routing gets\n // to resolve it.\n nsSeparator: config.nsSeparator ?? false,\n defaultNS: 'common',\n interpolation: { escapeValue: false },\n returnNull: false,\n });\n\n return {\n t(key, params) {\n const dotIdx = key.indexOf('.');\n if (dotIdx === -1) {\n return instance.t(key, params) as string;\n }\n const ns = key.substring(0, dotIdx);\n const subKey = key.substring(dotIdx + 1);\n const result = instance.t(subKey, { ns, ...(params ?? {}) }) as string;\n // i18next returns the bare subKey when missing — restore the full key as fallback\n if (result === subKey && !instance.exists(subKey, { ns })) {\n return key;\n }\n return result;\n },\n async changeLocale(locale) {\n await instance.changeLanguage(locale);\n },\n async loadNamespace(namespace, _locale) {\n await instance.loadNamespaces(namespace);\n },\n getCurrentLocale() {\n return instance.language;\n },\n onLocaleChange(cb) {\n instance.on('languageChanged', cb);\n return () => instance.off('languageChanged', cb);\n },\n hasKey(key) {\n const dotIdx = key.indexOf('.');\n const ns = dotIdx === -1 ? undefined : key.substring(0, dotIdx);\n const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);\n const opts = ns === undefined ? undefined : { ns };\n if (!instance.exists(subKey, opts)) return false;\n /*\n `exists()` alone is not enough, and the gap is the root cause of a whole\n family of bugs rather than a curiosity.\n\n i18next answers `exists()` with TRUE for an OBJECT node — `fields.gtin`\n once that entry is promoted from a plain string to `{ label, doc }`,\n `enums.<ns>.<value>` when it carries a `doc` but no `label`, and so on.\n `t()` on such a node neither throws nor returns the key: it returns the\n literal sentence\n\n key '…' returned an object instead of string.\n\n Since `resolveContentSlot` commits to the FIRST key whose `has()` is\n true, every resolver built on it — field labels, enum labels, nav\n labels, tab captions — would render that sentence to the user. No crash,\n no warning, just wrong text on screen. It was found three separate times\n in three separate resolvers before being fixed here, once.\n\n So `has` now means what its callers always assumed: there is a USABLE\n STRING at this key. Non-object primitives stay truthy, leaving plural\n forms and numeric values behaving exactly as before.\n */\n const value = instance.t(subKey, { ...(opts ?? {}), returnObjects: true }) as unknown;\n return typeof value !== 'object' || value === null;\n },\n };\n}\n","import { resolveKey } from './lookup.js';\nimport type { I18nAdapter } from './types/index.js';\n\nexport interface I18nFacade {\n t: (key: string, options?: { resource?: string; params?: Record<string, unknown> }) => string;\n /**\n * True when a translation for `key` EXISTS (through the same candidate chain\n * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a\n * miss — the content model probes presence with `has` to tell \"no bundle\n * entry\" (render nothing) from \"bundle entry === key text\".\n */\n has: (key: string, options?: { resource?: string }) => boolean;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n /**\n * Subscribe to active-locale changes. Returns an unsubscribe fn. When the\n * adapter can't notify, this is a no-op subscription (returns an unsubscribe\n * that does nothing) — the live language switch then falls back to a static\n * read. Consumed by `useLocale()` via `useSyncExternalStore`.\n */\n onLocaleChange: (cb: () => void) => () => void;\n loadNamespace: (namespace: string, locale: string) => Promise<void>;\n adapter: I18nAdapter;\n}\n\nexport interface CreateI18nConfig {\n adapter: I18nAdapter;\n warnOnMissing?: boolean;\n}\n\nexport function createI18n(config: CreateI18nConfig): I18nFacade {\n const { adapter, warnOnMissing = true } = config;\n\n return {\n t(key, options) {\n const candidates = resolveKey(key, options?.resource);\n for (const candidate of candidates) {\n if (adapter.hasKey(candidate)) {\n return adapter.t(candidate, options?.params);\n }\n }\n if (warnOnMissing && typeof console !== 'undefined') {\n console.warn(\n `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ''}`,\n );\n }\n return key;\n },\n has(key, options) {\n return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));\n },\n async changeLocale(locale) {\n await adapter.changeLocale(locale);\n },\n getCurrentLocale() {\n return adapter.getCurrentLocale();\n },\n onLocaleChange(cb) {\n return adapter.onLocaleChange?.(cb) ?? (() => {});\n },\n async loadNamespace(namespace, locale) {\n await adapter.loadNamespace(namespace, locale);\n },\n adapter,\n };\n}\n","import type { I18nFacade } from './i18n.js';\n\nexport interface ResourceI18n {\n t: (key: string, options?: { resource?: string; params?: Record<string, unknown> }) => string;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n resource: string;\n}\n\n/**\n * Wraps an I18nFacade with a default resource scope.\n * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.\n * Pass an explicit `resource` option to override the default scope.\n */\nexport function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n {\n return {\n t(key, options) {\n return i18n.t(key, {\n resource: options?.resource ?? resource,\n params: options?.params,\n });\n },\n changeLocale: i18n.changeLocale,\n getCurrentLocale: i18n.getCurrentLocale,\n resource,\n };\n}\n","import type { TranslationBundle } from './types/bundle.js';\n\n/**\n * One package/app/domain module's offering for one locale.\n *\n * A contribution is data, not code: nothing about `createI18nextAdapter`'s\n * `resources` shape is visible here, and nothing here names a fixed set of\n * locale codes or counts them. `locale` is whatever the file/module that\n * produced this contribution says it is — the registry never validates it\n * against a known list, because there is no known list. That is the whole\n * point: a twenty-first language is a twenty-first contribution, not a\n * twenty-first `case`.\n */\nexport interface LocaleContribution {\n /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */\n locale: string;\n /**\n * i18next namespace the bundle merges into. Defaults to `'common'` — the\n * same default namespace `createI18nextAdapter` uses.\n */\n namespace?: string;\n /**\n * Declared merge priority. On a key collision within the same\n * (locale, namespace), the contribution with the HIGHER priority wins —\n * not whichever happened to register last. Ties fall back to registration\n * order (last registered wins), which is why two genuinely competing\n * sources must declare distinct priorities rather than relying on the tie\n * break.\n */\n priority: number;\n /**\n * Where this contribution came from — a package name, `'app'`, an enum\n * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}\n * for debugging a collision); never branched on.\n */\n source: string;\n bundle: TranslationBundle;\n}\n\nexport interface LocaleRegistry {\n /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */\n register(contribution: LocaleContribution): void;\n /**\n * Every locale code that has at least one contribution, sorted. Derived\n * purely from what was registered — nothing enumerates or counts locales\n * anywhere else in this module.\n */\n locales(): string[];\n /** Every namespace that has at least one contribution, sorted. */\n namespaces(): string[];\n /** All registered contributions, in registration order. For diagnostics/tests. */\n contributions(): readonly LocaleContribution[];\n /**\n * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to\n * pass straight as `createI18nextAdapter`'s `resources` config. Within one\n * (locale, namespace) group, contributions are deep-merged in ascending\n * priority order, so a higher-priority contribution's leaves win over a\n * lower-priority one's on any overlapping key while leaving the rest of\n * the lower-priority bundle intact.\n */\n buildResources(): Record<string, Record<string, TranslationBundle>>;\n}\n\nconst DEFAULT_NAMESPACE = 'common';\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursively merge `overlay` onto `base`. Arrays and primitives are replaced\n * wholesale (a `TranslationBundle` array leaf — see that type's own comment\n * on why arrays are a real leaf shape — is not index-merged: the whole array\n * is the unit an overlay replaces). Plain objects merge key by key.\n */\nfunction mergeBundle(base: TranslationBundle, overlay: TranslationBundle): TranslationBundle {\n const out: Record<string, unknown> = { ...base };\n for (const [key, overlayValue] of Object.entries(overlay)) {\n const baseValue = out[key];\n out[key] =\n isPlainObject(baseValue) && isPlainObject(overlayValue)\n ? mergeBundle(baseValue as TranslationBundle, overlayValue as TranslationBundle)\n : overlayValue;\n }\n return out as TranslationBundle;\n}\n\nexport function createLocaleRegistry(): LocaleRegistry {\n const registered: LocaleContribution[] = [];\n\n return {\n register(contribution) {\n registered.push(contribution);\n },\n locales() {\n return Array.from(new Set(registered.map((c) => c.locale))).sort();\n },\n namespaces() {\n return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();\n },\n contributions() {\n return registered;\n },\n buildResources() {\n // Group by (locale, namespace) via NESTED maps rather than a\n // delimiter-joined composite string key. A locale or namespace code is\n // caller-supplied and unconstrained (see `LocaleContribution.locale`'s\n // own comment), so any single-character delimiter is a value that\n // *could* collide with real content; nested `Map`s need no delimiter\n // at all, so there is nothing to collide with and nothing unusual\n // embedded in this file's source for a diff tool to trip over.\n const byLocale = new Map<string, Map<string, LocaleContribution[]>>();\n for (const contribution of registered) {\n const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;\n let byNamespace = byLocale.get(contribution.locale);\n if (!byNamespace) {\n byNamespace = new Map<string, LocaleContribution[]>();\n byLocale.set(contribution.locale, byNamespace);\n }\n const group = byNamespace.get(namespace);\n if (group) {\n group.push(contribution);\n } else {\n byNamespace.set(namespace, [contribution]);\n }\n }\n\n const resources: Record<string, Record<string, TranslationBundle>> = {};\n for (const [locale, byNamespace] of byLocale) {\n const namespaces: Record<string, TranslationBundle> = {};\n for (const [namespace, group] of byNamespace) {\n // Stable sort ascending by priority: lower priority applied first,\n // higher priority applied last so it wins the overlapping leaves.\n // `Array#sort` is stable per spec (ES2019+), so contributions with\n // equal priority keep their registration order — last-registered\n // wins ONLY within a tie, never across a declared priority gap.\n const ordered = [...group].sort((a, b) => a.priority - b.priority);\n let merged: TranslationBundle = {};\n for (const contribution of ordered) {\n merged = mergeBundle(merged, contribution.bundle);\n }\n namespaces[namespace] = merged;\n }\n resources[locale] = namespaces;\n }\n return resources;\n },\n };\n}\n\n/**\n * A bundler-produced glob module, as `import.meta.glob` (Vite) or an\n * equivalent walk over `require.context` (webpack) would yield it: a flat\n * map of path → either the loaded module itself or `{ default }`.\n *\n * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s\n * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published\n * package must not require one — the ADOPTER calls `import.meta.glob` (or\n * whatever their bundler offers) and hands the plain object here.\n */\nexport type LocaleGlobModule = TranslationBundle | { default: TranslationBundle };\n\nexport interface LocaleContributionsFromGlobOptions {\n /** {@link LocaleContribution.source} for every contribution produced. */\n source: string;\n /** {@link LocaleContribution.priority} for every contribution produced. */\n priority: number;\n /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */\n namespace?: string;\n /**\n * Extracts the locale code from a glob path. Default matches a bare\n * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`\n * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a\n * known set of codes; whatever the filename says IS the locale code. Paths\n * that don't match are skipped rather than guessed at (same discipline as\n * `docsFromGlob`).\n */\n localeOf?: (path: string) => string | undefined;\n}\n\nconst DEFAULT_LOCALE_PATTERN = /(?:^|\\/)([A-Za-z0-9-]+)\\.(?:ts|tsx|js|jsx|mjs|cjs)$/;\n\nfunction defaultLocaleOf(path: string): string | undefined {\n return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];\n}\n\n/**\n * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.\n * One file → one locale → one contribution. Nothing here enumerates or counts\n * locale codes: whatever files are present in the globbed directory at build\n * time ARE the contributions produced — dropping in a new file is the entire\n * mechanism for adding a language.\n */\nexport function localeContributionsFromGlob(\n glob: Record<string, LocaleGlobModule>,\n options: LocaleContributionsFromGlobOptions,\n): LocaleContribution[] {\n const localeOf = options.localeOf ?? defaultLocaleOf;\n const out: LocaleContribution[] = [];\n for (const [path, mod] of Object.entries(glob)) {\n const locale = localeOf(path);\n if (locale === undefined) continue;\n // Eager `import.meta.glob` yields a module namespace object — always\n // `{ default, ...named exports }` for a file that does `export default\n // {...}`. Unwrap it when present; otherwise trust the glob already\n // handed a bare bundle (e.g. a caller that pre-unwrapped, or a\n // require.context-style walk that returns exports directly).\n const bundle = isPlainObject((mod as { default?: unknown }).default)\n ? (mod as { default: TranslationBundle }).default\n : (mod as TranslationBundle);\n out.push({\n locale,\n priority: options.priority,\n source: options.source,\n bundle,\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n });\n }\n return out;\n}\n","import { type I18nFacade, createI18n } from './i18n.js';\nimport { type I18nextAdapterConfig, createI18nextAdapter } from './i18next-adapter.js';\nimport type { LocaleRegistry } from './locale-registry.js';\n\nexport type CreateI18nFromRegistryConfig = Pick<\n I18nextAdapterConfig,\n 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'\n> & {\n warnOnMissing?: boolean;\n};\n\n/**\n * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +\n * `createI18n`: `registry.locales()` becomes the adapter's `locales`,\n * `registry.buildResources()` becomes its `resources`. This is the ONLY\n * place those two need to be threaded together — an app that builds its own\n * registry never re-lists its locale codes to hand them to the adapter, it\n * just calls this.\n */\nexport async function createI18nFromRegistry(\n registry: LocaleRegistry,\n config: CreateI18nFromRegistryConfig,\n): Promise<I18nFacade> {\n const adapter = await createI18nextAdapter({\n defaultLocale: config.defaultLocale,\n locales: registry.locales(),\n resources: registry.buildResources(),\n ...(config.fallbackLocale !== undefined ? { fallbackLocale: config.fallbackLocale } : {}),\n ...(config.keySeparator !== undefined ? { keySeparator: config.keySeparator } : {}),\n ...(config.nsSeparator !== undefined ? { nsSeparator: config.nsSeparator } : {}),\n });\n return createI18n({ adapter, warnOnMissing: config.warnOnMissing });\n}\n","// Constants\nexport const PACKAGE_NAME = '@burojs/i18n-core' as const;\nexport const PACKAGE_VERSION = '0.0.0' as const;\n\n// Types\nexport type * from './types/index.js';\n\n// Lookup\nexport { resolveKey } from './lookup.js';\n\n// i18next adapter\nexport { createI18nextAdapter, type I18nextAdapterConfig } from './i18next-adapter.js';\n\n// I18n facade\nexport { createI18n, type I18nFacade, type CreateI18nConfig } from './i18n.js';\n\n// Resource context\nexport { createResourceI18n, type ResourceI18n } from './resource-context.js';\n\n// Locale registry — assembling locales from what has REGISTERED (framework\n// packages, the app, domain enum modules) instead of a hand-printed literal.\nexport {\n createLocaleRegistry,\n localeContributionsFromGlob,\n type LocaleContribution,\n type LocaleContributionsFromGlobOptions,\n type LocaleGlobModule,\n type LocaleRegistry,\n} from './locale-registry.js';\nexport {\n createI18nFromRegistry,\n type CreateI18nFromRegistryConfig,\n} from './create-i18n-from-registry.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAYA,SAAgB,WAAW,KAAa,UAA6B;CACnE,IAAI,CAAC,KAAK,OAAO,CAAC;CAIlB,IAAI,IAAI,WAAW,YAAY,KAAK,IAAI,WAAW,QAAQ,KAAK,IAAI,WAAW,SAAS,GACtF,OAAO,CAAC,GAAG;CAGb,MAAM,aAAuB,CAAC;CAC9B,IAAI,UACF,WAAW,KAAK,aAAa,SAAS,GAAG,KAAK;CAEhD,WAAW,KAAK,eAAe,KAAK;CACpC,WAAW,KAAK,UAAU,KAAK;CAC/B,WAAW,KAAK,GAAG;CACnB,OAAO;AACT;;;ACJA,eAAsB,qBAAqB,QAAoD;CAC7F,MAAM,WAAiB,QAAA,QAAQ,eAAe;CAE9C,MAAM,SAAS,KAAK;EAClB,KAAK,OAAO;EACZ,aAAa,OAAO,kBAAkB,OAAO;EAC7C,eAAe,OAAO;EACtB,WAAW,OAAO;EAClB,cAAc,OAAO,gBAAgB;EAKrC,aAAa,OAAO,eAAe;EACnC,WAAW;EACX,eAAe,EAAE,aAAa,MAAM;EACpC,YAAY;CACd,CAAC;CAED,OAAO;EACL,EAAE,KAAK,QAAQ;GACb,MAAM,SAAS,IAAI,QAAQ,GAAG;GAC9B,IAAI,WAAW,IACb,OAAO,SAAS,EAAE,KAAK,MAAM;GAE/B,MAAM,KAAK,IAAI,UAAU,GAAG,MAAM;GAClC,MAAM,SAAS,IAAI,UAAU,SAAS,CAAC;GACvC,MAAM,SAAS,SAAS,EAAE,QAAQ;IAAE;IAAI,GAAI,UAAU,CAAC;GAAG,CAAC;GAE3D,IAAI,WAAW,UAAU,CAAC,SAAS,OAAO,QAAQ,EAAE,GAAG,CAAC,GACtD,OAAO;GAET,OAAO;EACT;EACA,MAAM,aAAa,QAAQ;GACzB,MAAM,SAAS,eAAe,MAAM;EACtC;EACA,MAAM,cAAc,WAAW,SAAS;GACtC,MAAM,SAAS,eAAe,SAAS;EACzC;EACA,mBAAmB;GACjB,OAAO,SAAS;EAClB;EACA,eAAe,IAAI;GACjB,SAAS,GAAG,mBAAmB,EAAE;GACjC,aAAa,SAAS,IAAI,mBAAmB,EAAE;EACjD;EACA,OAAO,KAAK;GACV,MAAM,SAAS,IAAI,QAAQ,GAAG;GAC9B,MAAM,KAAK,WAAW,KAAK,KAAA,IAAY,IAAI,UAAU,GAAG,MAAM;GAC9D,MAAM,SAAS,WAAW,KAAK,MAAM,IAAI,UAAU,SAAS,CAAC;GAC7D,MAAM,OAAO,OAAO,KAAA,IAAY,KAAA,IAAY,EAAE,GAAG;GACjD,IAAI,CAAC,SAAS,OAAO,QAAQ,IAAI,GAAG,OAAO;GAuB3C,MAAM,QAAQ,SAAS,EAAE,QAAQ;IAAE,GAAI,QAAQ,CAAC;IAAI,eAAe;GAAK,CAAC;GACzE,OAAO,OAAO,UAAU,YAAY,UAAU;EAChD;CACF;AACF;;;AC1EA,SAAgB,WAAW,QAAsC;CAC/D,MAAM,EAAE,SAAS,gBAAgB,SAAS;CAE1C,OAAO;EACL,EAAE,KAAK,SAAS;GACd,MAAM,aAAa,WAAW,KAAK,SAAS,QAAQ;GACpD,KAAK,MAAM,aAAa,YACtB,IAAI,QAAQ,OAAO,SAAS,GAC1B,OAAO,QAAQ,EAAE,WAAW,SAAS,MAAM;GAG/C,IAAI,iBAAiB,OAAO,YAAY,aACtC,QAAQ,KACN,6CAA6C,IAAI,GAAG,SAAS,WAAW,eAAe,QAAQ,SAAS,KAAK,IAC/G;GAEF,OAAO;EACT;EACA,IAAI,KAAK,SAAS;GAChB,OAAO,WAAW,KAAK,SAAS,QAAQ,CAAC,CAAC,MAAM,cAAc,QAAQ,OAAO,SAAS,CAAC;EACzF;EACA,MAAM,aAAa,QAAQ;GACzB,MAAM,QAAQ,aAAa,MAAM;EACnC;EACA,mBAAmB;GACjB,OAAO,QAAQ,iBAAiB;EAClC;EACA,eAAe,IAAI;GACjB,OAAO,QAAQ,iBAAiB,EAAE,YAAY,CAAC;EACjD;EACA,MAAM,cAAc,WAAW,QAAQ;GACrC,MAAM,QAAQ,cAAc,WAAW,MAAM;EAC/C;EACA;CACF;AACF;;;ACnDA,SAAgB,mBAAmB,MAAkB,UAAgC;CACnF,OAAO;EACL,EAAE,KAAK,SAAS;GACd,OAAO,KAAK,EAAE,KAAK;IACjB,UAAU,SAAS,YAAY;IAC/B,QAAQ,SAAS;GACnB,CAAC;EACH;EACA,cAAc,KAAK;EACnB,kBAAkB,KAAK;EACvB;CACF;AACF;;;ACqCA,MAAM,oBAAoB;AAE1B,SAAS,cAAc,OAAkD;CACvE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAQA,SAAS,YAAY,MAAyB,SAA+C;CAC3F,MAAM,MAA+B,EAAE,GAAG,KAAK;CAC/C,KAAK,MAAM,CAAC,KAAK,iBAAiB,OAAO,QAAQ,OAAO,GAAG;EACzD,MAAM,YAAY,IAAI;EACtB,IAAI,OACF,cAAc,SAAS,KAAK,cAAc,YAAY,IAClD,YAAY,WAAgC,YAAiC,IAC7E;CACR;CACA,OAAO;AACT;AAEA,SAAgB,uBAAuC;CACrD,MAAM,aAAmC,CAAC;CAE1C,OAAO;EACL,SAAS,cAAc;GACrB,WAAW,KAAK,YAAY;EAC9B;EACA,UAAU;GACR,OAAO,MAAM,KAAK,IAAI,IAAI,WAAW,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK;EACnE;EACA,aAAa;GACX,OAAO,MAAM,KAAK,IAAI,IAAI,WAAW,KAAK,MAAM,EAAE,aAAa,iBAAiB,CAAC,CAAC,CAAC,CAAC,KAAK;EAC3F;EACA,gBAAgB;GACd,OAAO;EACT;EACA,iBAAiB;GAQf,MAAM,WAAW,IAAI,IAA+C;GACpE,KAAK,MAAM,gBAAgB,YAAY;IACrC,MAAM,YAAY,aAAa,aAAa;IAC5C,IAAI,cAAc,SAAS,IAAI,aAAa,MAAM;IAClD,IAAI,CAAC,aAAa;KAChB,cAAc,IAAI,IAAkC;KACpD,SAAS,IAAI,aAAa,QAAQ,WAAW;IAC/C;IACA,MAAM,QAAQ,YAAY,IAAI,SAAS;IACvC,IAAI,OACF,MAAM,KAAK,YAAY;SAEvB,YAAY,IAAI,WAAW,CAAC,YAAY,CAAC;GAE7C;GAEA,MAAM,YAA+D,CAAC;GACtE,KAAK,MAAM,CAAC,QAAQ,gBAAgB,UAAU;IAC5C,MAAM,aAAgD,CAAC;IACvD,KAAK,MAAM,CAAC,WAAW,UAAU,aAAa;KAM5C,MAAM,UAAU,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;KACjE,IAAI,SAA4B,CAAC;KACjC,KAAK,MAAM,gBAAgB,SACzB,SAAS,YAAY,QAAQ,aAAa,MAAM;KAElD,WAAW,aAAa;IAC1B;IACA,UAAU,UAAU;GACtB;GACA,OAAO;EACT;CACF;AACF;AAgCA,MAAM,yBAAyB;AAE/B,SAAS,gBAAgB,MAAkC;CACzD,OAAO,uBAAuB,KAAK,IAAI,CAAC,GAAG;AAC7C;AASA,SAAgB,4BACd,MACA,SACsB;CACtB,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,MAA4B,CAAC;CACnC,KAAK,MAAM,CAAC,MAAM,QAAQ,OAAO,QAAQ,IAAI,GAAG;EAC9C,MAAM,SAAS,SAAS,IAAI;EAC5B,IAAI,WAAW,KAAA,GAAW;EAM1B,MAAM,SAAS,cAAe,IAA8B,OAAO,IAC9D,IAAuC,UACvC;EACL,IAAI,KAAK;GACP;GACA,UAAU,QAAQ;GAClB,QAAQ,QAAQ;GAChB;GACA,GAAI,QAAQ,cAAc,KAAA,IAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;EAC5E,CAAC;CACH;CACA,OAAO;AACT;;;ACxMA,eAAsB,uBACpB,UACA,QACqB;CASrB,OAAO,WAAW;EAAE,eARE,qBAAqB;GACzC,eAAe,OAAO;GACtB,SAAS,SAAS,QAAQ;GAC1B,WAAW,SAAS,eAAe;GACnC,GAAI,OAAO,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,OAAO,eAAe,IAAI,CAAC;GACvF,GAAI,OAAO,iBAAiB,KAAA,IAAY,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;GACjF,GAAI,OAAO,gBAAgB,KAAA,IAAY,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;EAChF,CAAC;EAC4B,eAAe,OAAO;CAAc,CAAC;AACpE;;;AC/BA,MAAa,eAAe;AAC5B,MAAa,kBAAkB"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/lookup.ts","../src/i18next-adapter.ts","../src/i18n.ts","../src/resource-context.ts","../src/locale-registry.ts","../src/create-i18n-from-registry.ts","../src/index.ts"],"sourcesContent":["/**\n * Build candidate keys for fallback lookup.\n *\n * Lookup order (first match wins):\n * 1. resources.<name>.<key> — resource-specific\n * 2. resources._.<key> — base for all resources\n * 3. common.<key> — global common\n * 4. <key> — raw fallback (debug only)\n *\n * If `key` is already namespaced (starts with `resources.`, `enums.`,\n * `common.` or `blocks.`), return it as-is without further scoping.\n */\nexport function resolveKey(key: string, resource?: string): string[] {\n if (!key) return [];\n\n // Already namespaced — return as-is. `enums.` is a first-class top-level\n // namespace (global enum labels), like `resources.` and `common.`; so is\n // `blocks.` (a block view's `t` reads `blocks.<id>.<key>`).\n if (\n key.startsWith('resources.') ||\n key.startsWith('enums.') ||\n key.startsWith('common.') ||\n key.startsWith('blocks.')\n ) {\n return [key];\n }\n\n const candidates: string[] = [];\n if (resource) {\n candidates.push(`resources.${resource}.${key}`);\n }\n candidates.push(`resources._.${key}`);\n candidates.push(`common.${key}`);\n candidates.push(key); // raw fallback for warnings\n return candidates;\n}\n","import i18next, { type FallbackLng, type i18n } from 'i18next';\nimport type { I18nAdapter, TranslationBundle } from './types/index.js';\n\n/** What a lookup answers when no locale in the chain holds a usable value. */\nconst MISSING = '\\u0000';\n\nexport interface I18nextAdapterConfig {\n defaultLocale: string;\n locales: string[];\n resources?: Record<string, Record<string, TranslationBundle>>;\n /**\n * A single fallback locale (`'en'`) applies uniformly to every locale. A\n * `fr → en` fallback for one locale but not another needs i18next's own\n * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so\n * this accepts i18next's full `FallbackLng` type rather than just `string`.\n * The chain is entirely CONFIG: nothing in this adapter special-cases any\n * particular locale code, here or anywhere else.\n */\n fallbackLocale?: FallbackLng;\n keySeparator?: string | false;\n nsSeparator?: string | false;\n}\n\n/**\n * Default I18nAdapter implementation backed by i18next.\n * Each top-level key in resources becomes a separate i18next namespace\n * (e.g. 'common', 'resources').\n */\nexport async function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter> {\n const instance: i18n = i18next.createInstance();\n\n await instance.init({\n lng: config.defaultLocale,\n fallbackLng: config.fallbackLocale ?? config.defaultLocale,\n supportedLngs: config.locales,\n resources: config.resources,\n keySeparator: config.keySeparator ?? '.',\n // Qualified Buro entity refs use `namespace:id`. Resource keys flow\n // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace\n // separator would split a valid ref before our explicit `ns` routing gets\n // to resolve it.\n nsSeparator: config.nsSeparator ?? false,\n defaultNS: 'common',\n interpolation: { escapeValue: false },\n returnNull: false,\n // An empty string is an untranslated key (what extraction tools write), not\n // a translation: `exists` and `t` walk on to the fallback locale, then the\n // caller's code default, so a label never renders blank.\n returnEmptyString: false,\n });\n\n return {\n t(key, params) {\n const dotIdx = key.indexOf('.');\n if (dotIdx === -1) {\n return instance.t(key, params) as string;\n }\n const ns = key.substring(0, dotIdx);\n const subKey = key.substring(dotIdx + 1);\n const result = instance.t(subKey, { ns, ...(params ?? {}) }) as string;\n // i18next returns the bare subKey when missing — restore the full key as\n // fallback. The probe carries the count, so a resolved count family is\n // not mistaken for a miss.\n const count = typeof params?.count === 'number' ? params.count : undefined;\n if (\n result === subKey &&\n !instance.exists(subKey, count === undefined ? { ns } : { ns, count })\n ) {\n return key;\n }\n return result;\n },\n async changeLocale(locale) {\n await instance.changeLanguage(locale);\n },\n async loadNamespace(namespace, _locale) {\n await instance.loadNamespaces(namespace);\n },\n getCurrentLocale() {\n return instance.language;\n },\n onLocaleChange(cb) {\n instance.on('languageChanged', cb);\n return () => instance.off('languageChanged', cb);\n },\n hasKey(key, options) {\n const dotIdx = key.indexOf('.');\n const ns = dotIdx === -1 ? undefined : key.substring(0, dotIdx);\n const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);\n const lookup = {\n ...(ns === undefined ? {} : { ns }),\n ...(options?.count === undefined ? {} : { count: options.count }),\n };\n /*\n `exists()` is not enough, and the gap is the root cause of a whole\n family of bugs rather than a curiosity.\n\n i18next answers `exists()` with TRUE for an OBJECT node — `fields.gtin`\n once that entry is promoted from a plain string to `{ label, doc }`,\n `enums.<ns>.<value>` when it carries a `doc` but no `label`, and so on.\n `t()` on such a node neither throws nor returns the key: it returns the\n literal sentence\n\n key '…' returned an object instead of string.\n\n Since `resolveContentSlot` commits to the FIRST key whose `has()` is\n true, every resolver built on it — field labels, enum labels, nav\n labels, tab captions — would render that sentence to the user. No crash,\n no warning, just wrong text on screen. It was found three separate times\n in three separate resolvers before being fixed here, once.\n\n `exists()` is also TRUE for an empty string in the last locale of the\n chain, even with `returnEmptyString: false`. So the lookup itself is the\n probe: `t()` walks the whole chain (count forms included) and answers the\n sentinel default only when no locale holds a usable value.\n\n So `has` means what its callers always assumed: there is a USABLE,\n non-empty STRING at this key. Non-object primitives stay truthy, leaving\n plural forms and numeric values behaving exactly as before.\n */\n const value = instance.t(subKey, {\n ...lookup,\n returnObjects: true,\n defaultValue: MISSING,\n }) as unknown;\n return value !== MISSING && (typeof value !== 'object' || value === null);\n },\n };\n}\n","import { resolveKey } from './lookup.js';\nimport type { I18nAdapter } from './types/index.js';\n\nexport interface I18nFacade {\n /**\n * Translate `key` through the candidate chain. `count` selects a count\n * family's form (`<key>_one | _few | _many | _other`, by the locale's CLDR\n * rules) and interpolates `{{count}}`; a family asked without a count is\n * missing. A `count` inside `params` only interpolates.\n */\n t: (\n key: string,\n options?: { resource?: string; params?: Record<string, unknown>; count?: number },\n ) => string;\n /**\n * True when a translation for `key` EXISTS (through the same candidate chain\n * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a\n * miss — the content model probes presence with `has` to tell \"no bundle\n * entry\" (render nothing) from \"bundle entry === key text\". Pass `count` to probe a count family.\n */\n has: (key: string, options?: { resource?: string; count?: number }) => boolean;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n /**\n * Subscribe to active-locale changes. Returns an unsubscribe fn. When the\n * adapter can't notify, this is a no-op subscription (returns an unsubscribe\n * that does nothing) — the live language switch then falls back to a static\n * read. Consumed by `useLocale()` via `useSyncExternalStore`.\n */\n onLocaleChange: (cb: () => void) => () => void;\n loadNamespace: (namespace: string, locale: string) => Promise<void>;\n /**\n * The locale codes this app can switch to. Optional: a facade built without\n * a list (a hand-rolled one, a test double) simply does not say, and a\n * language control then offers only the active locale. `createI18nFromRegistry`\n * fills it from the registry.\n */\n locales?: () => readonly string[];\n adapter: I18nAdapter;\n}\n\nexport interface CreateI18nConfig {\n adapter: I18nAdapter;\n warnOnMissing?: boolean;\n /** The switchable locale codes, exposed as {@link I18nFacade.locales}. */\n locales?: () => readonly string[];\n}\n\nexport function createI18n(config: CreateI18nConfig): I18nFacade {\n const { adapter, warnOnMissing = true, locales } = config;\n\n return {\n t(key, options) {\n // `count` is the one spelling: a `count` inside `params` only interpolates.\n const count = options?.count;\n const params = count === undefined ? options?.params : { ...options?.params, count };\n const candidates = resolveKey(key, options?.resource);\n for (const candidate of candidates) {\n const present =\n count === undefined ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count });\n if (present) {\n return adapter.t(candidate, params);\n }\n }\n if (warnOnMissing && typeof console !== 'undefined') {\n console.warn(\n `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ''}`,\n );\n }\n return key;\n },\n has(key, options) {\n const count = options?.count;\n return resolveKey(key, options?.resource).some((candidate) =>\n count === undefined ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count }),\n );\n },\n async changeLocale(locale) {\n await adapter.changeLocale(locale);\n },\n getCurrentLocale() {\n return adapter.getCurrentLocale();\n },\n onLocaleChange(cb) {\n return adapter.onLocaleChange?.(cb) ?? (() => {});\n },\n async loadNamespace(namespace, locale) {\n await adapter.loadNamespace(namespace, locale);\n },\n ...(locales === undefined ? {} : { locales }),\n adapter,\n };\n}\n","import type { I18nFacade } from './i18n.js';\n\nexport interface ResourceI18n {\n t: (\n key: string,\n options?: { resource?: string; params?: Record<string, unknown>; count?: number },\n ) => string;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n resource: string;\n}\n\n/**\n * Wraps an I18nFacade with a default resource scope.\n * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.\n * Pass an explicit `resource` option to override the default scope.\n */\nexport function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n {\n return {\n t(key, options) {\n return i18n.t(key, {\n resource: options?.resource ?? resource,\n params: options?.params,\n ...(options?.count === undefined ? {} : { count: options.count }),\n });\n },\n changeLocale: i18n.changeLocale,\n getCurrentLocale: i18n.getCurrentLocale,\n resource,\n };\n}\n","import type { TranslationBundle } from './types/bundle.js';\n\n/**\n * One package/app/domain module's offering for one locale.\n *\n * A contribution is data, not code: nothing about `createI18nextAdapter`'s\n * `resources` shape is visible here, and nothing here names a fixed set of\n * locale codes or counts them. `locale` is whatever the file/module that\n * produced this contribution says it is — the registry never validates it\n * against a known list, because there is no known list. That is the whole\n * point: a twenty-first language is a twenty-first contribution, not a\n * twenty-first `case`.\n */\nexport interface LocaleContribution {\n /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */\n locale: string;\n /**\n * i18next namespace the bundle merges into. Defaults to `'common'` — the\n * same default namespace `createI18nextAdapter` uses.\n */\n namespace?: string;\n /**\n * Declared merge priority. On a key collision within the same\n * (locale, namespace), the contribution with the HIGHER priority wins —\n * not whichever happened to register last. Ties fall back to registration\n * order (last registered wins), which is why two genuinely competing\n * sources must declare distinct priorities rather than relying on the tie\n * break.\n */\n priority: number;\n /**\n * Where this contribution came from — a package name, `'app'`, an enum\n * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}\n * for debugging a collision); never branched on.\n */\n source: string;\n bundle: TranslationBundle;\n}\n\nexport interface LocaleRegistry {\n /**\n * The primary locale, as declared to {@link createLocaleRegistry}: the one\n * whose text is the default in code, and the locale every other one falls\n * back to. The single source of truth for it — `createI18nFromRegistry`\n * defaults both the starting locale and the fallback to it, and\n * `@burojs/testing`'s `checkApp` reads it to know which locale is exempt\n * from `i18n.missing`. Never derived from contributions or their order.\n */\n primary(): string;\n /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */\n register(contribution: LocaleContribution): void;\n /**\n * Every locale code that has at least one contribution, sorted. Derived\n * purely from what was registered — nothing enumerates or counts locales\n * anywhere else in this module.\n */\n locales(): string[];\n /** Every namespace that has at least one contribution, sorted. */\n namespaces(): string[];\n /** All registered contributions, in registration order. For diagnostics/tests. */\n contributions(): readonly LocaleContribution[];\n /**\n * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to\n * pass straight as `createI18nextAdapter`'s `resources` config. Within one\n * (locale, namespace) group, contributions are deep-merged in ascending\n * priority order, so a higher-priority contribution's leaves win over a\n * lower-priority one's on any overlapping key while leaving the rest of\n * the lower-priority bundle intact.\n *\n * Throws when no contribution is registered for {@link primary}: a\n * fallback chain that ends at a locale with no bundle, and a primary that\n * `locales()` (and so every language switcher built from it) does not\n * list, is a misconfiguration (usually a typo, `'en-US'` for `'en'`, or a\n * glob that missed its file), not a state to render. An app that really\n * has no strings for its primary locale registers an empty bundle for it.\n */\n buildResources(): Record<string, Record<string, TranslationBundle>>;\n}\n\nconst DEFAULT_NAMESPACE = 'common';\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursively merge `overlay` onto `base`. Arrays and primitives are replaced\n * wholesale (a `TranslationBundle` array leaf — see that type's own comment\n * on why arrays are a real leaf shape — is not index-merged: the whole array\n * is the unit an overlay replaces). Plain objects merge key by key.\n */\nfunction mergeBundle(base: TranslationBundle, overlay: TranslationBundle): TranslationBundle {\n const out: Record<string, unknown> = { ...base };\n for (const [key, overlayValue] of Object.entries(overlay)) {\n const baseValue = out[key];\n out[key] =\n isPlainObject(baseValue) && isPlainObject(overlayValue)\n ? mergeBundle(baseValue as TranslationBundle, overlayValue as TranslationBundle)\n : overlayValue;\n }\n return out as TranslationBundle;\n}\n\nexport interface LocaleRegistryOptions {\n /**\n * The primary locale: the one whose text is the default in code. Required,\n * and declared here rather than inferred, because nothing in the\n * contributions says which locale it is — registration order and the\n * sorted `locales()` both carry no meaning.\n */\n primary: string;\n}\n\nexport function createLocaleRegistry(options: LocaleRegistryOptions): LocaleRegistry {\n const primary = (options as Partial<LocaleRegistryOptions> | undefined)?.primary;\n if (typeof primary !== 'string' || primary === '') {\n throw new TypeError(\n `createLocaleRegistry needs { primary }, the locale whose text is the default in code; received ${JSON.stringify(primary)}.`,\n );\n }\n const registered: LocaleContribution[] = [];\n\n return {\n primary() {\n return primary;\n },\n register(contribution) {\n registered.push(contribution);\n },\n locales() {\n return Array.from(new Set(registered.map((c) => c.locale))).sort();\n },\n namespaces() {\n return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();\n },\n contributions() {\n return registered;\n },\n buildResources() {\n if (!registered.some((c) => c.locale === primary)) {\n const known = Array.from(new Set(registered.map((c) => c.locale))).sort();\n throw new Error(\n `The locale registry's primary locale ${JSON.stringify(primary)} has no contribution` +\n ` (registered: ${known.length === 0 ? 'none' : known.join(', ')}). Register its bundle,` +\n ' an empty one if the app has no strings of its own, or declare the primary it does register.',\n );\n }\n // Group by (locale, namespace) via NESTED maps rather than a\n // delimiter-joined composite string key. A locale or namespace code is\n // caller-supplied and unconstrained (see `LocaleContribution.locale`'s\n // own comment), so any single-character delimiter is a value that\n // *could* collide with real content; nested `Map`s need no delimiter\n // at all, so there is nothing to collide with and nothing unusual\n // embedded in this file's source for a diff tool to trip over.\n const byLocale = new Map<string, Map<string, LocaleContribution[]>>();\n for (const contribution of registered) {\n const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;\n let byNamespace = byLocale.get(contribution.locale);\n if (!byNamespace) {\n byNamespace = new Map<string, LocaleContribution[]>();\n byLocale.set(contribution.locale, byNamespace);\n }\n const group = byNamespace.get(namespace);\n if (group) {\n group.push(contribution);\n } else {\n byNamespace.set(namespace, [contribution]);\n }\n }\n\n const resources: Record<string, Record<string, TranslationBundle>> = {};\n for (const [locale, byNamespace] of byLocale) {\n const namespaces: Record<string, TranslationBundle> = {};\n for (const [namespace, group] of byNamespace) {\n // Stable sort ascending by priority: lower priority applied first,\n // higher priority applied last so it wins the overlapping leaves.\n // `Array#sort` is stable per spec (ES2019+), so contributions with\n // equal priority keep their registration order — last-registered\n // wins ONLY within a tie, never across a declared priority gap.\n const ordered = [...group].sort((a, b) => a.priority - b.priority);\n let merged: TranslationBundle = {};\n for (const contribution of ordered) {\n merged = mergeBundle(merged, contribution.bundle);\n }\n namespaces[namespace] = merged;\n }\n resources[locale] = namespaces;\n }\n return resources;\n },\n };\n}\n\n/**\n * A bundler-produced glob module, as `import.meta.glob` (Vite) or an\n * equivalent walk over `require.context` (webpack) would yield it: a flat\n * map of path → either the loaded module itself or `{ default }`.\n *\n * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s\n * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published\n * package must not require one — the ADOPTER calls `import.meta.glob` (or\n * whatever their bundler offers) and hands the plain object here.\n */\nexport type LocaleGlobModule = TranslationBundle | { default: TranslationBundle };\n\nexport interface LocaleContributionsFromGlobOptions {\n /** {@link LocaleContribution.source} for every contribution produced. */\n source: string;\n /** {@link LocaleContribution.priority} for every contribution produced. */\n priority: number;\n /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */\n namespace?: string;\n /**\n * Extracts the locale code from a glob path. Default matches a bare\n * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`\n * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a\n * known set of codes; whatever the filename says IS the locale code. Paths\n * that don't match are skipped rather than guessed at (same discipline as\n * `docsFromGlob`).\n */\n localeOf?: (path: string) => string | undefined;\n}\n\nconst DEFAULT_LOCALE_PATTERN = /(?:^|\\/)([A-Za-z0-9-]+)\\.(?:ts|tsx|js|jsx|mjs|cjs)$/;\n\nfunction defaultLocaleOf(path: string): string | undefined {\n return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];\n}\n\n/**\n * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.\n * One file → one locale → one contribution. Nothing here enumerates or counts\n * locale codes: whatever files are present in the globbed directory at build\n * time ARE the contributions produced — dropping in a new file is the entire\n * mechanism for adding a language.\n */\nexport function localeContributionsFromGlob(\n glob: Record<string, LocaleGlobModule>,\n options: LocaleContributionsFromGlobOptions,\n): LocaleContribution[] {\n const localeOf = options.localeOf ?? defaultLocaleOf;\n const out: LocaleContribution[] = [];\n for (const [path, mod] of Object.entries(glob)) {\n const locale = localeOf(path);\n if (locale === undefined) continue;\n // Eager `import.meta.glob` yields a module namespace object — always\n // `{ default, ...named exports }` for a file that does `export default\n // {...}`. Unwrap it when present; otherwise trust the glob already\n // handed a bare bundle (e.g. a caller that pre-unwrapped, or a\n // require.context-style walk that returns exports directly).\n const bundle = isPlainObject((mod as { default?: unknown }).default)\n ? (mod as { default: TranslationBundle }).default\n : (mod as TranslationBundle);\n out.push({\n locale,\n priority: options.priority,\n source: options.source,\n bundle,\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n });\n }\n return out;\n}\n","import { type I18nFacade, createI18n } from './i18n.js';\nimport { type I18nextAdapterConfig, createI18nextAdapter } from './i18next-adapter.js';\nimport type { LocaleRegistry } from './locale-registry.js';\n\nexport type CreateI18nFromRegistryConfig = Partial<\n Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'>\n> & {\n warnOnMissing?: boolean;\n};\n\n/**\n * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +\n * `createI18n`: `registry.locales()` becomes the adapter's `locales`,\n * `registry.buildResources()` becomes its `resources`, and\n * `registry.primary()` is both the starting locale and the fallback unless\n * `defaultLocale` (the starting locale, say a stored choice) or\n * `fallbackLocale` (say a per-locale chain) overrides it. This is the ONLY\n * place those need to be threaded together — an app that builds its own\n * registry never re-lists its locale codes, or restates its primary locale,\n * to hand them to the adapter; it just calls this.\n *\n * Throws when the registry's primary locale has no contribution (see\n * {@link LocaleRegistry.buildResources}).\n */\nexport async function createI18nFromRegistry(\n registry: LocaleRegistry,\n config: CreateI18nFromRegistryConfig = {},\n): Promise<I18nFacade> {\n const resources = registry.buildResources();\n const primary = registry.primary();\n const adapter = await createI18nextAdapter({\n defaultLocale: config.defaultLocale ?? primary,\n locales: registry.locales(),\n resources,\n fallbackLocale: config.fallbackLocale ?? primary,\n ...(config.keySeparator !== undefined ? { keySeparator: config.keySeparator } : {}),\n ...(config.nsSeparator !== undefined ? { nsSeparator: config.nsSeparator } : {}),\n });\n return createI18n({\n adapter,\n warnOnMissing: config.warnOnMissing,\n locales: () => registry.locales(),\n });\n}\n","// Constants\nexport const PACKAGE_NAME = '@burojs/i18n-core' as const;\nexport const PACKAGE_VERSION = '0.0.0' as const;\n\n// Types\nexport type * from './types/index.js';\n\n// Lookup\nexport { resolveKey } from './lookup.js';\n\n// i18next adapter\nexport { createI18nextAdapter, type I18nextAdapterConfig } from './i18next-adapter.js';\n\n// I18n facade\nexport { createI18n, type I18nFacade, type CreateI18nConfig } from './i18n.js';\n\n// Resource context\nexport { createResourceI18n, type ResourceI18n } from './resource-context.js';\n\n// Locale registry — assembling locales from what has REGISTERED (framework\n// packages, the app, domain enum modules) instead of a hand-printed literal.\nexport {\n createLocaleRegistry,\n localeContributionsFromGlob,\n type LocaleContribution,\n type LocaleContributionsFromGlobOptions,\n type LocaleGlobModule,\n type LocaleRegistry,\n type LocaleRegistryOptions,\n} from './locale-registry.js';\nexport {\n createI18nFromRegistry,\n type CreateI18nFromRegistryConfig,\n} from './create-i18n-from-registry.js';\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -5,7 +5,15 @@ interface I18nAdapter {
5
5
  changeLocale: (locale: string) => Promise<void>;
6
6
  loadNamespace: (namespace: string, locale: string) => Promise<void>;
7
7
  getCurrentLocale: () => string;
8
- hasKey: (key: string) => boolean;
8
+ /**
9
+ * True when a USABLE STRING exists at `key` (an object node is not one).
10
+ * Pass `count` to ask about a count family (`<key>_one | _few | _many |
11
+ * _other`): i18next resolves the suffix only when it is asked with a count,
12
+ * so without one a present family reports missing.
13
+ */
14
+ hasKey: (key: string, options?: {
15
+ count?: number;
16
+ }) => boolean;
9
17
  /**
10
18
  * Subscribe to active-locale changes (fired by {@link I18nAdapter.changeLocale}).
11
19
  * Returns an unsubscribe fn. Optional — adapters that cannot notify omit it,
@@ -68,8 +76,8 @@ interface I18nKeysWithParams {}
68
76
  * 3. common.<key> — global common
69
77
  * 4. <key> — raw fallback (debug only)
70
78
  *
71
- * If `key` is already namespaced (starts with `resources.`, `enums.` or
72
- * `common.`), return it as-is without further scoping.
79
+ * If `key` is already namespaced (starts with `resources.`, `enums.`,
80
+ * `common.` or `blocks.`), return it as-is without further scoping.
73
81
  */
74
82
  declare function resolveKey(key: string, resource?: string): string[];
75
83
  //#endregion
@@ -99,18 +107,26 @@ declare function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18
99
107
  //#endregion
100
108
  //#region src/i18n.d.ts
101
109
  interface I18nFacade {
110
+ /**
111
+ * Translate `key` through the candidate chain. `count` selects a count
112
+ * family's form (`<key>_one | _few | _many | _other`, by the locale's CLDR
113
+ * rules) and interpolates `{{count}}`; a family asked without a count is
114
+ * missing. A `count` inside `params` only interpolates.
115
+ */
102
116
  t: (key: string, options?: {
103
117
  resource?: string;
104
118
  params?: Record<string, unknown>;
119
+ count?: number;
105
120
  }) => string;
106
121
  /**
107
122
  * True when a translation for `key` EXISTS (through the same candidate chain
108
123
  * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a
109
124
  * miss — the content model probes presence with `has` to tell "no bundle
110
- * entry" (render nothing) from "bundle entry === key text".
125
+ * entry" (render nothing) from "bundle entry === key text". Pass `count` to probe a count family.
111
126
  */
112
127
  has: (key: string, options?: {
113
128
  resource?: string;
129
+ count?: number;
114
130
  }) => boolean;
115
131
  changeLocale: (locale: string) => Promise<void>;
116
132
  getCurrentLocale: () => string;
@@ -122,11 +138,20 @@ interface I18nFacade {
122
138
  */
123
139
  onLocaleChange: (cb: () => void) => () => void;
124
140
  loadNamespace: (namespace: string, locale: string) => Promise<void>;
141
+ /**
142
+ * The locale codes this app can switch to. Optional: a facade built without
143
+ * a list (a hand-rolled one, a test double) simply does not say, and a
144
+ * language control then offers only the active locale. `createI18nFromRegistry`
145
+ * fills it from the registry.
146
+ */
147
+ locales?: () => readonly string[];
125
148
  adapter: I18nAdapter;
126
149
  }
127
150
  interface CreateI18nConfig {
128
151
  adapter: I18nAdapter;
129
152
  warnOnMissing?: boolean;
153
+ /** The switchable locale codes, exposed as {@link I18nFacade.locales}. */
154
+ locales?: () => readonly string[];
130
155
  }
131
156
  declare function createI18n(config: CreateI18nConfig): I18nFacade;
132
157
  //#endregion
@@ -135,6 +160,7 @@ interface ResourceI18n {
135
160
  t: (key: string, options?: {
136
161
  resource?: string;
137
162
  params?: Record<string, unknown>;
163
+ count?: number;
138
164
  }) => string;
139
165
  changeLocale: (locale: string) => Promise<void>;
140
166
  getCurrentLocale: () => string;
@@ -185,6 +211,15 @@ interface LocaleContribution {
185
211
  bundle: TranslationBundle;
186
212
  }
187
213
  interface LocaleRegistry {
214
+ /**
215
+ * The primary locale, as declared to {@link createLocaleRegistry}: the one
216
+ * whose text is the default in code, and the locale every other one falls
217
+ * back to. The single source of truth for it — `createI18nFromRegistry`
218
+ * defaults both the starting locale and the fallback to it, and
219
+ * `@burojs/testing`'s `checkApp` reads it to know which locale is exempt
220
+ * from `i18n.missing`. Never derived from contributions or their order.
221
+ */
222
+ primary(): string;
188
223
  /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */
189
224
  register(contribution: LocaleContribution): void;
190
225
  /**
@@ -204,10 +239,26 @@ interface LocaleRegistry {
204
239
  * priority order, so a higher-priority contribution's leaves win over a
205
240
  * lower-priority one's on any overlapping key while leaving the rest of
206
241
  * the lower-priority bundle intact.
242
+ *
243
+ * Throws when no contribution is registered for {@link primary}: a
244
+ * fallback chain that ends at a locale with no bundle, and a primary that
245
+ * `locales()` (and so every language switcher built from it) does not
246
+ * list, is a misconfiguration (usually a typo, `'en-US'` for `'en'`, or a
247
+ * glob that missed its file), not a state to render. An app that really
248
+ * has no strings for its primary locale registers an empty bundle for it.
207
249
  */
208
250
  buildResources(): Record<string, Record<string, TranslationBundle>>;
209
251
  }
210
- declare function createLocaleRegistry(): LocaleRegistry;
252
+ interface LocaleRegistryOptions {
253
+ /**
254
+ * The primary locale: the one whose text is the default in code. Required,
255
+ * and declared here rather than inferred, because nothing in the
256
+ * contributions says which locale it is — registration order and the
257
+ * sorted `locales()` both carry no meaning.
258
+ */
259
+ primary: string;
260
+ }
261
+ declare function createLocaleRegistry(options: LocaleRegistryOptions): LocaleRegistry;
211
262
  /**
212
263
  * A bundler-produced glob module, as `import.meta.glob` (Vite) or an
213
264
  * equivalent walk over `require.context` (webpack) would yield it: a flat
@@ -248,22 +299,28 @@ interface LocaleContributionsFromGlobOptions {
248
299
  declare function localeContributionsFromGlob(glob: Record<string, LocaleGlobModule>, options: LocaleContributionsFromGlobOptions): LocaleContribution[];
249
300
  //#endregion
250
301
  //#region src/create-i18n-from-registry.d.ts
251
- type CreateI18nFromRegistryConfig = Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'> & {
302
+ type CreateI18nFromRegistryConfig = Partial<Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'>> & {
252
303
  warnOnMissing?: boolean;
253
304
  };
254
305
  /**
255
306
  * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +
256
307
  * `createI18n`: `registry.locales()` becomes the adapter's `locales`,
257
- * `registry.buildResources()` becomes its `resources`. This is the ONLY
258
- * place those two need to be threaded together — an app that builds its own
259
- * registry never re-lists its locale codes to hand them to the adapter, it
260
- * just calls this.
308
+ * `registry.buildResources()` becomes its `resources`, and
309
+ * `registry.primary()` is both the starting locale and the fallback unless
310
+ * `defaultLocale` (the starting locale, say a stored choice) or
311
+ * `fallbackLocale` (say a per-locale chain) overrides it. This is the ONLY
312
+ * place those need to be threaded together — an app that builds its own
313
+ * registry never re-lists its locale codes, or restates its primary locale,
314
+ * to hand them to the adapter; it just calls this.
315
+ *
316
+ * Throws when the registry's primary locale has no contribution (see
317
+ * {@link LocaleRegistry.buildResources}).
261
318
  */
262
- declare function createI18nFromRegistry(registry: LocaleRegistry, config: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
319
+ declare function createI18nFromRegistry(registry: LocaleRegistry, config?: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
263
320
  //#endregion
264
321
  //#region src/index.d.ts
265
322
  declare const PACKAGE_NAME: "@burojs/i18n-core";
266
323
  declare const PACKAGE_VERSION: "0.0.0";
267
324
  //#endregion
268
- export { type CreateI18nConfig, type CreateI18nFromRegistryConfig, type I18nAdapter, type I18nFacade, type I18nKeys, type I18nKeysWithParams, type I18nextAdapterConfig, type LocaleContribution, type LocaleContributionsFromGlobOptions, type LocaleGlobModule, type LocaleRegistry, PACKAGE_NAME, PACKAGE_VERSION, type ResourceBundle, type ResourceI18n, type TranslationBundle, createI18n, createI18nFromRegistry, createI18nextAdapter, createLocaleRegistry, createResourceI18n, localeContributionsFromGlob, resolveKey };
325
+ export { type CreateI18nConfig, type CreateI18nFromRegistryConfig, type I18nAdapter, type I18nFacade, type I18nKeys, type I18nKeysWithParams, type I18nextAdapterConfig, type LocaleContribution, type LocaleContributionsFromGlobOptions, type LocaleGlobModule, type LocaleRegistry, type LocaleRegistryOptions, PACKAGE_NAME, PACKAGE_VERSION, type ResourceBundle, type ResourceI18n, type TranslationBundle, createI18n, createI18nFromRegistry, createI18nextAdapter, createLocaleRegistry, createResourceI18n, localeContributionsFromGlob, resolveKey };
269
326
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -5,7 +5,15 @@ interface I18nAdapter {
5
5
  changeLocale: (locale: string) => Promise<void>;
6
6
  loadNamespace: (namespace: string, locale: string) => Promise<void>;
7
7
  getCurrentLocale: () => string;
8
- hasKey: (key: string) => boolean;
8
+ /**
9
+ * True when a USABLE STRING exists at `key` (an object node is not one).
10
+ * Pass `count` to ask about a count family (`<key>_one | _few | _many |
11
+ * _other`): i18next resolves the suffix only when it is asked with a count,
12
+ * so without one a present family reports missing.
13
+ */
14
+ hasKey: (key: string, options?: {
15
+ count?: number;
16
+ }) => boolean;
9
17
  /**
10
18
  * Subscribe to active-locale changes (fired by {@link I18nAdapter.changeLocale}).
11
19
  * Returns an unsubscribe fn. Optional — adapters that cannot notify omit it,
@@ -68,8 +76,8 @@ interface I18nKeysWithParams {}
68
76
  * 3. common.<key> — global common
69
77
  * 4. <key> — raw fallback (debug only)
70
78
  *
71
- * If `key` is already namespaced (starts with `resources.`, `enums.` or
72
- * `common.`), return it as-is without further scoping.
79
+ * If `key` is already namespaced (starts with `resources.`, `enums.`,
80
+ * `common.` or `blocks.`), return it as-is without further scoping.
73
81
  */
74
82
  declare function resolveKey(key: string, resource?: string): string[];
75
83
  //#endregion
@@ -99,18 +107,26 @@ declare function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18
99
107
  //#endregion
100
108
  //#region src/i18n.d.ts
101
109
  interface I18nFacade {
110
+ /**
111
+ * Translate `key` through the candidate chain. `count` selects a count
112
+ * family's form (`<key>_one | _few | _many | _other`, by the locale's CLDR
113
+ * rules) and interpolates `{{count}}`; a family asked without a count is
114
+ * missing. A `count` inside `params` only interpolates.
115
+ */
102
116
  t: (key: string, options?: {
103
117
  resource?: string;
104
118
  params?: Record<string, unknown>;
119
+ count?: number;
105
120
  }) => string;
106
121
  /**
107
122
  * True when a translation for `key` EXISTS (through the same candidate chain
108
123
  * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a
109
124
  * miss — the content model probes presence with `has` to tell "no bundle
110
- * entry" (render nothing) from "bundle entry === key text".
125
+ * entry" (render nothing) from "bundle entry === key text". Pass `count` to probe a count family.
111
126
  */
112
127
  has: (key: string, options?: {
113
128
  resource?: string;
129
+ count?: number;
114
130
  }) => boolean;
115
131
  changeLocale: (locale: string) => Promise<void>;
116
132
  getCurrentLocale: () => string;
@@ -122,11 +138,20 @@ interface I18nFacade {
122
138
  */
123
139
  onLocaleChange: (cb: () => void) => () => void;
124
140
  loadNamespace: (namespace: string, locale: string) => Promise<void>;
141
+ /**
142
+ * The locale codes this app can switch to. Optional: a facade built without
143
+ * a list (a hand-rolled one, a test double) simply does not say, and a
144
+ * language control then offers only the active locale. `createI18nFromRegistry`
145
+ * fills it from the registry.
146
+ */
147
+ locales?: () => readonly string[];
125
148
  adapter: I18nAdapter;
126
149
  }
127
150
  interface CreateI18nConfig {
128
151
  adapter: I18nAdapter;
129
152
  warnOnMissing?: boolean;
153
+ /** The switchable locale codes, exposed as {@link I18nFacade.locales}. */
154
+ locales?: () => readonly string[];
130
155
  }
131
156
  declare function createI18n(config: CreateI18nConfig): I18nFacade;
132
157
  //#endregion
@@ -135,6 +160,7 @@ interface ResourceI18n {
135
160
  t: (key: string, options?: {
136
161
  resource?: string;
137
162
  params?: Record<string, unknown>;
163
+ count?: number;
138
164
  }) => string;
139
165
  changeLocale: (locale: string) => Promise<void>;
140
166
  getCurrentLocale: () => string;
@@ -185,6 +211,15 @@ interface LocaleContribution {
185
211
  bundle: TranslationBundle;
186
212
  }
187
213
  interface LocaleRegistry {
214
+ /**
215
+ * The primary locale, as declared to {@link createLocaleRegistry}: the one
216
+ * whose text is the default in code, and the locale every other one falls
217
+ * back to. The single source of truth for it — `createI18nFromRegistry`
218
+ * defaults both the starting locale and the fallback to it, and
219
+ * `@burojs/testing`'s `checkApp` reads it to know which locale is exempt
220
+ * from `i18n.missing`. Never derived from contributions or their order.
221
+ */
222
+ primary(): string;
188
223
  /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */
189
224
  register(contribution: LocaleContribution): void;
190
225
  /**
@@ -204,10 +239,26 @@ interface LocaleRegistry {
204
239
  * priority order, so a higher-priority contribution's leaves win over a
205
240
  * lower-priority one's on any overlapping key while leaving the rest of
206
241
  * the lower-priority bundle intact.
242
+ *
243
+ * Throws when no contribution is registered for {@link primary}: a
244
+ * fallback chain that ends at a locale with no bundle, and a primary that
245
+ * `locales()` (and so every language switcher built from it) does not
246
+ * list, is a misconfiguration (usually a typo, `'en-US'` for `'en'`, or a
247
+ * glob that missed its file), not a state to render. An app that really
248
+ * has no strings for its primary locale registers an empty bundle for it.
207
249
  */
208
250
  buildResources(): Record<string, Record<string, TranslationBundle>>;
209
251
  }
210
- declare function createLocaleRegistry(): LocaleRegistry;
252
+ interface LocaleRegistryOptions {
253
+ /**
254
+ * The primary locale: the one whose text is the default in code. Required,
255
+ * and declared here rather than inferred, because nothing in the
256
+ * contributions says which locale it is — registration order and the
257
+ * sorted `locales()` both carry no meaning.
258
+ */
259
+ primary: string;
260
+ }
261
+ declare function createLocaleRegistry(options: LocaleRegistryOptions): LocaleRegistry;
211
262
  /**
212
263
  * A bundler-produced glob module, as `import.meta.glob` (Vite) or an
213
264
  * equivalent walk over `require.context` (webpack) would yield it: a flat
@@ -248,22 +299,28 @@ interface LocaleContributionsFromGlobOptions {
248
299
  declare function localeContributionsFromGlob(glob: Record<string, LocaleGlobModule>, options: LocaleContributionsFromGlobOptions): LocaleContribution[];
249
300
  //#endregion
250
301
  //#region src/create-i18n-from-registry.d.ts
251
- type CreateI18nFromRegistryConfig = Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'> & {
302
+ type CreateI18nFromRegistryConfig = Partial<Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'>> & {
252
303
  warnOnMissing?: boolean;
253
304
  };
254
305
  /**
255
306
  * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +
256
307
  * `createI18n`: `registry.locales()` becomes the adapter's `locales`,
257
- * `registry.buildResources()` becomes its `resources`. This is the ONLY
258
- * place those two need to be threaded together — an app that builds its own
259
- * registry never re-lists its locale codes to hand them to the adapter, it
260
- * just calls this.
308
+ * `registry.buildResources()` becomes its `resources`, and
309
+ * `registry.primary()` is both the starting locale and the fallback unless
310
+ * `defaultLocale` (the starting locale, say a stored choice) or
311
+ * `fallbackLocale` (say a per-locale chain) overrides it. This is the ONLY
312
+ * place those need to be threaded together — an app that builds its own
313
+ * registry never re-lists its locale codes, or restates its primary locale,
314
+ * to hand them to the adapter; it just calls this.
315
+ *
316
+ * Throws when the registry's primary locale has no contribution (see
317
+ * {@link LocaleRegistry.buildResources}).
261
318
  */
262
- declare function createI18nFromRegistry(registry: LocaleRegistry, config: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
319
+ declare function createI18nFromRegistry(registry: LocaleRegistry, config?: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
263
320
  //#endregion
264
321
  //#region src/index.d.ts
265
322
  declare const PACKAGE_NAME: "@burojs/i18n-core";
266
323
  declare const PACKAGE_VERSION: "0.0.0";
267
324
  //#endregion
268
- export { type CreateI18nConfig, type CreateI18nFromRegistryConfig, type I18nAdapter, type I18nFacade, type I18nKeys, type I18nKeysWithParams, type I18nextAdapterConfig, type LocaleContribution, type LocaleContributionsFromGlobOptions, type LocaleGlobModule, type LocaleRegistry, PACKAGE_NAME, PACKAGE_VERSION, type ResourceBundle, type ResourceI18n, type TranslationBundle, createI18n, createI18nFromRegistry, createI18nextAdapter, createLocaleRegistry, createResourceI18n, localeContributionsFromGlob, resolveKey };
325
+ export { type CreateI18nConfig, type CreateI18nFromRegistryConfig, type I18nAdapter, type I18nFacade, type I18nKeys, type I18nKeysWithParams, type I18nextAdapterConfig, type LocaleContribution, type LocaleContributionsFromGlobOptions, type LocaleGlobModule, type LocaleRegistry, type LocaleRegistryOptions, PACKAGE_NAME, PACKAGE_VERSION, type ResourceBundle, type ResourceI18n, type TranslationBundle, createI18n, createI18nFromRegistry, createI18nextAdapter, createLocaleRegistry, createResourceI18n, localeContributionsFromGlob, resolveKey };
269
326
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import i18next from "i18next";
2
2
  //#region src/lookup.ts
3
3
  function resolveKey(key, resource) {
4
4
  if (!key) return [];
5
- if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.")) return [key];
5
+ if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.") || key.startsWith("blocks.")) return [key];
6
6
  const candidates = [];
7
7
  if (resource) candidates.push(`resources.${resource}.${key}`);
8
8
  candidates.push(`resources._.${key}`);
@@ -12,6 +12,7 @@ function resolveKey(key, resource) {
12
12
  }
13
13
  //#endregion
14
14
  //#region src/i18next-adapter.ts
15
+ const MISSING = "\0";
15
16
  async function createI18nextAdapter(config) {
16
17
  const instance = i18next.createInstance();
17
18
  await instance.init({
@@ -23,7 +24,8 @@ async function createI18nextAdapter(config) {
23
24
  nsSeparator: config.nsSeparator ?? false,
24
25
  defaultNS: "common",
25
26
  interpolation: { escapeValue: false },
26
- returnNull: false
27
+ returnNull: false,
28
+ returnEmptyString: false
27
29
  });
28
30
  return {
29
31
  t(key, params) {
@@ -35,7 +37,11 @@ async function createI18nextAdapter(config) {
35
37
  ns,
36
38
  ...params ?? {}
37
39
  });
38
- if (result === subKey && !instance.exists(subKey, { ns })) return key;
40
+ const count = typeof params?.count === "number" ? params.count : void 0;
41
+ if (result === subKey && !instance.exists(subKey, count === void 0 ? { ns } : {
42
+ ns,
43
+ count
44
+ })) return key;
39
45
  return result;
40
46
  },
41
47
  async changeLocale(locale) {
@@ -51,33 +57,42 @@ async function createI18nextAdapter(config) {
51
57
  instance.on("languageChanged", cb);
52
58
  return () => instance.off("languageChanged", cb);
53
59
  },
54
- hasKey(key) {
60
+ hasKey(key, options) {
55
61
  const dotIdx = key.indexOf(".");
56
62
  const ns = dotIdx === -1 ? void 0 : key.substring(0, dotIdx);
57
63
  const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);
58
- const opts = ns === void 0 ? void 0 : { ns };
59
- if (!instance.exists(subKey, opts)) return false;
64
+ const lookup = {
65
+ ...ns === void 0 ? {} : { ns },
66
+ ...options?.count === void 0 ? {} : { count: options.count }
67
+ };
60
68
  const value = instance.t(subKey, {
61
- ...opts ?? {},
62
- returnObjects: true
69
+ ...lookup,
70
+ returnObjects: true,
71
+ defaultValue: MISSING
63
72
  });
64
- return typeof value !== "object" || value === null;
73
+ return value !== MISSING && (typeof value !== "object" || value === null);
65
74
  }
66
75
  };
67
76
  }
68
77
  //#endregion
69
78
  //#region src/i18n.ts
70
79
  function createI18n(config) {
71
- const { adapter, warnOnMissing = true } = config;
80
+ const { adapter, warnOnMissing = true, locales } = config;
72
81
  return {
73
82
  t(key, options) {
83
+ const count = options?.count;
84
+ const params = count === void 0 ? options?.params : {
85
+ ...options?.params,
86
+ count
87
+ };
74
88
  const candidates = resolveKey(key, options?.resource);
75
- for (const candidate of candidates) if (adapter.hasKey(candidate)) return adapter.t(candidate, options?.params);
89
+ for (const candidate of candidates) if (count === void 0 ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count })) return adapter.t(candidate, params);
76
90
  if (warnOnMissing && typeof console !== "undefined") console.warn(`[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ""}`);
77
91
  return key;
78
92
  },
79
93
  has(key, options) {
80
- return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));
94
+ const count = options?.count;
95
+ return resolveKey(key, options?.resource).some((candidate) => count === void 0 ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count }));
81
96
  },
82
97
  async changeLocale(locale) {
83
98
  await adapter.changeLocale(locale);
@@ -91,6 +106,7 @@ function createI18n(config) {
91
106
  async loadNamespace(namespace, locale) {
92
107
  await adapter.loadNamespace(namespace, locale);
93
108
  },
109
+ ...locales === void 0 ? {} : { locales },
94
110
  adapter
95
111
  };
96
112
  }
@@ -101,7 +117,8 @@ function createResourceI18n(i18n, resource) {
101
117
  t(key, options) {
102
118
  return i18n.t(key, {
103
119
  resource: options?.resource ?? resource,
104
- params: options?.params
120
+ params: options?.params,
121
+ ...options?.count === void 0 ? {} : { count: options.count }
105
122
  });
106
123
  },
107
124
  changeLocale: i18n.changeLocale,
@@ -123,9 +140,14 @@ function mergeBundle(base, overlay) {
123
140
  }
124
141
  return out;
125
142
  }
126
- function createLocaleRegistry() {
143
+ function createLocaleRegistry(options) {
144
+ const primary = options?.primary;
145
+ if (typeof primary !== "string" || primary === "") throw new TypeError(`createLocaleRegistry needs { primary }, the locale whose text is the default in code; received ${JSON.stringify(primary)}.`);
127
146
  const registered = [];
128
147
  return {
148
+ primary() {
149
+ return primary;
150
+ },
129
151
  register(contribution) {
130
152
  registered.push(contribution);
131
153
  },
@@ -139,6 +161,10 @@ function createLocaleRegistry() {
139
161
  return registered;
140
162
  },
141
163
  buildResources() {
164
+ if (!registered.some((c) => c.locale === primary)) {
165
+ const known = Array.from(new Set(registered.map((c) => c.locale))).sort();
166
+ throw new Error(`The locale registry's primary locale ${JSON.stringify(primary)} has no contribution (registered: ${known.length === 0 ? "none" : known.join(", ")}). Register its bundle, an empty one if the app has no strings of its own, or declare the primary it does register.`);
167
+ }
142
168
  const byLocale = new Map();
143
169
  for (const contribution of registered) {
144
170
  const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;
@@ -189,17 +215,20 @@ function localeContributionsFromGlob(glob, options) {
189
215
  }
190
216
  //#endregion
191
217
  //#region src/create-i18n-from-registry.ts
192
- async function createI18nFromRegistry(registry, config) {
218
+ async function createI18nFromRegistry(registry, config = {}) {
219
+ const resources = registry.buildResources();
220
+ const primary = registry.primary();
193
221
  return createI18n({
194
222
  adapter: await createI18nextAdapter({
195
- defaultLocale: config.defaultLocale,
223
+ defaultLocale: config.defaultLocale ?? primary,
196
224
  locales: registry.locales(),
197
- resources: registry.buildResources(),
198
- ...config.fallbackLocale !== void 0 ? { fallbackLocale: config.fallbackLocale } : {},
225
+ resources,
226
+ fallbackLocale: config.fallbackLocale ?? primary,
199
227
  ...config.keySeparator !== void 0 ? { keySeparator: config.keySeparator } : {},
200
228
  ...config.nsSeparator !== void 0 ? { nsSeparator: config.nsSeparator } : {}
201
229
  }),
202
- warnOnMissing: config.warnOnMissing
230
+ warnOnMissing: config.warnOnMissing,
231
+ locales: () => registry.locales()
203
232
  });
204
233
  }
205
234
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/lookup.ts","../src/i18next-adapter.ts","../src/i18n.ts","../src/resource-context.ts","../src/locale-registry.ts","../src/create-i18n-from-registry.ts","../src/index.ts"],"sourcesContent":["/**\n * Build candidate keys for fallback lookup.\n *\n * Lookup order (first match wins):\n * 1. resources.<name>.<key> — resource-specific\n * 2. resources._.<key> — base for all resources\n * 3. common.<key> — global common\n * 4. <key> — raw fallback (debug only)\n *\n * If `key` is already namespaced (starts with `resources.`, `enums.` or\n * `common.`), return it as-is without further scoping.\n */\nexport function resolveKey(key: string, resource?: string): string[] {\n if (!key) return [];\n\n // Already namespaced — return as-is. `enums.` is a first-class top-level\n // namespace (global enum labels), like `resources.` and `common.`.\n if (key.startsWith('resources.') || key.startsWith('enums.') || key.startsWith('common.')) {\n return [key];\n }\n\n const candidates: string[] = [];\n if (resource) {\n candidates.push(`resources.${resource}.${key}`);\n }\n candidates.push(`resources._.${key}`);\n candidates.push(`common.${key}`);\n candidates.push(key); // raw fallback for warnings\n return candidates;\n}\n","import i18next, { type FallbackLng, type i18n } from 'i18next';\nimport type { I18nAdapter, TranslationBundle } from './types/index.js';\n\nexport interface I18nextAdapterConfig {\n defaultLocale: string;\n locales: string[];\n resources?: Record<string, Record<string, TranslationBundle>>;\n /**\n * A single fallback locale (`'en'`) applies uniformly to every locale. A\n * `fr → en` fallback for one locale but not another needs i18next's own\n * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so\n * this accepts i18next's full `FallbackLng` type rather than just `string`.\n * The chain is entirely CONFIG: nothing in this adapter special-cases any\n * particular locale code, here or anywhere else.\n */\n fallbackLocale?: FallbackLng;\n keySeparator?: string | false;\n nsSeparator?: string | false;\n}\n\n/**\n * Default I18nAdapter implementation backed by i18next.\n * Each top-level key in resources becomes a separate i18next namespace\n * (e.g. 'common', 'resources').\n */\nexport async function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter> {\n const instance: i18n = i18next.createInstance();\n\n await instance.init({\n lng: config.defaultLocale,\n fallbackLng: config.fallbackLocale ?? config.defaultLocale,\n supportedLngs: config.locales,\n resources: config.resources,\n keySeparator: config.keySeparator ?? '.',\n // Qualified Buro entity refs use `namespace:id`. Resource keys flow\n // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace\n // separator would split a valid ref before our explicit `ns` routing gets\n // to resolve it.\n nsSeparator: config.nsSeparator ?? false,\n defaultNS: 'common',\n interpolation: { escapeValue: false },\n returnNull: false,\n });\n\n return {\n t(key, params) {\n const dotIdx = key.indexOf('.');\n if (dotIdx === -1) {\n return instance.t(key, params) as string;\n }\n const ns = key.substring(0, dotIdx);\n const subKey = key.substring(dotIdx + 1);\n const result = instance.t(subKey, { ns, ...(params ?? {}) }) as string;\n // i18next returns the bare subKey when missing — restore the full key as fallback\n if (result === subKey && !instance.exists(subKey, { ns })) {\n return key;\n }\n return result;\n },\n async changeLocale(locale) {\n await instance.changeLanguage(locale);\n },\n async loadNamespace(namespace, _locale) {\n await instance.loadNamespaces(namespace);\n },\n getCurrentLocale() {\n return instance.language;\n },\n onLocaleChange(cb) {\n instance.on('languageChanged', cb);\n return () => instance.off('languageChanged', cb);\n },\n hasKey(key) {\n const dotIdx = key.indexOf('.');\n const ns = dotIdx === -1 ? undefined : key.substring(0, dotIdx);\n const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);\n const opts = ns === undefined ? undefined : { ns };\n if (!instance.exists(subKey, opts)) return false;\n /*\n `exists()` alone is not enough, and the gap is the root cause of a whole\n family of bugs rather than a curiosity.\n\n i18next answers `exists()` with TRUE for an OBJECT node — `fields.gtin`\n once that entry is promoted from a plain string to `{ label, doc }`,\n `enums.<ns>.<value>` when it carries a `doc` but no `label`, and so on.\n `t()` on such a node neither throws nor returns the key: it returns the\n literal sentence\n\n key '…' returned an object instead of string.\n\n Since `resolveContentSlot` commits to the FIRST key whose `has()` is\n true, every resolver built on it — field labels, enum labels, nav\n labels, tab captions — would render that sentence to the user. No crash,\n no warning, just wrong text on screen. It was found three separate times\n in three separate resolvers before being fixed here, once.\n\n So `has` now means what its callers always assumed: there is a USABLE\n STRING at this key. Non-object primitives stay truthy, leaving plural\n forms and numeric values behaving exactly as before.\n */\n const value = instance.t(subKey, { ...(opts ?? {}), returnObjects: true }) as unknown;\n return typeof value !== 'object' || value === null;\n },\n };\n}\n","import { resolveKey } from './lookup.js';\nimport type { I18nAdapter } from './types/index.js';\n\nexport interface I18nFacade {\n t: (key: string, options?: { resource?: string; params?: Record<string, unknown> }) => string;\n /**\n * True when a translation for `key` EXISTS (through the same candidate chain\n * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a\n * miss — the content model probes presence with `has` to tell \"no bundle\n * entry\" (render nothing) from \"bundle entry === key text\".\n */\n has: (key: string, options?: { resource?: string }) => boolean;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n /**\n * Subscribe to active-locale changes. Returns an unsubscribe fn. When the\n * adapter can't notify, this is a no-op subscription (returns an unsubscribe\n * that does nothing) — the live language switch then falls back to a static\n * read. Consumed by `useLocale()` via `useSyncExternalStore`.\n */\n onLocaleChange: (cb: () => void) => () => void;\n loadNamespace: (namespace: string, locale: string) => Promise<void>;\n adapter: I18nAdapter;\n}\n\nexport interface CreateI18nConfig {\n adapter: I18nAdapter;\n warnOnMissing?: boolean;\n}\n\nexport function createI18n(config: CreateI18nConfig): I18nFacade {\n const { adapter, warnOnMissing = true } = config;\n\n return {\n t(key, options) {\n const candidates = resolveKey(key, options?.resource);\n for (const candidate of candidates) {\n if (adapter.hasKey(candidate)) {\n return adapter.t(candidate, options?.params);\n }\n }\n if (warnOnMissing && typeof console !== 'undefined') {\n console.warn(\n `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ''}`,\n );\n }\n return key;\n },\n has(key, options) {\n return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));\n },\n async changeLocale(locale) {\n await adapter.changeLocale(locale);\n },\n getCurrentLocale() {\n return adapter.getCurrentLocale();\n },\n onLocaleChange(cb) {\n return adapter.onLocaleChange?.(cb) ?? (() => {});\n },\n async loadNamespace(namespace, locale) {\n await adapter.loadNamespace(namespace, locale);\n },\n adapter,\n };\n}\n","import type { I18nFacade } from './i18n.js';\n\nexport interface ResourceI18n {\n t: (key: string, options?: { resource?: string; params?: Record<string, unknown> }) => string;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n resource: string;\n}\n\n/**\n * Wraps an I18nFacade with a default resource scope.\n * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.\n * Pass an explicit `resource` option to override the default scope.\n */\nexport function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n {\n return {\n t(key, options) {\n return i18n.t(key, {\n resource: options?.resource ?? resource,\n params: options?.params,\n });\n },\n changeLocale: i18n.changeLocale,\n getCurrentLocale: i18n.getCurrentLocale,\n resource,\n };\n}\n","import type { TranslationBundle } from './types/bundle.js';\n\n/**\n * One package/app/domain module's offering for one locale.\n *\n * A contribution is data, not code: nothing about `createI18nextAdapter`'s\n * `resources` shape is visible here, and nothing here names a fixed set of\n * locale codes or counts them. `locale` is whatever the file/module that\n * produced this contribution says it is — the registry never validates it\n * against a known list, because there is no known list. That is the whole\n * point: a twenty-first language is a twenty-first contribution, not a\n * twenty-first `case`.\n */\nexport interface LocaleContribution {\n /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */\n locale: string;\n /**\n * i18next namespace the bundle merges into. Defaults to `'common'` — the\n * same default namespace `createI18nextAdapter` uses.\n */\n namespace?: string;\n /**\n * Declared merge priority. On a key collision within the same\n * (locale, namespace), the contribution with the HIGHER priority wins —\n * not whichever happened to register last. Ties fall back to registration\n * order (last registered wins), which is why two genuinely competing\n * sources must declare distinct priorities rather than relying on the tie\n * break.\n */\n priority: number;\n /**\n * Where this contribution came from — a package name, `'app'`, an enum\n * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}\n * for debugging a collision); never branched on.\n */\n source: string;\n bundle: TranslationBundle;\n}\n\nexport interface LocaleRegistry {\n /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */\n register(contribution: LocaleContribution): void;\n /**\n * Every locale code that has at least one contribution, sorted. Derived\n * purely from what was registered — nothing enumerates or counts locales\n * anywhere else in this module.\n */\n locales(): string[];\n /** Every namespace that has at least one contribution, sorted. */\n namespaces(): string[];\n /** All registered contributions, in registration order. For diagnostics/tests. */\n contributions(): readonly LocaleContribution[];\n /**\n * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to\n * pass straight as `createI18nextAdapter`'s `resources` config. Within one\n * (locale, namespace) group, contributions are deep-merged in ascending\n * priority order, so a higher-priority contribution's leaves win over a\n * lower-priority one's on any overlapping key while leaving the rest of\n * the lower-priority bundle intact.\n */\n buildResources(): Record<string, Record<string, TranslationBundle>>;\n}\n\nconst DEFAULT_NAMESPACE = 'common';\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursively merge `overlay` onto `base`. Arrays and primitives are replaced\n * wholesale (a `TranslationBundle` array leaf — see that type's own comment\n * on why arrays are a real leaf shape — is not index-merged: the whole array\n * is the unit an overlay replaces). Plain objects merge key by key.\n */\nfunction mergeBundle(base: TranslationBundle, overlay: TranslationBundle): TranslationBundle {\n const out: Record<string, unknown> = { ...base };\n for (const [key, overlayValue] of Object.entries(overlay)) {\n const baseValue = out[key];\n out[key] =\n isPlainObject(baseValue) && isPlainObject(overlayValue)\n ? mergeBundle(baseValue as TranslationBundle, overlayValue as TranslationBundle)\n : overlayValue;\n }\n return out as TranslationBundle;\n}\n\nexport function createLocaleRegistry(): LocaleRegistry {\n const registered: LocaleContribution[] = [];\n\n return {\n register(contribution) {\n registered.push(contribution);\n },\n locales() {\n return Array.from(new Set(registered.map((c) => c.locale))).sort();\n },\n namespaces() {\n return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();\n },\n contributions() {\n return registered;\n },\n buildResources() {\n // Group by (locale, namespace) via NESTED maps rather than a\n // delimiter-joined composite string key. A locale or namespace code is\n // caller-supplied and unconstrained (see `LocaleContribution.locale`'s\n // own comment), so any single-character delimiter is a value that\n // *could* collide with real content; nested `Map`s need no delimiter\n // at all, so there is nothing to collide with and nothing unusual\n // embedded in this file's source for a diff tool to trip over.\n const byLocale = new Map<string, Map<string, LocaleContribution[]>>();\n for (const contribution of registered) {\n const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;\n let byNamespace = byLocale.get(contribution.locale);\n if (!byNamespace) {\n byNamespace = new Map<string, LocaleContribution[]>();\n byLocale.set(contribution.locale, byNamespace);\n }\n const group = byNamespace.get(namespace);\n if (group) {\n group.push(contribution);\n } else {\n byNamespace.set(namespace, [contribution]);\n }\n }\n\n const resources: Record<string, Record<string, TranslationBundle>> = {};\n for (const [locale, byNamespace] of byLocale) {\n const namespaces: Record<string, TranslationBundle> = {};\n for (const [namespace, group] of byNamespace) {\n // Stable sort ascending by priority: lower priority applied first,\n // higher priority applied last so it wins the overlapping leaves.\n // `Array#sort` is stable per spec (ES2019+), so contributions with\n // equal priority keep their registration order — last-registered\n // wins ONLY within a tie, never across a declared priority gap.\n const ordered = [...group].sort((a, b) => a.priority - b.priority);\n let merged: TranslationBundle = {};\n for (const contribution of ordered) {\n merged = mergeBundle(merged, contribution.bundle);\n }\n namespaces[namespace] = merged;\n }\n resources[locale] = namespaces;\n }\n return resources;\n },\n };\n}\n\n/**\n * A bundler-produced glob module, as `import.meta.glob` (Vite) or an\n * equivalent walk over `require.context` (webpack) would yield it: a flat\n * map of path → either the loaded module itself or `{ default }`.\n *\n * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s\n * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published\n * package must not require one — the ADOPTER calls `import.meta.glob` (or\n * whatever their bundler offers) and hands the plain object here.\n */\nexport type LocaleGlobModule = TranslationBundle | { default: TranslationBundle };\n\nexport interface LocaleContributionsFromGlobOptions {\n /** {@link LocaleContribution.source} for every contribution produced. */\n source: string;\n /** {@link LocaleContribution.priority} for every contribution produced. */\n priority: number;\n /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */\n namespace?: string;\n /**\n * Extracts the locale code from a glob path. Default matches a bare\n * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`\n * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a\n * known set of codes; whatever the filename says IS the locale code. Paths\n * that don't match are skipped rather than guessed at (same discipline as\n * `docsFromGlob`).\n */\n localeOf?: (path: string) => string | undefined;\n}\n\nconst DEFAULT_LOCALE_PATTERN = /(?:^|\\/)([A-Za-z0-9-]+)\\.(?:ts|tsx|js|jsx|mjs|cjs)$/;\n\nfunction defaultLocaleOf(path: string): string | undefined {\n return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];\n}\n\n/**\n * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.\n * One file → one locale → one contribution. Nothing here enumerates or counts\n * locale codes: whatever files are present in the globbed directory at build\n * time ARE the contributions produced — dropping in a new file is the entire\n * mechanism for adding a language.\n */\nexport function localeContributionsFromGlob(\n glob: Record<string, LocaleGlobModule>,\n options: LocaleContributionsFromGlobOptions,\n): LocaleContribution[] {\n const localeOf = options.localeOf ?? defaultLocaleOf;\n const out: LocaleContribution[] = [];\n for (const [path, mod] of Object.entries(glob)) {\n const locale = localeOf(path);\n if (locale === undefined) continue;\n // Eager `import.meta.glob` yields a module namespace object — always\n // `{ default, ...named exports }` for a file that does `export default\n // {...}`. Unwrap it when present; otherwise trust the glob already\n // handed a bare bundle (e.g. a caller that pre-unwrapped, or a\n // require.context-style walk that returns exports directly).\n const bundle = isPlainObject((mod as { default?: unknown }).default)\n ? (mod as { default: TranslationBundle }).default\n : (mod as TranslationBundle);\n out.push({\n locale,\n priority: options.priority,\n source: options.source,\n bundle,\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n });\n }\n return out;\n}\n","import { type I18nFacade, createI18n } from './i18n.js';\nimport { type I18nextAdapterConfig, createI18nextAdapter } from './i18next-adapter.js';\nimport type { LocaleRegistry } from './locale-registry.js';\n\nexport type CreateI18nFromRegistryConfig = Pick<\n I18nextAdapterConfig,\n 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'\n> & {\n warnOnMissing?: boolean;\n};\n\n/**\n * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +\n * `createI18n`: `registry.locales()` becomes the adapter's `locales`,\n * `registry.buildResources()` becomes its `resources`. This is the ONLY\n * place those two need to be threaded together — an app that builds its own\n * registry never re-lists its locale codes to hand them to the adapter, it\n * just calls this.\n */\nexport async function createI18nFromRegistry(\n registry: LocaleRegistry,\n config: CreateI18nFromRegistryConfig,\n): Promise<I18nFacade> {\n const adapter = await createI18nextAdapter({\n defaultLocale: config.defaultLocale,\n locales: registry.locales(),\n resources: registry.buildResources(),\n ...(config.fallbackLocale !== undefined ? { fallbackLocale: config.fallbackLocale } : {}),\n ...(config.keySeparator !== undefined ? { keySeparator: config.keySeparator } : {}),\n ...(config.nsSeparator !== undefined ? { nsSeparator: config.nsSeparator } : {}),\n });\n return createI18n({ adapter, warnOnMissing: config.warnOnMissing });\n}\n","// Constants\nexport const PACKAGE_NAME = '@burojs/i18n-core' as const;\nexport const PACKAGE_VERSION = '0.0.0' as const;\n\n// Types\nexport type * from './types/index.js';\n\n// Lookup\nexport { resolveKey } from './lookup.js';\n\n// i18next adapter\nexport { createI18nextAdapter, type I18nextAdapterConfig } from './i18next-adapter.js';\n\n// I18n facade\nexport { createI18n, type I18nFacade, type CreateI18nConfig } from './i18n.js';\n\n// Resource context\nexport { createResourceI18n, type ResourceI18n } from './resource-context.js';\n\n// Locale registry — assembling locales from what has REGISTERED (framework\n// packages, the app, domain enum modules) instead of a hand-printed literal.\nexport {\n createLocaleRegistry,\n localeContributionsFromGlob,\n type LocaleContribution,\n type LocaleContributionsFromGlobOptions,\n type LocaleGlobModule,\n type LocaleRegistry,\n} from './locale-registry.js';\nexport {\n createI18nFromRegistry,\n type CreateI18nFromRegistryConfig,\n} from './create-i18n-from-registry.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/lookup.ts","../src/i18next-adapter.ts","../src/i18n.ts","../src/resource-context.ts","../src/locale-registry.ts","../src/create-i18n-from-registry.ts","../src/index.ts"],"sourcesContent":["/**\n * Build candidate keys for fallback lookup.\n *\n * Lookup order (first match wins):\n * 1. resources.<name>.<key> — resource-specific\n * 2. resources._.<key> — base for all resources\n * 3. common.<key> — global common\n * 4. <key> — raw fallback (debug only)\n *\n * If `key` is already namespaced (starts with `resources.`, `enums.`,\n * `common.` or `blocks.`), return it as-is without further scoping.\n */\nexport function resolveKey(key: string, resource?: string): string[] {\n if (!key) return [];\n\n // Already namespaced — return as-is. `enums.` is a first-class top-level\n // namespace (global enum labels), like `resources.` and `common.`; so is\n // `blocks.` (a block view's `t` reads `blocks.<id>.<key>`).\n if (\n key.startsWith('resources.') ||\n key.startsWith('enums.') ||\n key.startsWith('common.') ||\n key.startsWith('blocks.')\n ) {\n return [key];\n }\n\n const candidates: string[] = [];\n if (resource) {\n candidates.push(`resources.${resource}.${key}`);\n }\n candidates.push(`resources._.${key}`);\n candidates.push(`common.${key}`);\n candidates.push(key); // raw fallback for warnings\n return candidates;\n}\n","import i18next, { type FallbackLng, type i18n } from 'i18next';\nimport type { I18nAdapter, TranslationBundle } from './types/index.js';\n\n/** What a lookup answers when no locale in the chain holds a usable value. */\nconst MISSING = '\\u0000';\n\nexport interface I18nextAdapterConfig {\n defaultLocale: string;\n locales: string[];\n resources?: Record<string, Record<string, TranslationBundle>>;\n /**\n * A single fallback locale (`'en'`) applies uniformly to every locale. A\n * `fr → en` fallback for one locale but not another needs i18next's own\n * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so\n * this accepts i18next's full `FallbackLng` type rather than just `string`.\n * The chain is entirely CONFIG: nothing in this adapter special-cases any\n * particular locale code, here or anywhere else.\n */\n fallbackLocale?: FallbackLng;\n keySeparator?: string | false;\n nsSeparator?: string | false;\n}\n\n/**\n * Default I18nAdapter implementation backed by i18next.\n * Each top-level key in resources becomes a separate i18next namespace\n * (e.g. 'common', 'resources').\n */\nexport async function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter> {\n const instance: i18n = i18next.createInstance();\n\n await instance.init({\n lng: config.defaultLocale,\n fallbackLng: config.fallbackLocale ?? config.defaultLocale,\n supportedLngs: config.locales,\n resources: config.resources,\n keySeparator: config.keySeparator ?? '.',\n // Qualified Buro entity refs use `namespace:id`. Resource keys flow\n // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace\n // separator would split a valid ref before our explicit `ns` routing gets\n // to resolve it.\n nsSeparator: config.nsSeparator ?? false,\n defaultNS: 'common',\n interpolation: { escapeValue: false },\n returnNull: false,\n // An empty string is an untranslated key (what extraction tools write), not\n // a translation: `exists` and `t` walk on to the fallback locale, then the\n // caller's code default, so a label never renders blank.\n returnEmptyString: false,\n });\n\n return {\n t(key, params) {\n const dotIdx = key.indexOf('.');\n if (dotIdx === -1) {\n return instance.t(key, params) as string;\n }\n const ns = key.substring(0, dotIdx);\n const subKey = key.substring(dotIdx + 1);\n const result = instance.t(subKey, { ns, ...(params ?? {}) }) as string;\n // i18next returns the bare subKey when missing — restore the full key as\n // fallback. The probe carries the count, so a resolved count family is\n // not mistaken for a miss.\n const count = typeof params?.count === 'number' ? params.count : undefined;\n if (\n result === subKey &&\n !instance.exists(subKey, count === undefined ? { ns } : { ns, count })\n ) {\n return key;\n }\n return result;\n },\n async changeLocale(locale) {\n await instance.changeLanguage(locale);\n },\n async loadNamespace(namespace, _locale) {\n await instance.loadNamespaces(namespace);\n },\n getCurrentLocale() {\n return instance.language;\n },\n onLocaleChange(cb) {\n instance.on('languageChanged', cb);\n return () => instance.off('languageChanged', cb);\n },\n hasKey(key, options) {\n const dotIdx = key.indexOf('.');\n const ns = dotIdx === -1 ? undefined : key.substring(0, dotIdx);\n const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);\n const lookup = {\n ...(ns === undefined ? {} : { ns }),\n ...(options?.count === undefined ? {} : { count: options.count }),\n };\n /*\n `exists()` is not enough, and the gap is the root cause of a whole\n family of bugs rather than a curiosity.\n\n i18next answers `exists()` with TRUE for an OBJECT node — `fields.gtin`\n once that entry is promoted from a plain string to `{ label, doc }`,\n `enums.<ns>.<value>` when it carries a `doc` but no `label`, and so on.\n `t()` on such a node neither throws nor returns the key: it returns the\n literal sentence\n\n key '…' returned an object instead of string.\n\n Since `resolveContentSlot` commits to the FIRST key whose `has()` is\n true, every resolver built on it — field labels, enum labels, nav\n labels, tab captions — would render that sentence to the user. No crash,\n no warning, just wrong text on screen. It was found three separate times\n in three separate resolvers before being fixed here, once.\n\n `exists()` is also TRUE for an empty string in the last locale of the\n chain, even with `returnEmptyString: false`. So the lookup itself is the\n probe: `t()` walks the whole chain (count forms included) and answers the\n sentinel default only when no locale holds a usable value.\n\n So `has` means what its callers always assumed: there is a USABLE,\n non-empty STRING at this key. Non-object primitives stay truthy, leaving\n plural forms and numeric values behaving exactly as before.\n */\n const value = instance.t(subKey, {\n ...lookup,\n returnObjects: true,\n defaultValue: MISSING,\n }) as unknown;\n return value !== MISSING && (typeof value !== 'object' || value === null);\n },\n };\n}\n","import { resolveKey } from './lookup.js';\nimport type { I18nAdapter } from './types/index.js';\n\nexport interface I18nFacade {\n /**\n * Translate `key` through the candidate chain. `count` selects a count\n * family's form (`<key>_one | _few | _many | _other`, by the locale's CLDR\n * rules) and interpolates `{{count}}`; a family asked without a count is\n * missing. A `count` inside `params` only interpolates.\n */\n t: (\n key: string,\n options?: { resource?: string; params?: Record<string, unknown>; count?: number },\n ) => string;\n /**\n * True when a translation for `key` EXISTS (through the same candidate chain\n * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a\n * miss — the content model probes presence with `has` to tell \"no bundle\n * entry\" (render nothing) from \"bundle entry === key text\". Pass `count` to probe a count family.\n */\n has: (key: string, options?: { resource?: string; count?: number }) => boolean;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n /**\n * Subscribe to active-locale changes. Returns an unsubscribe fn. When the\n * adapter can't notify, this is a no-op subscription (returns an unsubscribe\n * that does nothing) — the live language switch then falls back to a static\n * read. Consumed by `useLocale()` via `useSyncExternalStore`.\n */\n onLocaleChange: (cb: () => void) => () => void;\n loadNamespace: (namespace: string, locale: string) => Promise<void>;\n /**\n * The locale codes this app can switch to. Optional: a facade built without\n * a list (a hand-rolled one, a test double) simply does not say, and a\n * language control then offers only the active locale. `createI18nFromRegistry`\n * fills it from the registry.\n */\n locales?: () => readonly string[];\n adapter: I18nAdapter;\n}\n\nexport interface CreateI18nConfig {\n adapter: I18nAdapter;\n warnOnMissing?: boolean;\n /** The switchable locale codes, exposed as {@link I18nFacade.locales}. */\n locales?: () => readonly string[];\n}\n\nexport function createI18n(config: CreateI18nConfig): I18nFacade {\n const { adapter, warnOnMissing = true, locales } = config;\n\n return {\n t(key, options) {\n // `count` is the one spelling: a `count` inside `params` only interpolates.\n const count = options?.count;\n const params = count === undefined ? options?.params : { ...options?.params, count };\n const candidates = resolveKey(key, options?.resource);\n for (const candidate of candidates) {\n const present =\n count === undefined ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count });\n if (present) {\n return adapter.t(candidate, params);\n }\n }\n if (warnOnMissing && typeof console !== 'undefined') {\n console.warn(\n `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ''}`,\n );\n }\n return key;\n },\n has(key, options) {\n const count = options?.count;\n return resolveKey(key, options?.resource).some((candidate) =>\n count === undefined ? adapter.hasKey(candidate) : adapter.hasKey(candidate, { count }),\n );\n },\n async changeLocale(locale) {\n await adapter.changeLocale(locale);\n },\n getCurrentLocale() {\n return adapter.getCurrentLocale();\n },\n onLocaleChange(cb) {\n return adapter.onLocaleChange?.(cb) ?? (() => {});\n },\n async loadNamespace(namespace, locale) {\n await adapter.loadNamespace(namespace, locale);\n },\n ...(locales === undefined ? {} : { locales }),\n adapter,\n };\n}\n","import type { I18nFacade } from './i18n.js';\n\nexport interface ResourceI18n {\n t: (\n key: string,\n options?: { resource?: string; params?: Record<string, unknown>; count?: number },\n ) => string;\n changeLocale: (locale: string) => Promise<void>;\n getCurrentLocale: () => string;\n resource: string;\n}\n\n/**\n * Wraps an I18nFacade with a default resource scope.\n * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.\n * Pass an explicit `resource` option to override the default scope.\n */\nexport function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n {\n return {\n t(key, options) {\n return i18n.t(key, {\n resource: options?.resource ?? resource,\n params: options?.params,\n ...(options?.count === undefined ? {} : { count: options.count }),\n });\n },\n changeLocale: i18n.changeLocale,\n getCurrentLocale: i18n.getCurrentLocale,\n resource,\n };\n}\n","import type { TranslationBundle } from './types/bundle.js';\n\n/**\n * One package/app/domain module's offering for one locale.\n *\n * A contribution is data, not code: nothing about `createI18nextAdapter`'s\n * `resources` shape is visible here, and nothing here names a fixed set of\n * locale codes or counts them. `locale` is whatever the file/module that\n * produced this contribution says it is — the registry never validates it\n * against a known list, because there is no known list. That is the whole\n * point: a twenty-first language is a twenty-first contribution, not a\n * twenty-first `case`.\n */\nexport interface LocaleContribution {\n /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */\n locale: string;\n /**\n * i18next namespace the bundle merges into. Defaults to `'common'` — the\n * same default namespace `createI18nextAdapter` uses.\n */\n namespace?: string;\n /**\n * Declared merge priority. On a key collision within the same\n * (locale, namespace), the contribution with the HIGHER priority wins —\n * not whichever happened to register last. Ties fall back to registration\n * order (last registered wins), which is why two genuinely competing\n * sources must declare distinct priorities rather than relying on the tie\n * break.\n */\n priority: number;\n /**\n * Where this contribution came from — a package name, `'app'`, an enum\n * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}\n * for debugging a collision); never branched on.\n */\n source: string;\n bundle: TranslationBundle;\n}\n\nexport interface LocaleRegistry {\n /**\n * The primary locale, as declared to {@link createLocaleRegistry}: the one\n * whose text is the default in code, and the locale every other one falls\n * back to. The single source of truth for it — `createI18nFromRegistry`\n * defaults both the starting locale and the fallback to it, and\n * `@burojs/testing`'s `checkApp` reads it to know which locale is exempt\n * from `i18n.missing`. Never derived from contributions or their order.\n */\n primary(): string;\n /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */\n register(contribution: LocaleContribution): void;\n /**\n * Every locale code that has at least one contribution, sorted. Derived\n * purely from what was registered — nothing enumerates or counts locales\n * anywhere else in this module.\n */\n locales(): string[];\n /** Every namespace that has at least one contribution, sorted. */\n namespaces(): string[];\n /** All registered contributions, in registration order. For diagnostics/tests. */\n contributions(): readonly LocaleContribution[];\n /**\n * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to\n * pass straight as `createI18nextAdapter`'s `resources` config. Within one\n * (locale, namespace) group, contributions are deep-merged in ascending\n * priority order, so a higher-priority contribution's leaves win over a\n * lower-priority one's on any overlapping key while leaving the rest of\n * the lower-priority bundle intact.\n *\n * Throws when no contribution is registered for {@link primary}: a\n * fallback chain that ends at a locale with no bundle, and a primary that\n * `locales()` (and so every language switcher built from it) does not\n * list, is a misconfiguration (usually a typo, `'en-US'` for `'en'`, or a\n * glob that missed its file), not a state to render. An app that really\n * has no strings for its primary locale registers an empty bundle for it.\n */\n buildResources(): Record<string, Record<string, TranslationBundle>>;\n}\n\nconst DEFAULT_NAMESPACE = 'common';\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Recursively merge `overlay` onto `base`. Arrays and primitives are replaced\n * wholesale (a `TranslationBundle` array leaf — see that type's own comment\n * on why arrays are a real leaf shape — is not index-merged: the whole array\n * is the unit an overlay replaces). Plain objects merge key by key.\n */\nfunction mergeBundle(base: TranslationBundle, overlay: TranslationBundle): TranslationBundle {\n const out: Record<string, unknown> = { ...base };\n for (const [key, overlayValue] of Object.entries(overlay)) {\n const baseValue = out[key];\n out[key] =\n isPlainObject(baseValue) && isPlainObject(overlayValue)\n ? mergeBundle(baseValue as TranslationBundle, overlayValue as TranslationBundle)\n : overlayValue;\n }\n return out as TranslationBundle;\n}\n\nexport interface LocaleRegistryOptions {\n /**\n * The primary locale: the one whose text is the default in code. Required,\n * and declared here rather than inferred, because nothing in the\n * contributions says which locale it is — registration order and the\n * sorted `locales()` both carry no meaning.\n */\n primary: string;\n}\n\nexport function createLocaleRegistry(options: LocaleRegistryOptions): LocaleRegistry {\n const primary = (options as Partial<LocaleRegistryOptions> | undefined)?.primary;\n if (typeof primary !== 'string' || primary === '') {\n throw new TypeError(\n `createLocaleRegistry needs { primary }, the locale whose text is the default in code; received ${JSON.stringify(primary)}.`,\n );\n }\n const registered: LocaleContribution[] = [];\n\n return {\n primary() {\n return primary;\n },\n register(contribution) {\n registered.push(contribution);\n },\n locales() {\n return Array.from(new Set(registered.map((c) => c.locale))).sort();\n },\n namespaces() {\n return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();\n },\n contributions() {\n return registered;\n },\n buildResources() {\n if (!registered.some((c) => c.locale === primary)) {\n const known = Array.from(new Set(registered.map((c) => c.locale))).sort();\n throw new Error(\n `The locale registry's primary locale ${JSON.stringify(primary)} has no contribution` +\n ` (registered: ${known.length === 0 ? 'none' : known.join(', ')}). Register its bundle,` +\n ' an empty one if the app has no strings of its own, or declare the primary it does register.',\n );\n }\n // Group by (locale, namespace) via NESTED maps rather than a\n // delimiter-joined composite string key. A locale or namespace code is\n // caller-supplied and unconstrained (see `LocaleContribution.locale`'s\n // own comment), so any single-character delimiter is a value that\n // *could* collide with real content; nested `Map`s need no delimiter\n // at all, so there is nothing to collide with and nothing unusual\n // embedded in this file's source for a diff tool to trip over.\n const byLocale = new Map<string, Map<string, LocaleContribution[]>>();\n for (const contribution of registered) {\n const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;\n let byNamespace = byLocale.get(contribution.locale);\n if (!byNamespace) {\n byNamespace = new Map<string, LocaleContribution[]>();\n byLocale.set(contribution.locale, byNamespace);\n }\n const group = byNamespace.get(namespace);\n if (group) {\n group.push(contribution);\n } else {\n byNamespace.set(namespace, [contribution]);\n }\n }\n\n const resources: Record<string, Record<string, TranslationBundle>> = {};\n for (const [locale, byNamespace] of byLocale) {\n const namespaces: Record<string, TranslationBundle> = {};\n for (const [namespace, group] of byNamespace) {\n // Stable sort ascending by priority: lower priority applied first,\n // higher priority applied last so it wins the overlapping leaves.\n // `Array#sort` is stable per spec (ES2019+), so contributions with\n // equal priority keep their registration order — last-registered\n // wins ONLY within a tie, never across a declared priority gap.\n const ordered = [...group].sort((a, b) => a.priority - b.priority);\n let merged: TranslationBundle = {};\n for (const contribution of ordered) {\n merged = mergeBundle(merged, contribution.bundle);\n }\n namespaces[namespace] = merged;\n }\n resources[locale] = namespaces;\n }\n return resources;\n },\n };\n}\n\n/**\n * A bundler-produced glob module, as `import.meta.glob` (Vite) or an\n * equivalent walk over `require.context` (webpack) would yield it: a flat\n * map of path → either the loaded module itself or `{ default }`.\n *\n * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s\n * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published\n * package must not require one — the ADOPTER calls `import.meta.glob` (or\n * whatever their bundler offers) and hands the plain object here.\n */\nexport type LocaleGlobModule = TranslationBundle | { default: TranslationBundle };\n\nexport interface LocaleContributionsFromGlobOptions {\n /** {@link LocaleContribution.source} for every contribution produced. */\n source: string;\n /** {@link LocaleContribution.priority} for every contribution produced. */\n priority: number;\n /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */\n namespace?: string;\n /**\n * Extracts the locale code from a glob path. Default matches a bare\n * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`\n * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a\n * known set of codes; whatever the filename says IS the locale code. Paths\n * that don't match are skipped rather than guessed at (same discipline as\n * `docsFromGlob`).\n */\n localeOf?: (path: string) => string | undefined;\n}\n\nconst DEFAULT_LOCALE_PATTERN = /(?:^|\\/)([A-Za-z0-9-]+)\\.(?:ts|tsx|js|jsx|mjs|cjs)$/;\n\nfunction defaultLocaleOf(path: string): string | undefined {\n return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];\n}\n\n/**\n * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.\n * One file → one locale → one contribution. Nothing here enumerates or counts\n * locale codes: whatever files are present in the globbed directory at build\n * time ARE the contributions produced — dropping in a new file is the entire\n * mechanism for adding a language.\n */\nexport function localeContributionsFromGlob(\n glob: Record<string, LocaleGlobModule>,\n options: LocaleContributionsFromGlobOptions,\n): LocaleContribution[] {\n const localeOf = options.localeOf ?? defaultLocaleOf;\n const out: LocaleContribution[] = [];\n for (const [path, mod] of Object.entries(glob)) {\n const locale = localeOf(path);\n if (locale === undefined) continue;\n // Eager `import.meta.glob` yields a module namespace object — always\n // `{ default, ...named exports }` for a file that does `export default\n // {...}`. Unwrap it when present; otherwise trust the glob already\n // handed a bare bundle (e.g. a caller that pre-unwrapped, or a\n // require.context-style walk that returns exports directly).\n const bundle = isPlainObject((mod as { default?: unknown }).default)\n ? (mod as { default: TranslationBundle }).default\n : (mod as TranslationBundle);\n out.push({\n locale,\n priority: options.priority,\n source: options.source,\n bundle,\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n });\n }\n return out;\n}\n","import { type I18nFacade, createI18n } from './i18n.js';\nimport { type I18nextAdapterConfig, createI18nextAdapter } from './i18next-adapter.js';\nimport type { LocaleRegistry } from './locale-registry.js';\n\nexport type CreateI18nFromRegistryConfig = Partial<\n Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'>\n> & {\n warnOnMissing?: boolean;\n};\n\n/**\n * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +\n * `createI18n`: `registry.locales()` becomes the adapter's `locales`,\n * `registry.buildResources()` becomes its `resources`, and\n * `registry.primary()` is both the starting locale and the fallback unless\n * `defaultLocale` (the starting locale, say a stored choice) or\n * `fallbackLocale` (say a per-locale chain) overrides it. This is the ONLY\n * place those need to be threaded together — an app that builds its own\n * registry never re-lists its locale codes, or restates its primary locale,\n * to hand them to the adapter; it just calls this.\n *\n * Throws when the registry's primary locale has no contribution (see\n * {@link LocaleRegistry.buildResources}).\n */\nexport async function createI18nFromRegistry(\n registry: LocaleRegistry,\n config: CreateI18nFromRegistryConfig = {},\n): Promise<I18nFacade> {\n const resources = registry.buildResources();\n const primary = registry.primary();\n const adapter = await createI18nextAdapter({\n defaultLocale: config.defaultLocale ?? primary,\n locales: registry.locales(),\n resources,\n fallbackLocale: config.fallbackLocale ?? primary,\n ...(config.keySeparator !== undefined ? { keySeparator: config.keySeparator } : {}),\n ...(config.nsSeparator !== undefined ? { nsSeparator: config.nsSeparator } : {}),\n });\n return createI18n({\n adapter,\n warnOnMissing: config.warnOnMissing,\n locales: () => registry.locales(),\n });\n}\n","// Constants\nexport const PACKAGE_NAME = '@burojs/i18n-core' as const;\nexport const PACKAGE_VERSION = '0.0.0' as const;\n\n// Types\nexport type * from './types/index.js';\n\n// Lookup\nexport { resolveKey } from './lookup.js';\n\n// i18next adapter\nexport { createI18nextAdapter, type I18nextAdapterConfig } from './i18next-adapter.js';\n\n// I18n facade\nexport { createI18n, type I18nFacade, type CreateI18nConfig } from './i18n.js';\n\n// Resource context\nexport { createResourceI18n, type ResourceI18n } from './resource-context.js';\n\n// Locale registry — assembling locales from what has REGISTERED (framework\n// packages, the app, domain enum modules) instead of a hand-printed literal.\nexport {\n createLocaleRegistry,\n localeContributionsFromGlob,\n type LocaleContribution,\n type LocaleContributionsFromGlobOptions,\n type LocaleGlobModule,\n type LocaleRegistry,\n type LocaleRegistryOptions,\n} from './locale-registry.js';\nexport {\n createI18nFromRegistry,\n type CreateI18nFromRegistryConfig,\n} from './create-i18n-from-registry.js';\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burojs/i18n-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "description": "Buro: hierarchical i18n with fallback chain over i18next",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@burojs/core": "0.4.0"
28
+ "@burojs/core": "0.5.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "i18next": ">=23 <27"