@burojs/i18n-core 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Kuznetsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @burojs/i18n-core
2
+
3
+ Hierarchical i18n for [buro](https://github.com/buro/buro), with a per-resource fallback chain over [i18next](https://www.i18next.com).
4
+
5
+ Translation happens at **render time**, so switching language updates the whole UI live — no reload, no refetch.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @burojs/i18n-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createI18n, createI18nextAdapter } from '@burojs/i18n-core';
17
+
18
+ const i18n = createI18n({
19
+ adapter: createI18nextAdapter({
20
+ defaultLocale: 'ru',
21
+ locales: ['ru', 'en'],
22
+ resources: { ru: ruBundle, en: enBundle },
23
+ fallbackLocale: 'en',
24
+ }),
25
+ });
26
+ ```
27
+
28
+ ## Key resolution
29
+
30
+ A key is resolved against the most specific namespace first and falls back outward, so a resource can override a shared string without copying the rest:
31
+
32
+ ```
33
+ resources.<resource>.fields.<field>.label → fields.<field>.label → <the key itself>
34
+ ```
35
+
36
+ `resolveKey` implements that chain and is exported for callers that need to know *which* key won.
37
+
38
+ ## Locale registry
39
+
40
+ Most apps assemble their `resources` from more than one place: the framework
41
+ ships a base dictionary, the app adds its own chrome strings, a domain module
42
+ adds its enum labels. Printing that as one literal object means adding a
43
+ language is a code change. `createLocaleRegistry` instead lets each source
44
+ **register** a contribution — the registry merges them:
45
+
46
+ ```ts
47
+ import {
48
+ createLocaleRegistry,
49
+ localeContributionsFromGlob,
50
+ createI18nFromRegistry,
51
+ } from '@burojs/i18n-core';
52
+ import { en as frameworkEn } from '@burojs/react';
53
+
54
+ const registry = createLocaleRegistry();
55
+ registry.register({ locale: 'en', priority: 0, source: '@burojs/react', bundle: frameworkEn });
56
+
57
+ // One file per locale, globbed by the bundler — dropping in `es.ts` is the
58
+ // entire mechanism for adding Spanish. Nothing here lists locale codes.
59
+ const glob = import.meta.glob('./locales/*.ts', { eager: true });
60
+ for (const c of localeContributionsFromGlob(glob, { source: 'app', priority: 10 })) {
61
+ registry.register(c);
62
+ }
63
+
64
+ const i18n = await createI18nFromRegistry(registry, {
65
+ defaultLocale: 'en',
66
+ fallbackLocale: 'en', // or a per-locale chain: { fr: ['en'], default: ['en'] }
67
+ });
68
+ ```
69
+
70
+ A key collision within one (locale, namespace) resolves by the higher
71
+ DECLARED `priority`, never by which contribution happened to register last.
72
+ `localeContributionsFromGlob` is bundler-agnostic (same reasoning as
73
+ `@burojs/core`'s `docsFromGlob`): it accepts whatever flat `path → module`
74
+ map `import.meta.glob` or an equivalent walk produces, and the caller — not
75
+ this package — is the one that actually calls the bundler API.
76
+
77
+ ## Main exports
78
+
79
+ - **`createI18n` / `I18nFacade`** — the facade the React layer consumes.
80
+ - **`createI18nextAdapter`** — the default backend; swap it to use another i18n runtime.
81
+ - **`createResourceI18n` / `ResourceI18n`** — a resource-scoped view of the facade.
82
+ - **`resolveKey`** — the fallback-chain resolver, usable standalone.
83
+ - **`createLocaleRegistry` / `LocaleRegistry`** — merges per-source locale contributions by declared priority.
84
+ - **`localeContributionsFromGlob`** — turns a bundler glob of per-locale files into contributions.
85
+ - **`createI18nFromRegistry`** — wires a `LocaleRegistry` straight into `createI18nextAdapter` + `createI18n`.
86
+
87
+ ## License
88
+
89
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ var i18next = require('i18next');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var i18next__default = /*#__PURE__*/_interopDefault(i18next);
8
+
9
+ // src/lookup.ts
10
+ function resolveKey(key, resource) {
11
+ if (!key) return [];
12
+ if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.")) {
13
+ return [key];
14
+ }
15
+ const candidates = [];
16
+ if (resource) {
17
+ candidates.push(`resources.${resource}.${key}`);
18
+ }
19
+ candidates.push(`resources._.${key}`);
20
+ candidates.push(`common.${key}`);
21
+ candidates.push(key);
22
+ return candidates;
23
+ }
24
+ async function createI18nextAdapter(config) {
25
+ const instance = i18next__default.default.createInstance();
26
+ await instance.init({
27
+ lng: config.defaultLocale,
28
+ fallbackLng: config.fallbackLocale ?? config.defaultLocale,
29
+ supportedLngs: config.locales,
30
+ resources: config.resources,
31
+ keySeparator: config.keySeparator ?? ".",
32
+ // Qualified Buro entity refs use `namespace:id`. Resource keys flow
33
+ // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace
34
+ // separator would split a valid ref before our explicit `ns` routing gets
35
+ // to resolve it.
36
+ nsSeparator: config.nsSeparator ?? false,
37
+ defaultNS: "common",
38
+ interpolation: { escapeValue: false },
39
+ returnNull: false
40
+ });
41
+ return {
42
+ t(key, params) {
43
+ const dotIdx = key.indexOf(".");
44
+ if (dotIdx === -1) {
45
+ return instance.t(key, params);
46
+ }
47
+ const ns = key.substring(0, dotIdx);
48
+ const subKey = key.substring(dotIdx + 1);
49
+ const result = instance.t(subKey, { ns, ...params ?? {} });
50
+ if (result === subKey && !instance.exists(subKey, { ns })) {
51
+ return key;
52
+ }
53
+ return result;
54
+ },
55
+ async changeLocale(locale) {
56
+ await instance.changeLanguage(locale);
57
+ },
58
+ async loadNamespace(namespace, _locale) {
59
+ await instance.loadNamespaces(namespace);
60
+ },
61
+ getCurrentLocale() {
62
+ return instance.language;
63
+ },
64
+ onLocaleChange(cb) {
65
+ instance.on("languageChanged", cb);
66
+ return () => instance.off("languageChanged", cb);
67
+ },
68
+ hasKey(key) {
69
+ const dotIdx = key.indexOf(".");
70
+ const ns = dotIdx === -1 ? void 0 : key.substring(0, dotIdx);
71
+ const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);
72
+ const opts = ns === void 0 ? void 0 : { ns };
73
+ if (!instance.exists(subKey, opts)) return false;
74
+ const value = instance.t(subKey, { ...opts ?? {}, returnObjects: true });
75
+ return typeof value !== "object" || value === null;
76
+ }
77
+ };
78
+ }
79
+
80
+ // src/i18n.ts
81
+ function createI18n(config) {
82
+ const { adapter, warnOnMissing = true } = config;
83
+ return {
84
+ t(key, options) {
85
+ const candidates = resolveKey(key, options?.resource);
86
+ for (const candidate of candidates) {
87
+ if (adapter.hasKey(candidate)) {
88
+ return adapter.t(candidate, options?.params);
89
+ }
90
+ }
91
+ if (warnOnMissing && typeof console !== "undefined") {
92
+ console.warn(
93
+ `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ""}`
94
+ );
95
+ }
96
+ return key;
97
+ },
98
+ has(key, options) {
99
+ return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));
100
+ },
101
+ async changeLocale(locale) {
102
+ await adapter.changeLocale(locale);
103
+ },
104
+ getCurrentLocale() {
105
+ return adapter.getCurrentLocale();
106
+ },
107
+ onLocaleChange(cb) {
108
+ return adapter.onLocaleChange?.(cb) ?? (() => {
109
+ });
110
+ },
111
+ async loadNamespace(namespace, locale) {
112
+ await adapter.loadNamespace(namespace, locale);
113
+ },
114
+ adapter
115
+ };
116
+ }
117
+
118
+ // src/resource-context.ts
119
+ function createResourceI18n(i18n, resource) {
120
+ return {
121
+ t(key, options) {
122
+ return i18n.t(key, {
123
+ resource: options?.resource ?? resource,
124
+ params: options?.params
125
+ });
126
+ },
127
+ changeLocale: i18n.changeLocale,
128
+ getCurrentLocale: i18n.getCurrentLocale,
129
+ resource
130
+ };
131
+ }
132
+
133
+ // src/locale-registry.ts
134
+ var DEFAULT_NAMESPACE = "common";
135
+ function isPlainObject(value) {
136
+ return typeof value === "object" && value !== null && !Array.isArray(value);
137
+ }
138
+ function mergeBundle(base, overlay) {
139
+ const out = { ...base };
140
+ for (const [key, overlayValue] of Object.entries(overlay)) {
141
+ const baseValue = out[key];
142
+ out[key] = isPlainObject(baseValue) && isPlainObject(overlayValue) ? mergeBundle(baseValue, overlayValue) : overlayValue;
143
+ }
144
+ return out;
145
+ }
146
+ function createLocaleRegistry() {
147
+ const registered = [];
148
+ return {
149
+ register(contribution) {
150
+ registered.push(contribution);
151
+ },
152
+ locales() {
153
+ return Array.from(new Set(registered.map((c) => c.locale))).sort();
154
+ },
155
+ namespaces() {
156
+ return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();
157
+ },
158
+ contributions() {
159
+ return registered;
160
+ },
161
+ buildResources() {
162
+ const byLocale = /* @__PURE__ */ new Map();
163
+ for (const contribution of registered) {
164
+ const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;
165
+ let byNamespace = byLocale.get(contribution.locale);
166
+ if (!byNamespace) {
167
+ byNamespace = /* @__PURE__ */ new Map();
168
+ byLocale.set(contribution.locale, byNamespace);
169
+ }
170
+ const group = byNamespace.get(namespace);
171
+ if (group) {
172
+ group.push(contribution);
173
+ } else {
174
+ byNamespace.set(namespace, [contribution]);
175
+ }
176
+ }
177
+ const resources = {};
178
+ for (const [locale, byNamespace] of byLocale) {
179
+ const namespaces = {};
180
+ for (const [namespace, group] of byNamespace) {
181
+ const ordered = [...group].sort((a, b) => a.priority - b.priority);
182
+ let merged = {};
183
+ for (const contribution of ordered) {
184
+ merged = mergeBundle(merged, contribution.bundle);
185
+ }
186
+ namespaces[namespace] = merged;
187
+ }
188
+ resources[locale] = namespaces;
189
+ }
190
+ return resources;
191
+ }
192
+ };
193
+ }
194
+ var DEFAULT_LOCALE_PATTERN = /(?:^|\/)([A-Za-z0-9-]+)\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
195
+ function defaultLocaleOf(path) {
196
+ return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];
197
+ }
198
+ function localeContributionsFromGlob(glob, options) {
199
+ const localeOf = options.localeOf ?? defaultLocaleOf;
200
+ const out = [];
201
+ for (const [path, mod] of Object.entries(glob)) {
202
+ const locale = localeOf(path);
203
+ if (locale === void 0) continue;
204
+ const bundle = isPlainObject(mod.default) ? mod.default : mod;
205
+ out.push({
206
+ locale,
207
+ priority: options.priority,
208
+ source: options.source,
209
+ bundle,
210
+ ...options.namespace !== void 0 ? { namespace: options.namespace } : {}
211
+ });
212
+ }
213
+ return out;
214
+ }
215
+
216
+ // src/create-i18n-from-registry.ts
217
+ async function createI18nFromRegistry(registry, config) {
218
+ const adapter = await createI18nextAdapter({
219
+ defaultLocale: config.defaultLocale,
220
+ locales: registry.locales(),
221
+ resources: registry.buildResources(),
222
+ ...config.fallbackLocale !== void 0 ? { fallbackLocale: config.fallbackLocale } : {},
223
+ ...config.keySeparator !== void 0 ? { keySeparator: config.keySeparator } : {},
224
+ ...config.nsSeparator !== void 0 ? { nsSeparator: config.nsSeparator } : {}
225
+ });
226
+ return createI18n({ adapter, warnOnMissing: config.warnOnMissing });
227
+ }
228
+
229
+ // src/index.ts
230
+ var PACKAGE_NAME = "@burojs/i18n-core";
231
+ var PACKAGE_VERSION = "0.0.0";
232
+
233
+ exports.PACKAGE_NAME = PACKAGE_NAME;
234
+ exports.PACKAGE_VERSION = PACKAGE_VERSION;
235
+ exports.createI18n = createI18n;
236
+ exports.createI18nFromRegistry = createI18nFromRegistry;
237
+ exports.createI18nextAdapter = createI18nextAdapter;
238
+ exports.createLocaleRegistry = createLocaleRegistry;
239
+ exports.createResourceI18n = createResourceI18n;
240
+ exports.localeContributionsFromGlob = localeContributionsFromGlob;
241
+ exports.resolveKey = resolveKey;
242
+ //# sourceMappingURL=index.cjs.map
243
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"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"],"names":["i18next"],"mappings":"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","file":"index.cjs","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"]}
@@ -0,0 +1,261 @@
1
+ import { FallbackLng } from 'i18next';
2
+
3
+ interface I18nAdapter {
4
+ t: (key: string, params?: Record<string, unknown>) => string;
5
+ changeLocale: (locale: string) => Promise<void>;
6
+ loadNamespace: (namespace: string, locale: string) => Promise<void>;
7
+ getCurrentLocale: () => string;
8
+ hasKey: (key: string) => boolean;
9
+ /**
10
+ * Subscribe to active-locale changes (fired by {@link I18nAdapter.changeLocale}).
11
+ * Returns an unsubscribe fn. Optional — adapters that cannot notify omit it,
12
+ * and the React layer then degrades to a non-reactive locale read. This is
13
+ * what powers the live language switch (no page reload): `useLocale()` wires
14
+ * it into `useSyncExternalStore`.
15
+ */
16
+ onLocaleChange?: (cb: () => void) => () => void;
17
+ }
18
+
19
+ /**
20
+ * Recursive translation bundle: keys map to a string, a nested bundle, or an
21
+ * ARRAY of either.
22
+ *
23
+ * The array arm is not a convenience. i18next addresses array members by index
24
+ * — `doc.links.0.label` is exactly how `resolveDoc` reads a documentation
25
+ * entry's external references — so an ordered list is a shape the store really
26
+ * holds. The type said otherwise, and the mismatch was hidden rather than
27
+ * caught: resource bundles are cast to `ResourceTranslationBundle` at the
28
+ * boundary, so nothing complained until a bundle carrying `links` was passed
29
+ * WITHOUT a cast, at which point the error surfaced in the app rather than in
30
+ * the package that owns the type.
31
+ */
32
+ type TranslationBundle = {
33
+ [key: string]: string | TranslationBundle | readonly (string | TranslationBundle)[];
34
+ };
35
+ /**
36
+ * Per-resource bundle: optional async loader returning default-export bundle.
37
+ */
38
+ type ResourceBundle = (() => Promise<{
39
+ default: TranslationBundle;
40
+ }>) | Record<string, () => Promise<{
41
+ default: TranslationBundle;
42
+ }>>;
43
+
44
+ /**
45
+ * Open interface for opt-in codegen-typed i18n keys.
46
+ *
47
+ * declare module '@burojs/i18n-core' {
48
+ * interface I18nKeys {
49
+ * 'actions.create': void;
50
+ * }
51
+ * }
52
+ */
53
+ interface I18nKeys {
54
+ }
55
+ /**
56
+ * Open interface for keys requiring interpolation parameters.
57
+ */
58
+ interface I18nKeysWithParams {
59
+ }
60
+
61
+ /**
62
+ * Build candidate keys for fallback lookup.
63
+ *
64
+ * Lookup order (first match wins):
65
+ * 1. resources.<name>.<key> — resource-specific
66
+ * 2. resources._.<key> — base for all resources
67
+ * 3. common.<key> — global common
68
+ * 4. <key> — raw fallback (debug only)
69
+ *
70
+ * If `key` is already namespaced (starts with `resources.`, `enums.` or
71
+ * `common.`), return it as-is without further scoping.
72
+ */
73
+ declare function resolveKey(key: string, resource?: string): string[];
74
+
75
+ interface I18nextAdapterConfig {
76
+ defaultLocale: string;
77
+ locales: string[];
78
+ resources?: Record<string, Record<string, TranslationBundle>>;
79
+ /**
80
+ * A single fallback locale (`'en'`) applies uniformly to every locale. A
81
+ * `fr → en` fallback for one locale but not another needs i18next's own
82
+ * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so
83
+ * this accepts i18next's full `FallbackLng` type rather than just `string`.
84
+ * The chain is entirely CONFIG: nothing in this adapter special-cases any
85
+ * particular locale code, here or anywhere else.
86
+ */
87
+ fallbackLocale?: FallbackLng;
88
+ keySeparator?: string | false;
89
+ nsSeparator?: string | false;
90
+ }
91
+ /**
92
+ * Default I18nAdapter implementation backed by i18next.
93
+ * Each top-level key in resources becomes a separate i18next namespace
94
+ * (e.g. 'common', 'resources').
95
+ */
96
+ declare function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter>;
97
+
98
+ interface I18nFacade {
99
+ t: (key: string, options?: {
100
+ resource?: string;
101
+ params?: Record<string, unknown>;
102
+ }) => string;
103
+ /**
104
+ * True when a translation for `key` EXISTS (through the same candidate chain
105
+ * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a
106
+ * miss — the content model probes presence with `has` to tell "no bundle
107
+ * entry" (render nothing) from "bundle entry === key text".
108
+ */
109
+ has: (key: string, options?: {
110
+ resource?: string;
111
+ }) => boolean;
112
+ changeLocale: (locale: string) => Promise<void>;
113
+ getCurrentLocale: () => string;
114
+ /**
115
+ * Subscribe to active-locale changes. Returns an unsubscribe fn. When the
116
+ * adapter can't notify, this is a no-op subscription (returns an unsubscribe
117
+ * that does nothing) — the live language switch then falls back to a static
118
+ * read. Consumed by `useLocale()` via `useSyncExternalStore`.
119
+ */
120
+ onLocaleChange: (cb: () => void) => () => void;
121
+ loadNamespace: (namespace: string, locale: string) => Promise<void>;
122
+ adapter: I18nAdapter;
123
+ }
124
+ interface CreateI18nConfig {
125
+ adapter: I18nAdapter;
126
+ warnOnMissing?: boolean;
127
+ }
128
+ declare function createI18n(config: CreateI18nConfig): I18nFacade;
129
+
130
+ interface ResourceI18n {
131
+ t: (key: string, options?: {
132
+ resource?: string;
133
+ params?: Record<string, unknown>;
134
+ }) => string;
135
+ changeLocale: (locale: string) => Promise<void>;
136
+ getCurrentLocale: () => string;
137
+ resource: string;
138
+ }
139
+ /**
140
+ * Wraps an I18nFacade with a default resource scope.
141
+ * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.
142
+ * Pass an explicit `resource` option to override the default scope.
143
+ */
144
+ declare function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n;
145
+
146
+ /**
147
+ * One package/app/domain module's offering for one locale.
148
+ *
149
+ * A contribution is data, not code: nothing about `createI18nextAdapter`'s
150
+ * `resources` shape is visible here, and nothing here names a fixed set of
151
+ * locale codes or counts them. `locale` is whatever the file/module that
152
+ * produced this contribution says it is — the registry never validates it
153
+ * against a known list, because there is no known list. That is the whole
154
+ * point: a twenty-first language is a twenty-first contribution, not a
155
+ * twenty-first `case`.
156
+ */
157
+ interface LocaleContribution {
158
+ /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */
159
+ locale: string;
160
+ /**
161
+ * i18next namespace the bundle merges into. Defaults to `'common'` — the
162
+ * same default namespace `createI18nextAdapter` uses.
163
+ */
164
+ namespace?: string;
165
+ /**
166
+ * Declared merge priority. On a key collision within the same
167
+ * (locale, namespace), the contribution with the HIGHER priority wins —
168
+ * not whichever happened to register last. Ties fall back to registration
169
+ * order (last registered wins), which is why two genuinely competing
170
+ * sources must declare distinct priorities rather than relying on the tie
171
+ * break.
172
+ */
173
+ priority: number;
174
+ /**
175
+ * Where this contribution came from — a package name, `'app'`, an enum
176
+ * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}
177
+ * for debugging a collision); never branched on.
178
+ */
179
+ source: string;
180
+ bundle: TranslationBundle;
181
+ }
182
+ interface LocaleRegistry {
183
+ /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */
184
+ register(contribution: LocaleContribution): void;
185
+ /**
186
+ * Every locale code that has at least one contribution, sorted. Derived
187
+ * purely from what was registered — nothing enumerates or counts locales
188
+ * anywhere else in this module.
189
+ */
190
+ locales(): string[];
191
+ /** Every namespace that has at least one contribution, sorted. */
192
+ namespaces(): string[];
193
+ /** All registered contributions, in registration order. For diagnostics/tests. */
194
+ contributions(): readonly LocaleContribution[];
195
+ /**
196
+ * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to
197
+ * pass straight as `createI18nextAdapter`'s `resources` config. Within one
198
+ * (locale, namespace) group, contributions are deep-merged in ascending
199
+ * priority order, so a higher-priority contribution's leaves win over a
200
+ * lower-priority one's on any overlapping key while leaving the rest of
201
+ * the lower-priority bundle intact.
202
+ */
203
+ buildResources(): Record<string, Record<string, TranslationBundle>>;
204
+ }
205
+ declare function createLocaleRegistry(): LocaleRegistry;
206
+ /**
207
+ * A bundler-produced glob module, as `import.meta.glob` (Vite) or an
208
+ * equivalent walk over `require.context` (webpack) would yield it: a flat
209
+ * map of path → either the loaded module itself or `{ default }`.
210
+ *
211
+ * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s
212
+ * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published
213
+ * package must not require one — the ADOPTER calls `import.meta.glob` (or
214
+ * whatever their bundler offers) and hands the plain object here.
215
+ */
216
+ type LocaleGlobModule = TranslationBundle | {
217
+ default: TranslationBundle;
218
+ };
219
+ interface LocaleContributionsFromGlobOptions {
220
+ /** {@link LocaleContribution.source} for every contribution produced. */
221
+ source: string;
222
+ /** {@link LocaleContribution.priority} for every contribution produced. */
223
+ priority: number;
224
+ /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */
225
+ namespace?: string;
226
+ /**
227
+ * Extracts the locale code from a glob path. Default matches a bare
228
+ * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`
229
+ * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a
230
+ * known set of codes; whatever the filename says IS the locale code. Paths
231
+ * that don't match are skipped rather than guessed at (same discipline as
232
+ * `docsFromGlob`).
233
+ */
234
+ localeOf?: (path: string) => string | undefined;
235
+ }
236
+ /**
237
+ * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.
238
+ * One file → one locale → one contribution. Nothing here enumerates or counts
239
+ * locale codes: whatever files are present in the globbed directory at build
240
+ * time ARE the contributions produced — dropping in a new file is the entire
241
+ * mechanism for adding a language.
242
+ */
243
+ declare function localeContributionsFromGlob(glob: Record<string, LocaleGlobModule>, options: LocaleContributionsFromGlobOptions): LocaleContribution[];
244
+
245
+ type CreateI18nFromRegistryConfig = Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'> & {
246
+ warnOnMissing?: boolean;
247
+ };
248
+ /**
249
+ * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +
250
+ * `createI18n`: `registry.locales()` becomes the adapter's `locales`,
251
+ * `registry.buildResources()` becomes its `resources`. This is the ONLY
252
+ * place those two need to be threaded together — an app that builds its own
253
+ * registry never re-lists its locale codes to hand them to the adapter, it
254
+ * just calls this.
255
+ */
256
+ declare function createI18nFromRegistry(registry: LocaleRegistry, config: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
257
+
258
+ declare const PACKAGE_NAME: "@burojs/i18n-core";
259
+ declare const PACKAGE_VERSION: "0.0.0";
260
+
261
+ 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 };
@@ -0,0 +1,261 @@
1
+ import { FallbackLng } from 'i18next';
2
+
3
+ interface I18nAdapter {
4
+ t: (key: string, params?: Record<string, unknown>) => string;
5
+ changeLocale: (locale: string) => Promise<void>;
6
+ loadNamespace: (namespace: string, locale: string) => Promise<void>;
7
+ getCurrentLocale: () => string;
8
+ hasKey: (key: string) => boolean;
9
+ /**
10
+ * Subscribe to active-locale changes (fired by {@link I18nAdapter.changeLocale}).
11
+ * Returns an unsubscribe fn. Optional — adapters that cannot notify omit it,
12
+ * and the React layer then degrades to a non-reactive locale read. This is
13
+ * what powers the live language switch (no page reload): `useLocale()` wires
14
+ * it into `useSyncExternalStore`.
15
+ */
16
+ onLocaleChange?: (cb: () => void) => () => void;
17
+ }
18
+
19
+ /**
20
+ * Recursive translation bundle: keys map to a string, a nested bundle, or an
21
+ * ARRAY of either.
22
+ *
23
+ * The array arm is not a convenience. i18next addresses array members by index
24
+ * — `doc.links.0.label` is exactly how `resolveDoc` reads a documentation
25
+ * entry's external references — so an ordered list is a shape the store really
26
+ * holds. The type said otherwise, and the mismatch was hidden rather than
27
+ * caught: resource bundles are cast to `ResourceTranslationBundle` at the
28
+ * boundary, so nothing complained until a bundle carrying `links` was passed
29
+ * WITHOUT a cast, at which point the error surfaced in the app rather than in
30
+ * the package that owns the type.
31
+ */
32
+ type TranslationBundle = {
33
+ [key: string]: string | TranslationBundle | readonly (string | TranslationBundle)[];
34
+ };
35
+ /**
36
+ * Per-resource bundle: optional async loader returning default-export bundle.
37
+ */
38
+ type ResourceBundle = (() => Promise<{
39
+ default: TranslationBundle;
40
+ }>) | Record<string, () => Promise<{
41
+ default: TranslationBundle;
42
+ }>>;
43
+
44
+ /**
45
+ * Open interface for opt-in codegen-typed i18n keys.
46
+ *
47
+ * declare module '@burojs/i18n-core' {
48
+ * interface I18nKeys {
49
+ * 'actions.create': void;
50
+ * }
51
+ * }
52
+ */
53
+ interface I18nKeys {
54
+ }
55
+ /**
56
+ * Open interface for keys requiring interpolation parameters.
57
+ */
58
+ interface I18nKeysWithParams {
59
+ }
60
+
61
+ /**
62
+ * Build candidate keys for fallback lookup.
63
+ *
64
+ * Lookup order (first match wins):
65
+ * 1. resources.<name>.<key> — resource-specific
66
+ * 2. resources._.<key> — base for all resources
67
+ * 3. common.<key> — global common
68
+ * 4. <key> — raw fallback (debug only)
69
+ *
70
+ * If `key` is already namespaced (starts with `resources.`, `enums.` or
71
+ * `common.`), return it as-is without further scoping.
72
+ */
73
+ declare function resolveKey(key: string, resource?: string): string[];
74
+
75
+ interface I18nextAdapterConfig {
76
+ defaultLocale: string;
77
+ locales: string[];
78
+ resources?: Record<string, Record<string, TranslationBundle>>;
79
+ /**
80
+ * A single fallback locale (`'en'`) applies uniformly to every locale. A
81
+ * `fr → en` fallback for one locale but not another needs i18next's own
82
+ * per-locale chain shape instead — `{ fr: ['en'], default: ['en'] }` — so
83
+ * this accepts i18next's full `FallbackLng` type rather than just `string`.
84
+ * The chain is entirely CONFIG: nothing in this adapter special-cases any
85
+ * particular locale code, here or anywhere else.
86
+ */
87
+ fallbackLocale?: FallbackLng;
88
+ keySeparator?: string | false;
89
+ nsSeparator?: string | false;
90
+ }
91
+ /**
92
+ * Default I18nAdapter implementation backed by i18next.
93
+ * Each top-level key in resources becomes a separate i18next namespace
94
+ * (e.g. 'common', 'resources').
95
+ */
96
+ declare function createI18nextAdapter(config: I18nextAdapterConfig): Promise<I18nAdapter>;
97
+
98
+ interface I18nFacade {
99
+ t: (key: string, options?: {
100
+ resource?: string;
101
+ params?: Record<string, unknown>;
102
+ }) => string;
103
+ /**
104
+ * True when a translation for `key` EXISTS (through the same candidate chain
105
+ * as {@link I18nFacade.t}). Distinct from `t`, which returns the raw key on a
106
+ * miss — the content model probes presence with `has` to tell "no bundle
107
+ * entry" (render nothing) from "bundle entry === key text".
108
+ */
109
+ has: (key: string, options?: {
110
+ resource?: string;
111
+ }) => boolean;
112
+ changeLocale: (locale: string) => Promise<void>;
113
+ getCurrentLocale: () => string;
114
+ /**
115
+ * Subscribe to active-locale changes. Returns an unsubscribe fn. When the
116
+ * adapter can't notify, this is a no-op subscription (returns an unsubscribe
117
+ * that does nothing) — the live language switch then falls back to a static
118
+ * read. Consumed by `useLocale()` via `useSyncExternalStore`.
119
+ */
120
+ onLocaleChange: (cb: () => void) => () => void;
121
+ loadNamespace: (namespace: string, locale: string) => Promise<void>;
122
+ adapter: I18nAdapter;
123
+ }
124
+ interface CreateI18nConfig {
125
+ adapter: I18nAdapter;
126
+ warnOnMissing?: boolean;
127
+ }
128
+ declare function createI18n(config: CreateI18nConfig): I18nFacade;
129
+
130
+ interface ResourceI18n {
131
+ t: (key: string, options?: {
132
+ resource?: string;
133
+ params?: Record<string, unknown>;
134
+ }) => string;
135
+ changeLocale: (locale: string) => Promise<void>;
136
+ getCurrentLocale: () => string;
137
+ resource: string;
138
+ }
139
+ /**
140
+ * Wraps an I18nFacade with a default resource scope.
141
+ * `t('actions.delete')` will lookup `resources.<resource>.actions.delete` first.
142
+ * Pass an explicit `resource` option to override the default scope.
143
+ */
144
+ declare function createResourceI18n(i18n: I18nFacade, resource: string): ResourceI18n;
145
+
146
+ /**
147
+ * One package/app/domain module's offering for one locale.
148
+ *
149
+ * A contribution is data, not code: nothing about `createI18nextAdapter`'s
150
+ * `resources` shape is visible here, and nothing here names a fixed set of
151
+ * locale codes or counts them. `locale` is whatever the file/module that
152
+ * produced this contribution says it is — the registry never validates it
153
+ * against a known list, because there is no known list. That is the whole
154
+ * point: a twenty-first language is a twenty-first contribution, not a
155
+ * twenty-first `case`.
156
+ */
157
+ interface LocaleContribution {
158
+ /** Locale code this contribution provides strings for (e.g. 'en', 'es', 'fr'). */
159
+ locale: string;
160
+ /**
161
+ * i18next namespace the bundle merges into. Defaults to `'common'` — the
162
+ * same default namespace `createI18nextAdapter` uses.
163
+ */
164
+ namespace?: string;
165
+ /**
166
+ * Declared merge priority. On a key collision within the same
167
+ * (locale, namespace), the contribution with the HIGHER priority wins —
168
+ * not whichever happened to register last. Ties fall back to registration
169
+ * order (last registered wins), which is why two genuinely competing
170
+ * sources must declare distinct priorities rather than relying on the tie
171
+ * break.
172
+ */
173
+ priority: number;
174
+ /**
175
+ * Where this contribution came from — a package name, `'app'`, an enum
176
+ * module name. Diagnostic only (surfaced by {@link LocaleRegistry.contributions}
177
+ * for debugging a collision); never branched on.
178
+ */
179
+ source: string;
180
+ bundle: TranslationBundle;
181
+ }
182
+ interface LocaleRegistry {
183
+ /** Add one contribution. Order of registration does not matter for the merge result — only `priority` does (see {@link LocaleContribution.priority}). */
184
+ register(contribution: LocaleContribution): void;
185
+ /**
186
+ * Every locale code that has at least one contribution, sorted. Derived
187
+ * purely from what was registered — nothing enumerates or counts locales
188
+ * anywhere else in this module.
189
+ */
190
+ locales(): string[];
191
+ /** Every namespace that has at least one contribution, sorted. */
192
+ namespaces(): string[];
193
+ /** All registered contributions, in registration order. For diagnostics/tests. */
194
+ contributions(): readonly LocaleContribution[];
195
+ /**
196
+ * Assemble `{ [locale]: { [namespace]: TranslationBundle } }`, ready to
197
+ * pass straight as `createI18nextAdapter`'s `resources` config. Within one
198
+ * (locale, namespace) group, contributions are deep-merged in ascending
199
+ * priority order, so a higher-priority contribution's leaves win over a
200
+ * lower-priority one's on any overlapping key while leaving the rest of
201
+ * the lower-priority bundle intact.
202
+ */
203
+ buildResources(): Record<string, Record<string, TranslationBundle>>;
204
+ }
205
+ declare function createLocaleRegistry(): LocaleRegistry;
206
+ /**
207
+ * A bundler-produced glob module, as `import.meta.glob` (Vite) or an
208
+ * equivalent walk over `require.context` (webpack) would yield it: a flat
209
+ * map of path → either the loaded module itself or `{ default }`.
210
+ *
211
+ * Bundler-agnostic on purpose, same reasoning as `@burojs/core`'s
212
+ * `docsFromGlob`: no bundler-agnostic glob primitive exists, and a published
213
+ * package must not require one — the ADOPTER calls `import.meta.glob` (or
214
+ * whatever their bundler offers) and hands the plain object here.
215
+ */
216
+ type LocaleGlobModule = TranslationBundle | {
217
+ default: TranslationBundle;
218
+ };
219
+ interface LocaleContributionsFromGlobOptions {
220
+ /** {@link LocaleContribution.source} for every contribution produced. */
221
+ source: string;
222
+ /** {@link LocaleContribution.priority} for every contribution produced. */
223
+ priority: number;
224
+ /** {@link LocaleContribution.namespace}. Defaults to `'common'`. */
225
+ namespace?: string;
226
+ /**
227
+ * Extracts the locale code from a glob path. Default matches a bare
228
+ * `<locale>.ts`/`<locale>.tsx`/`<locale>.js` filename (`'./locales/en.ts'`
229
+ * → `'en'`, `'./locales/fr-CA.ts'` → `'fr-CA'`) — no restriction to a
230
+ * known set of codes; whatever the filename says IS the locale code. Paths
231
+ * that don't match are skipped rather than guessed at (same discipline as
232
+ * `docsFromGlob`).
233
+ */
234
+ localeOf?: (path: string) => string | undefined;
235
+ }
236
+ /**
237
+ * Normalize a bundler glob of per-locale files into {@link LocaleContribution}s.
238
+ * One file → one locale → one contribution. Nothing here enumerates or counts
239
+ * locale codes: whatever files are present in the globbed directory at build
240
+ * time ARE the contributions produced — dropping in a new file is the entire
241
+ * mechanism for adding a language.
242
+ */
243
+ declare function localeContributionsFromGlob(glob: Record<string, LocaleGlobModule>, options: LocaleContributionsFromGlobOptions): LocaleContribution[];
244
+
245
+ type CreateI18nFromRegistryConfig = Pick<I18nextAdapterConfig, 'defaultLocale' | 'fallbackLocale' | 'keySeparator' | 'nsSeparator'> & {
246
+ warnOnMissing?: boolean;
247
+ };
248
+ /**
249
+ * Wires a {@link LocaleRegistry} straight into `createI18nextAdapter` +
250
+ * `createI18n`: `registry.locales()` becomes the adapter's `locales`,
251
+ * `registry.buildResources()` becomes its `resources`. This is the ONLY
252
+ * place those two need to be threaded together — an app that builds its own
253
+ * registry never re-lists its locale codes to hand them to the adapter, it
254
+ * just calls this.
255
+ */
256
+ declare function createI18nFromRegistry(registry: LocaleRegistry, config: CreateI18nFromRegistryConfig): Promise<I18nFacade>;
257
+
258
+ declare const PACKAGE_NAME: "@burojs/i18n-core";
259
+ declare const PACKAGE_VERSION: "0.0.0";
260
+
261
+ 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 };
package/dist/index.js ADDED
@@ -0,0 +1,229 @@
1
+ import i18next from 'i18next';
2
+
3
+ // src/lookup.ts
4
+ function resolveKey(key, resource) {
5
+ if (!key) return [];
6
+ if (key.startsWith("resources.") || key.startsWith("enums.") || key.startsWith("common.")) {
7
+ return [key];
8
+ }
9
+ const candidates = [];
10
+ if (resource) {
11
+ candidates.push(`resources.${resource}.${key}`);
12
+ }
13
+ candidates.push(`resources._.${key}`);
14
+ candidates.push(`common.${key}`);
15
+ candidates.push(key);
16
+ return candidates;
17
+ }
18
+ async function createI18nextAdapter(config) {
19
+ const instance = i18next.createInstance();
20
+ await instance.init({
21
+ lng: config.defaultLocale,
22
+ fallbackLng: config.fallbackLocale ?? config.defaultLocale,
23
+ supportedLngs: config.locales,
24
+ resources: config.resources,
25
+ keySeparator: config.keySeparator ?? ".",
26
+ // Qualified Buro entity refs use `namespace:id`. Resource keys flow
27
+ // through `resources.<ref>.<key>`, so treating ':' as i18next's namespace
28
+ // separator would split a valid ref before our explicit `ns` routing gets
29
+ // to resolve it.
30
+ nsSeparator: config.nsSeparator ?? false,
31
+ defaultNS: "common",
32
+ interpolation: { escapeValue: false },
33
+ returnNull: false
34
+ });
35
+ return {
36
+ t(key, params) {
37
+ const dotIdx = key.indexOf(".");
38
+ if (dotIdx === -1) {
39
+ return instance.t(key, params);
40
+ }
41
+ const ns = key.substring(0, dotIdx);
42
+ const subKey = key.substring(dotIdx + 1);
43
+ const result = instance.t(subKey, { ns, ...params ?? {} });
44
+ if (result === subKey && !instance.exists(subKey, { ns })) {
45
+ return key;
46
+ }
47
+ return result;
48
+ },
49
+ async changeLocale(locale) {
50
+ await instance.changeLanguage(locale);
51
+ },
52
+ async loadNamespace(namespace, _locale) {
53
+ await instance.loadNamespaces(namespace);
54
+ },
55
+ getCurrentLocale() {
56
+ return instance.language;
57
+ },
58
+ onLocaleChange(cb) {
59
+ instance.on("languageChanged", cb);
60
+ return () => instance.off("languageChanged", cb);
61
+ },
62
+ hasKey(key) {
63
+ const dotIdx = key.indexOf(".");
64
+ const ns = dotIdx === -1 ? void 0 : key.substring(0, dotIdx);
65
+ const subKey = dotIdx === -1 ? key : key.substring(dotIdx + 1);
66
+ const opts = ns === void 0 ? void 0 : { ns };
67
+ if (!instance.exists(subKey, opts)) return false;
68
+ const value = instance.t(subKey, { ...opts ?? {}, returnObjects: true });
69
+ return typeof value !== "object" || value === null;
70
+ }
71
+ };
72
+ }
73
+
74
+ // src/i18n.ts
75
+ function createI18n(config) {
76
+ const { adapter, warnOnMissing = true } = config;
77
+ return {
78
+ t(key, options) {
79
+ const candidates = resolveKey(key, options?.resource);
80
+ for (const candidate of candidates) {
81
+ if (adapter.hasKey(candidate)) {
82
+ return adapter.t(candidate, options?.params);
83
+ }
84
+ }
85
+ if (warnOnMissing && typeof console !== "undefined") {
86
+ console.warn(
87
+ `[@burojs/i18n-core] Missing translation: '${key}'${options?.resource ? ` (resource: ${options.resource})` : ""}`
88
+ );
89
+ }
90
+ return key;
91
+ },
92
+ has(key, options) {
93
+ return resolveKey(key, options?.resource).some((candidate) => adapter.hasKey(candidate));
94
+ },
95
+ async changeLocale(locale) {
96
+ await adapter.changeLocale(locale);
97
+ },
98
+ getCurrentLocale() {
99
+ return adapter.getCurrentLocale();
100
+ },
101
+ onLocaleChange(cb) {
102
+ return adapter.onLocaleChange?.(cb) ?? (() => {
103
+ });
104
+ },
105
+ async loadNamespace(namespace, locale) {
106
+ await adapter.loadNamespace(namespace, locale);
107
+ },
108
+ adapter
109
+ };
110
+ }
111
+
112
+ // src/resource-context.ts
113
+ function createResourceI18n(i18n, resource) {
114
+ return {
115
+ t(key, options) {
116
+ return i18n.t(key, {
117
+ resource: options?.resource ?? resource,
118
+ params: options?.params
119
+ });
120
+ },
121
+ changeLocale: i18n.changeLocale,
122
+ getCurrentLocale: i18n.getCurrentLocale,
123
+ resource
124
+ };
125
+ }
126
+
127
+ // src/locale-registry.ts
128
+ var DEFAULT_NAMESPACE = "common";
129
+ function isPlainObject(value) {
130
+ return typeof value === "object" && value !== null && !Array.isArray(value);
131
+ }
132
+ function mergeBundle(base, overlay) {
133
+ const out = { ...base };
134
+ for (const [key, overlayValue] of Object.entries(overlay)) {
135
+ const baseValue = out[key];
136
+ out[key] = isPlainObject(baseValue) && isPlainObject(overlayValue) ? mergeBundle(baseValue, overlayValue) : overlayValue;
137
+ }
138
+ return out;
139
+ }
140
+ function createLocaleRegistry() {
141
+ const registered = [];
142
+ return {
143
+ register(contribution) {
144
+ registered.push(contribution);
145
+ },
146
+ locales() {
147
+ return Array.from(new Set(registered.map((c) => c.locale))).sort();
148
+ },
149
+ namespaces() {
150
+ return Array.from(new Set(registered.map((c) => c.namespace ?? DEFAULT_NAMESPACE))).sort();
151
+ },
152
+ contributions() {
153
+ return registered;
154
+ },
155
+ buildResources() {
156
+ const byLocale = /* @__PURE__ */ new Map();
157
+ for (const contribution of registered) {
158
+ const namespace = contribution.namespace ?? DEFAULT_NAMESPACE;
159
+ let byNamespace = byLocale.get(contribution.locale);
160
+ if (!byNamespace) {
161
+ byNamespace = /* @__PURE__ */ new Map();
162
+ byLocale.set(contribution.locale, byNamespace);
163
+ }
164
+ const group = byNamespace.get(namespace);
165
+ if (group) {
166
+ group.push(contribution);
167
+ } else {
168
+ byNamespace.set(namespace, [contribution]);
169
+ }
170
+ }
171
+ const resources = {};
172
+ for (const [locale, byNamespace] of byLocale) {
173
+ const namespaces = {};
174
+ for (const [namespace, group] of byNamespace) {
175
+ const ordered = [...group].sort((a, b) => a.priority - b.priority);
176
+ let merged = {};
177
+ for (const contribution of ordered) {
178
+ merged = mergeBundle(merged, contribution.bundle);
179
+ }
180
+ namespaces[namespace] = merged;
181
+ }
182
+ resources[locale] = namespaces;
183
+ }
184
+ return resources;
185
+ }
186
+ };
187
+ }
188
+ var DEFAULT_LOCALE_PATTERN = /(?:^|\/)([A-Za-z0-9-]+)\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
189
+ function defaultLocaleOf(path) {
190
+ return DEFAULT_LOCALE_PATTERN.exec(path)?.[1];
191
+ }
192
+ function localeContributionsFromGlob(glob, options) {
193
+ const localeOf = options.localeOf ?? defaultLocaleOf;
194
+ const out = [];
195
+ for (const [path, mod] of Object.entries(glob)) {
196
+ const locale = localeOf(path);
197
+ if (locale === void 0) continue;
198
+ const bundle = isPlainObject(mod.default) ? mod.default : mod;
199
+ out.push({
200
+ locale,
201
+ priority: options.priority,
202
+ source: options.source,
203
+ bundle,
204
+ ...options.namespace !== void 0 ? { namespace: options.namespace } : {}
205
+ });
206
+ }
207
+ return out;
208
+ }
209
+
210
+ // src/create-i18n-from-registry.ts
211
+ async function createI18nFromRegistry(registry, config) {
212
+ const adapter = await createI18nextAdapter({
213
+ defaultLocale: config.defaultLocale,
214
+ locales: registry.locales(),
215
+ resources: registry.buildResources(),
216
+ ...config.fallbackLocale !== void 0 ? { fallbackLocale: config.fallbackLocale } : {},
217
+ ...config.keySeparator !== void 0 ? { keySeparator: config.keySeparator } : {},
218
+ ...config.nsSeparator !== void 0 ? { nsSeparator: config.nsSeparator } : {}
219
+ });
220
+ return createI18n({ adapter, warnOnMissing: config.warnOnMissing });
221
+ }
222
+
223
+ // src/index.ts
224
+ var PACKAGE_NAME = "@burojs/i18n-core";
225
+ var PACKAGE_VERSION = "0.0.0";
226
+
227
+ export { PACKAGE_NAME, PACKAGE_VERSION, createI18n, createI18nFromRegistry, createI18nextAdapter, createLocaleRegistry, createResourceI18n, localeContributionsFromGlob, resolveKey };
228
+ //# sourceMappingURL=index.js.map
229
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"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"],"names":[],"mappings":"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","file":"index.js","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"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@burojs/i18n-core",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Buro: hierarchical i18n with fallback chain over i18next",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@burojs/core": "0.2.0"
29
+ },
30
+ "peerDependencies": {
31
+ "i18next": ">=23 <27"
32
+ },
33
+ "devDependencies": {
34
+ "expect-type": "^1.1.0",
35
+ "i18next": "^24.0.0",
36
+ "tsup": "^8.3.0",
37
+ "typescript": "^5.5.0",
38
+ "vitest": "^2.1.0",
39
+ "@burojs/tooling-eslint": "0.0.0",
40
+ "@burojs/tooling-tsconfig": "0.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "typecheck": "tsc --noEmit",
49
+ "typecheck:tests": "tsc -p tsconfig.test.json"
50
+ }
51
+ }