@xndrjs/i18n 0.6.0 → 0.7.0-alpha.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.
Files changed (51) hide show
  1. package/README.md +154 -134
  2. package/dist/codegen/index.js.map +1 -1
  3. package/dist/index.d.ts +276 -61
  4. package/dist/index.js +687 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/validation/index.d.ts +23 -7
  7. package/dist/validation/index.js +196 -89
  8. package/dist/validation/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/IcuTranslationProviderMulti.test.ts +115 -215
  11. package/src/IcuTranslationProviderMulti.ts +107 -96
  12. package/src/IcuTranslationProviderSingle.test.ts +36 -294
  13. package/src/IcuTranslationProviderSingle.ts +55 -72
  14. package/src/builder-load-registry.ts +18 -0
  15. package/src/builder-loaders.ts +18 -0
  16. package/src/builder-multi.ts +481 -0
  17. package/src/builder-types.test.ts +80 -0
  18. package/src/builder-types.ts +24 -0
  19. package/src/builder.test.ts +421 -0
  20. package/src/builder.ts +112 -0
  21. package/src/codegen/delivery-artifacts.test.ts +2 -1
  22. package/src/codegen/delivery-artifacts.ts +2 -1
  23. package/src/codegen/emit/dictionary-file.test.ts +0 -7
  24. package/src/codegen/emit/dictionary-schema-file.ts +55 -42
  25. package/src/codegen/emit/instance-file.test.ts +88 -0
  26. package/src/codegen/emit/instance-file.ts +164 -17
  27. package/src/codegen/emit/namespace-loaders-file.test.ts +35 -10
  28. package/src/codegen/emit/namespace-loaders-file.ts +11 -61
  29. package/src/codegen/emit/types-file.test.ts +0 -2
  30. package/src/codegen/generate-i18n-types.test.ts +8 -303
  31. package/src/codegen/generate-i18n-types.ts +16 -1
  32. package/src/codegen/project-locales-set-namespace.test.ts +25 -32
  33. package/src/engine.ts +71 -0
  34. package/src/index.ts +47 -7
  35. package/src/patch-key.test.ts +186 -0
  36. package/src/patch-key.ts +140 -0
  37. package/src/project-locales.test.ts +76 -0
  38. package/src/project-locales.ts +58 -1
  39. package/src/scope-multi.ts +124 -0
  40. package/src/scope-single.ts +85 -0
  41. package/src/scope-types.ts +27 -0
  42. package/src/scope.test.ts +481 -0
  43. package/src/single-builder.ts +153 -0
  44. package/src/types.ts +16 -0
  45. package/src/validation/create-normalized-schema.ts +1 -1
  46. package/src/validation/errors.ts +2 -0
  47. package/src/validation/index.ts +135 -27
  48. package/src/validation/normalize.ts +176 -46
  49. package/src/validation/types.ts +4 -0
  50. package/src/validation/validate-normalized.ts +43 -1
  51. package/src/validation/validation.test.ts +200 -7
package/dist/index.js CHANGED
@@ -1,4 +1,206 @@
1
1
  import { IntlMessageFormat } from 'intl-messageformat';
2
+ import { parse } from '@formatjs/icu-messageformat-parser';
3
+
4
+ // src/builder-loaders.ts
5
+ async function invokeNamespaceLoader(loader, partition) {
6
+ if (partition === void 0) {
7
+ return await loader();
8
+ }
9
+ return await loader(partition);
10
+ }
11
+
12
+ // src/builder-multi.ts
13
+ var I18nBuilderMultiInitialImpl = class {
14
+ constructor(engine, options) {
15
+ this.engine = engine;
16
+ this.options = options;
17
+ }
18
+ engine;
19
+ options;
20
+ withNamespaces(namespaces) {
21
+ return new I18nBuilderMultiReadyImpl(this.engine, this.options, { namespaces });
22
+ }
23
+ };
24
+ var I18nBuilderMultiReadyImpl = class _I18nBuilderMultiReadyImpl {
25
+ constructor(engine, options, state) {
26
+ this.engine = engine;
27
+ this.options = options;
28
+ this.state = state;
29
+ }
30
+ engine;
31
+ options;
32
+ state;
33
+ clone() {
34
+ return {
35
+ namespaces: this.state.namespaces,
36
+ ...this.state.locale !== void 0 ? { locale: this.state.locale } : {},
37
+ ...this.state.deliveryArea !== void 0 ? { deliveryArea: this.state.deliveryArea } : {}
38
+ };
39
+ }
40
+ withNamespaces(namespaces) {
41
+ return new _I18nBuilderMultiReadyImpl(this.engine, this.options, {
42
+ ...this.clone(),
43
+ namespaces
44
+ });
45
+ }
46
+ withLocale(locale) {
47
+ const { deliveryArea: _deliveryArea, ...rest } = this.clone();
48
+ return new I18nBuilderMultiForLocaleImpl(this.engine, this.options, { ...rest, locale });
49
+ }
50
+ withDeliveryArea(area) {
51
+ const { locale: _locale, ...rest } = this.clone();
52
+ return new I18nBuilderMultiPartitionedImpl(this.engine, this.options, { ...rest, deliveryArea: area });
53
+ }
54
+ async load() {
55
+ await applyMultiBuilderLoad(this.engine, this.options, this.state);
56
+ return this.engine.toScope({ namespaces: this.state.namespaces });
57
+ }
58
+ };
59
+ var I18nBuilderMultiPartitionedImpl = class _I18nBuilderMultiPartitionedImpl {
60
+ constructor(engine, options, state) {
61
+ this.engine = engine;
62
+ this.options = options;
63
+ this.state = state;
64
+ }
65
+ engine;
66
+ options;
67
+ state;
68
+ withNamespaces(namespaces) {
69
+ return new _I18nBuilderMultiPartitionedImpl(this.engine, this.options, {
70
+ ...this.state,
71
+ namespaces
72
+ });
73
+ }
74
+ async load() {
75
+ await applyMultiBuilderLoad(this.engine, this.options, this.state);
76
+ return this.engine.toScope({ namespaces: this.state.namespaces });
77
+ }
78
+ };
79
+ var I18nBuilderMultiForLocaleImpl = class {
80
+ constructor(engine, options, state) {
81
+ this.engine = engine;
82
+ this.options = options;
83
+ this.state = state;
84
+ }
85
+ engine;
86
+ options;
87
+ state;
88
+ async load() {
89
+ await applyMultiBuilderLoad(this.engine, this.options, this.state);
90
+ return this.engine.toScope({ namespaces: this.state.namespaces }).forLocale(this.state.locale);
91
+ }
92
+ };
93
+ async function applyMultiBuilderLoad(engine, options, state) {
94
+ if (state.namespaces.length === 0) {
95
+ throw new Error("[i18n] withNamespaces(...) is required before load().");
96
+ }
97
+ const partition = state.locale ?? state.deliveryArea;
98
+ await Promise.all(
99
+ state.namespaces.map(async (namespace) => {
100
+ const loader = options?.namespaceLoaders?.[namespace];
101
+ if (loader === void 0) {
102
+ return;
103
+ }
104
+ if (engine.hasBuilderResourceLoaded(namespace, partition)) {
105
+ return;
106
+ }
107
+ const data = await invokeNamespaceLoader(loader, partition);
108
+ engine.applyLoadMergeNamespace(namespace, data);
109
+ engine.markBuilderResourceLoaded(namespace, partition);
110
+ })
111
+ );
112
+ }
113
+
114
+ // src/single-builder.ts
115
+ var I18nBuilderSingleImpl = class _I18nBuilderSingleImpl {
116
+ constructor(engine, options, state) {
117
+ this.engine = engine;
118
+ this.options = options;
119
+ if (state !== void 0) {
120
+ this.state = {
121
+ ...state.locale !== void 0 ? { locale: state.locale } : {},
122
+ ...state.deliveryArea !== void 0 ? { deliveryArea: state.deliveryArea } : {}
123
+ };
124
+ }
125
+ }
126
+ engine;
127
+ options;
128
+ state = {};
129
+ clone() {
130
+ return {
131
+ ...this.state.locale !== void 0 ? { locale: this.state.locale } : {},
132
+ ...this.state.deliveryArea !== void 0 ? { deliveryArea: this.state.deliveryArea } : {}
133
+ };
134
+ }
135
+ withLocale(locale) {
136
+ const { deliveryArea: _deliveryArea, ...rest } = this.clone();
137
+ return new I18nBuilderSingleForLocaleImpl(
138
+ this.engine,
139
+ this.options,
140
+ { ...rest, locale }
141
+ );
142
+ }
143
+ withDeliveryArea(_area) {
144
+ const { locale: _locale, ...rest } = this.clone();
145
+ return new _I18nBuilderSingleImpl(this.engine, this.options, {
146
+ ...rest,
147
+ deliveryArea: _area
148
+ });
149
+ }
150
+ async load() {
151
+ await applySingleBuilderLoad(this.engine, this.options, this.state);
152
+ return this.engine.toScope();
153
+ }
154
+ };
155
+ var I18nBuilderSingleForLocaleImpl = class {
156
+ constructor(engine, options, state) {
157
+ this.engine = engine;
158
+ this.options = options;
159
+ this.state = state;
160
+ }
161
+ engine;
162
+ options;
163
+ state;
164
+ async load() {
165
+ await applySingleBuilderLoad(this.engine, this.options, this.state);
166
+ return this.engine.toScope({ locale: this.state.locale });
167
+ }
168
+ };
169
+ async function applySingleBuilderLoad(engine, options, state) {
170
+ const partition = state.locale ?? state.deliveryArea;
171
+ const loader = options?.dictionaryLoader;
172
+ if (loader !== void 0) {
173
+ if (engine.hasBuilderResourceLoaded(partition)) {
174
+ return;
175
+ }
176
+ const data = await invokeNamespaceLoader(loader, partition);
177
+ engine.applyLoadMergeSingle(data);
178
+ engine.markBuilderResourceLoaded(partition);
179
+ }
180
+ }
181
+
182
+ // src/builder.ts
183
+ function isMultiEngine(engine) {
184
+ return engine.__i18nEngineMode === "multi";
185
+ }
186
+ function createI18nSingleBuilder(engine, options) {
187
+ return new I18nBuilderSingleImpl(engine, options);
188
+ }
189
+ function createI18nMultiBuilder(engine, options) {
190
+ return new I18nBuilderMultiInitialImpl(engine, options);
191
+ }
192
+ function createI18nBuilder(engine, options) {
193
+ if (isMultiEngine(engine)) {
194
+ return createI18nMultiBuilder(
195
+ engine,
196
+ options
197
+ );
198
+ }
199
+ return createI18nSingleBuilder(
200
+ engine,
201
+ options
202
+ );
203
+ }
2
204
 
3
205
  // src/deep-freeze.ts
4
206
  function deepFreeze(value) {
@@ -18,6 +220,21 @@ function cloneAndFreeze(value) {
18
220
  return deepFreeze(structuredClone(value));
19
221
  }
20
222
 
223
+ // src/builder-load-registry.ts
224
+ function formatBuilderResourceKey(namespace, partition) {
225
+ return `${namespace}\0${partition ?? ""}`;
226
+ }
227
+ var SINGLE_BUILDER_RESOURCE = "__dictionary__";
228
+ var BuilderLoadRegistry = class {
229
+ loaded = /* @__PURE__ */ new Set();
230
+ has(namespace, partition) {
231
+ return this.loaded.has(formatBuilderResourceKey(namespace, partition));
232
+ }
233
+ mark(namespace, partition) {
234
+ this.loaded.add(formatBuilderResourceKey(namespace, partition));
235
+ }
236
+ };
237
+
21
238
  // src/resolve-locale.ts
22
239
  function validateLocaleFallback(map) {
23
240
  for (const startLocale of Object.keys(map)) {
@@ -143,153 +360,238 @@ function resolveAndFormat(options) {
143
360
  }
144
361
  }
145
362
 
146
- // src/IcuTranslationProviderSingle.ts
147
- var IcuTranslationProviderSingleForLocale = class {
148
- constructor(provider, locale) {
149
- this.provider = provider;
150
- this.locale = locale;
363
+ // src/icu/extract-variables.ts
364
+ function isPluralOrOrdinalNode(node) {
365
+ return node.type === 6 && "pluralType" in node && Boolean(node.pluralType);
366
+ }
367
+ function hasVariableName(node) {
368
+ return "value" in node && typeof node.value === "string";
369
+ }
370
+ function addVariableMeta(variables, name, type, role) {
371
+ const existing = variables[name];
372
+ if (!existing) {
373
+ variables[name] = { type, roles: /* @__PURE__ */ new Set([role]) };
374
+ return;
375
+ }
376
+ existing.roles.add(role);
377
+ existing.type = mergeVariableTypes(existing.type, type);
378
+ }
379
+ function mergeVariableTypes(left, right) {
380
+ if (left === right) {
381
+ return left;
151
382
  }
152
- provider;
153
- locale;
154
- get(key, ...args) {
155
- const params = args[0];
156
- return this.provider.getWithLocale(String(key), this.locale, params);
383
+ if (left === "string" && right === "number" || left === "number" && right === "string") {
384
+ return "number";
157
385
  }
158
- };
159
- var IcuTranslationProviderSingle = class {
160
- dictionary;
161
- compiledCache = {};
162
- localeFallback;
163
- onMissing;
164
- constructor(dictionary, options) {
165
- this.dictionary = structuredClone(dictionary);
166
- if (options?.localeFallback) {
167
- validateLocaleFallback(options.localeFallback);
168
- this.localeFallback = options.localeFallback;
386
+ return right;
387
+ }
388
+ function extractVariableMeta(nodes) {
389
+ const variables = {};
390
+ const walk = (walkNodes) => {
391
+ for (const node of walkNodes) {
392
+ if (node.type === 1 && hasVariableName(node)) {
393
+ addVariableMeta(variables, node.value, "string", "simple");
394
+ } else if (node.type === 2 && hasVariableName(node)) {
395
+ addVariableMeta(variables, node.value, "number", "number");
396
+ } else if (isPluralOrOrdinalNode(node) && hasVariableName(node)) {
397
+ const role = node.pluralType === "ordinal" ? "selectordinal" : "plural";
398
+ addVariableMeta(variables, node.value, "number", role);
399
+ } else if (node.type === 3 && hasVariableName(node)) {
400
+ addVariableMeta(variables, node.value, "date", "date");
401
+ } else if (node.type === 4 && hasVariableName(node)) {
402
+ addVariableMeta(variables, node.value, "date", "time");
403
+ } else if (node.type === 5 && hasVariableName(node)) {
404
+ addVariableMeta(variables, node.value, "string", "select");
405
+ }
406
+ if ("options" in node && node.options) {
407
+ for (const option of Object.values(node.options)) {
408
+ walk(option.value);
409
+ }
410
+ }
411
+ if ("children" in node && Array.isArray(node.children)) {
412
+ walk(node.children);
413
+ }
169
414
  }
170
- this.onMissing = options?.onMissing ?? "throw";
415
+ };
416
+ walk(nodes);
417
+ return variables;
418
+ }
419
+ function variableMetaToSpec(meta) {
420
+ return Object.fromEntries(Object.entries(meta).map(([name, entry]) => [name, entry.type]));
421
+ }
422
+ function inferMergedVariableType(roles) {
423
+ const hasPlural = roles.has("plural");
424
+ const hasSelectordinal = roles.has("selectordinal");
425
+ const hasSelect = roles.has("select");
426
+ const hasSimple = roles.has("simple");
427
+ const hasNumber = roles.has("number");
428
+ const hasDate = roles.has("date");
429
+ const hasTime = roles.has("time");
430
+ if (hasPlural && hasSelectordinal) {
431
+ return "CONFLICT";
432
+ }
433
+ if (hasSelect && (hasPlural || hasSelectordinal || hasNumber)) {
434
+ return "CONFLICT";
435
+ }
436
+ if (hasPlural && hasSelect) {
437
+ return "CONFLICT";
438
+ }
439
+ if (hasSelectordinal && hasSelect) {
440
+ return "CONFLICT";
441
+ }
442
+ if (hasDate || hasTime) {
443
+ if (hasPlural || hasSelect || hasSelectordinal || hasSimple || hasNumber) {
444
+ return "CONFLICT";
445
+ }
446
+ return "date";
171
447
  }
172
- get(key, locale, ...args) {
173
- const params = args[0];
174
- return this.getWithLocale(String(key), locale, params);
448
+ if (hasPlural || hasSelectordinal || hasNumber) {
449
+ return "number";
175
450
  }
176
- getWithLocale(key, locale, params) {
177
- return resolveAndFormat({
178
- localeByKey: this.dictionary[key],
179
- locale,
180
- params,
181
- getCache: (resolvedLocale) => {
182
- if (!this.compiledCache[resolvedLocale]) {
183
- this.compiledCache[resolvedLocale] = {};
184
- }
185
- return this.compiledCache[resolvedLocale];
186
- },
187
- context: {
188
- key,
189
- locale,
190
- localeFallback: this.localeFallback,
191
- onMissing: this.onMissing
192
- }
193
- });
451
+ if (hasSelect || hasSimple) {
452
+ return "string";
194
453
  }
195
- forLocale(locale) {
196
- return new IcuTranslationProviderSingleForLocale(this, locale);
454
+ return "string";
455
+ }
456
+ function mergeVariableMetaAcrossLocales(localeMetas) {
457
+ const keySignatures = localeMetas.map((meta) => Object.keys(meta).sort().join(","));
458
+ if (new Set(keySignatures).size > 1) {
459
+ return {
460
+ ok: false,
461
+ message: `Inconsistent ICU variable names across locales (keys: ${keySignatures.join(" vs ")})`
462
+ };
197
463
  }
198
- getAll() {
199
- return cloneAndFreeze(this.dictionary);
464
+ const merged = {};
465
+ const variableNames = new Set(localeMetas.flatMap((meta) => Object.keys(meta)));
466
+ for (const variableName of variableNames) {
467
+ const combinedRoles = /* @__PURE__ */ new Set();
468
+ for (const meta of localeMetas) {
469
+ for (const role of meta[variableName]?.roles ?? []) {
470
+ combinedRoles.add(role);
471
+ }
472
+ }
473
+ const mergedType = inferMergedVariableType(combinedRoles);
474
+ if (mergedType === "CONFLICT") {
475
+ const rolesByLocale = localeMetas.map((meta) => {
476
+ const entry = meta[variableName];
477
+ if (!entry) {
478
+ return null;
479
+ }
480
+ return [...entry.roles].sort().join("|");
481
+ }).filter((value) => value !== null);
482
+ return {
483
+ ok: false,
484
+ message: `Incompatible ICU variable "${variableName}" across locales (roles: ${rolesByLocale.join(" vs ")})`
485
+ };
486
+ }
487
+ merged[variableName] = mergedType;
200
488
  }
201
- setAll(values) {
202
- this.dictionary = structuredClone(values);
203
- this.compiledCache = {};
489
+ return { ok: true, merged };
490
+ }
491
+ function parseTemplate(template) {
492
+ try {
493
+ const ast = parse(template);
494
+ const meta = extractVariableMeta(ast);
495
+ return { ok: true, args: variableMetaToSpec(meta), meta };
496
+ } catch (error) {
497
+ const message = error instanceof Error ? error.message : String(error);
498
+ return { ok: false, message };
204
499
  }
205
- };
500
+ }
206
501
 
207
- // src/IcuTranslationProviderMulti.ts
208
- var IcuTranslationProviderMultiForLocale = class {
209
- constructor(provider, locale) {
210
- this.provider = provider;
211
- this.locale = locale;
502
+ // src/patch-key.ts
503
+ function preloadKeySingle(key, locale) {
504
+ return `${key}:${locale}`;
505
+ }
506
+ function preloadKeyMulti(namespace, key, locale) {
507
+ return `${namespace}:${key}:${locale}`;
508
+ }
509
+ function seedPreloadedKeysSingle(dictionary, preloadedKeys) {
510
+ for (const [key, localeByKey] of Object.entries(dictionary)) {
511
+ if (localeByKey === void 0 || typeof localeByKey !== "object") {
512
+ continue;
513
+ }
514
+ for (const locale of Object.keys(localeByKey)) {
515
+ preloadedKeys.add(preloadKeySingle(key, locale));
516
+ }
212
517
  }
213
- provider;
214
- locale;
215
- get(namespace, key, ...args) {
216
- const params = args[0];
217
- return this.provider.getWithLocale(String(namespace), String(key), this.locale, params);
518
+ }
519
+ function seedPreloadedKeysMulti(dictionary, preloadedKeys) {
520
+ for (const [namespace, keyDictionary] of Object.entries(dictionary)) {
521
+ if (keyDictionary === void 0 || typeof keyDictionary !== "object") {
522
+ continue;
523
+ }
524
+ for (const [key, localeByKey] of Object.entries(keyDictionary)) {
525
+ if (localeByKey === void 0 || typeof localeByKey !== "object") {
526
+ continue;
527
+ }
528
+ for (const locale of Object.keys(localeByKey)) {
529
+ preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
530
+ }
531
+ }
218
532
  }
219
- };
220
- var IcuTranslationProviderMulti = class {
221
- dictionary;
222
- compiledCache = {};
223
- localeFallback;
224
- onMissing;
225
- loadedNamespaces;
226
- constructor(dictionary, options) {
227
- this.dictionary = structuredClone(dictionary);
228
- this.loadedNamespaces = new Set(Object.keys(dictionary));
229
- if (options?.localeFallback) {
230
- validateLocaleFallback(options.localeFallback);
231
- this.localeFallback = options.localeFallback;
533
+ }
534
+ function recordPreloadedKeysSingle(partial, preloadedKeys) {
535
+ for (const [key, incomingLocales] of Object.entries(partial)) {
536
+ if (incomingLocales === void 0 || typeof incomingLocales !== "object") {
537
+ continue;
538
+ }
539
+ for (const locale of Object.keys(incomingLocales)) {
540
+ preloadedKeys.add(preloadKeySingle(key, locale));
232
541
  }
233
- this.onMissing = options?.onMissing ?? "throw";
234
542
  }
235
- get(namespace, key, locale, ...args) {
236
- const params = args[0];
237
- return this.getWithLocale(String(namespace), String(key), locale, params);
543
+ }
544
+ function recordPreloadedKeysMulti(namespace, partial, preloadedKeys) {
545
+ for (const [key, incomingLocales] of Object.entries(partial)) {
546
+ if (incomingLocales === void 0 || typeof incomingLocales !== "object") {
547
+ continue;
548
+ }
549
+ for (const locale of Object.keys(incomingLocales)) {
550
+ preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
551
+ }
238
552
  }
239
- getWithLocale(namespace, key, locale, params) {
240
- if (!this.loadedNamespaces.has(namespace)) {
241
- throw new Error(
242
- `[i18n] Namespace not loaded: "${namespace}". Register it with setNamespace() before calling .get().`
243
- );
553
+ }
554
+ function collectLocaleMetasForKey(localeByKey, patchedLocale, patchedMeta) {
555
+ const localeMetas = [patchedMeta];
556
+ for (const [locale, template] of Object.entries(localeByKey)) {
557
+ if (locale === patchedLocale) {
558
+ continue;
559
+ }
560
+ const parsed = parseTemplate(template);
561
+ if (parsed.ok) {
562
+ localeMetas.push(parsed.meta);
244
563
  }
245
- const localeByKey = this.dictionary[namespace]?.[key];
246
- return resolveAndFormat({
247
- localeByKey,
248
- locale,
249
- params,
250
- getCache: (resolvedLocale) => {
251
- if (!this.compiledCache[resolvedLocale]) {
252
- this.compiledCache[resolvedLocale] = {};
253
- }
254
- if (!this.compiledCache[resolvedLocale][namespace]) {
255
- this.compiledCache[resolvedLocale][namespace] = {};
256
- }
257
- return this.compiledCache[resolvedLocale][namespace];
258
- },
259
- context: {
260
- namespace,
261
- key,
262
- locale,
263
- localeFallback: this.localeFallback,
264
- onMissing: this.onMissing
265
- }
266
- });
267
564
  }
268
- forLocale(locale) {
269
- return new IcuTranslationProviderMultiForLocale(this, locale);
565
+ return localeMetas;
566
+ }
567
+ function assertPatchKeySingle(key, locale, template, preloadedKeys, localeByKey) {
568
+ if (!preloadedKeys.has(preloadKeySingle(key, locale))) {
569
+ throw new Error(`[i18n] Key not preloaded: ${key} (${locale})`);
270
570
  }
271
- getAll() {
272
- return cloneAndFreeze(this.dictionary);
571
+ const parsed = parseTemplate(template);
572
+ if (!parsed.ok) {
573
+ throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
273
574
  }
274
- hasNamespace(namespace) {
275
- return this.loadedNamespaces.has(namespace);
575
+ const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
576
+ const merged = mergeVariableMetaAcrossLocales(localeMetas);
577
+ if (!merged.ok) {
578
+ throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
276
579
  }
277
- setAll(values) {
278
- this.dictionary = structuredClone(values);
279
- this.compiledCache = {};
280
- this.loadedNamespaces = new Set(Object.keys(values));
580
+ }
581
+ function assertPatchKeyMulti(namespace, key, locale, template, preloadedKeys, localeByKey) {
582
+ if (!preloadedKeys.has(preloadKeyMulti(namespace, key, locale))) {
583
+ throw new Error(`[i18n] Key not preloaded: ${namespace}.${key} (${locale})`);
281
584
  }
282
- setNamespace(namespace, values) {
283
- this.dictionary = {
284
- ...this.dictionary,
285
- [namespace]: structuredClone(values)
286
- };
287
- this.loadedNamespaces.add(namespace);
288
- for (const locale of Object.keys(this.compiledCache)) {
289
- delete this.compiledCache[locale]?.[namespace];
290
- }
585
+ const parsed = parseTemplate(template);
586
+ if (!parsed.ok) {
587
+ throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
291
588
  }
292
- };
589
+ const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
590
+ const merged = mergeVariableMetaAcrossLocales(localeMetas);
591
+ if (!merged.ok) {
592
+ throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
593
+ }
594
+ }
293
595
 
294
596
  // src/project-locales.ts
295
597
  function classifyAreaLocales(areaLocales, localeFallback) {
@@ -380,7 +682,266 @@ function projectDictionaryLocalesCore(dictionary, locales, localeFallback) {
380
682
  }
381
683
  return result;
382
684
  }
685
+ function mergeNamespaceLocalesCore(existing, incoming) {
686
+ const merged = structuredClone(existing);
687
+ for (const [key, incomingLocales] of Object.entries(incoming)) {
688
+ if (incomingLocales === void 0 || typeof incomingLocales !== "object") {
689
+ continue;
690
+ }
691
+ merged[key] = {
692
+ ...merged[key] ?? {},
693
+ ...incomingLocales
694
+ };
695
+ }
696
+ return merged;
697
+ }
698
+ function mergeDictionaryLocalesCore(existing, incoming) {
699
+ const merged = structuredClone(existing);
700
+ for (const namespace of Object.keys(incoming)) {
701
+ const incomingNamespace = incoming[namespace];
702
+ if (incomingNamespace === void 0) {
703
+ continue;
704
+ }
705
+ merged[namespace] = mergeNamespaceLocalesCore(
706
+ merged[namespace] ?? {},
707
+ incomingNamespace
708
+ );
709
+ }
710
+ return merged;
711
+ }
712
+
713
+ // src/scope-single.ts
714
+ var I18nScopeSingleImpl = class {
715
+ constructor(engine) {
716
+ this.engine = engine;
717
+ }
718
+ engine;
719
+ t = (key, locale, ...args) => {
720
+ const params = args[0];
721
+ return this.engine.getWithLocale(String(key), locale, params);
722
+ };
723
+ forLocale = (locale) => {
724
+ return new I18nScopeSingleForLocaleImpl(this.engine, locale);
725
+ };
726
+ };
727
+ var I18nScopeSingleForLocaleImpl = class _I18nScopeSingleForLocaleImpl {
728
+ constructor(engine, locale) {
729
+ this.engine = engine;
730
+ this.locale = locale;
731
+ }
732
+ engine;
733
+ locale;
734
+ t = (key, ...args) => {
735
+ const params = args[0];
736
+ return this.engine.getWithLocale(String(key), this.locale, params);
737
+ };
738
+ set = (key, template) => {
739
+ this.engine.patchKey(String(key), this.locale, template);
740
+ };
741
+ forLocale = (locale) => {
742
+ return new _I18nScopeSingleForLocaleImpl(this.engine, locale);
743
+ };
744
+ };
745
+
746
+ // src/IcuTranslationProviderSingle.ts
747
+ var IcuTranslationProviderSingle = class {
748
+ __i18nEngineMode = "single";
749
+ dictionary;
750
+ compiledCache = {};
751
+ preloadedKeys = /* @__PURE__ */ new Set();
752
+ builderLoadRegistry = new BuilderLoadRegistry();
753
+ localeFallback;
754
+ onMissing;
755
+ constructor(dictionary, options) {
756
+ this.dictionary = structuredClone(dictionary);
757
+ seedPreloadedKeysSingle(this.dictionary, this.preloadedKeys);
758
+ if (options?.localeFallback) {
759
+ validateLocaleFallback(options.localeFallback);
760
+ this.localeFallback = options.localeFallback;
761
+ }
762
+ this.onMissing = options?.onMissing ?? "throw";
763
+ }
764
+ getWithLocale(key, locale, params) {
765
+ return resolveAndFormat({
766
+ localeByKey: this.dictionary[key],
767
+ locale,
768
+ params,
769
+ getCache: (resolvedLocale) => {
770
+ if (!this.compiledCache[resolvedLocale]) {
771
+ this.compiledCache[resolvedLocale] = {};
772
+ }
773
+ return this.compiledCache[resolvedLocale];
774
+ },
775
+ context: {
776
+ key,
777
+ locale,
778
+ localeFallback: this.localeFallback,
779
+ onMissing: this.onMissing
780
+ }
781
+ });
782
+ }
783
+ toScope(options) {
784
+ if (options?.locale !== void 0) {
785
+ return new I18nScopeSingleForLocaleImpl(this, options.locale);
786
+ }
787
+ return new I18nScopeSingleImpl(this);
788
+ }
789
+ getAll() {
790
+ return cloneAndFreeze(this.dictionary);
791
+ }
792
+ hasBuilderResourceLoaded(partition) {
793
+ return this.builderLoadRegistry.has(SINGLE_BUILDER_RESOURCE, partition);
794
+ }
795
+ markBuilderResourceLoaded(partition) {
796
+ this.builderLoadRegistry.mark(SINGLE_BUILDER_RESOURCE, partition);
797
+ }
798
+ applyLoadMergeSingle(values) {
799
+ this.dictionary = mergeNamespaceLocalesCore(this.dictionary, values);
800
+ recordPreloadedKeysSingle(values, this.preloadedKeys);
801
+ for (const [key, incomingLocales] of Object.entries(values)) {
802
+ if (incomingLocales === void 0 || typeof incomingLocales !== "object") {
803
+ continue;
804
+ }
805
+ for (const locale of Object.keys(incomingLocales)) {
806
+ delete this.compiledCache[locale]?.[key];
807
+ }
808
+ }
809
+ }
810
+ patchKey(key, locale, template) {
811
+ assertPatchKeySingle(key, locale, template, this.preloadedKeys, this.dictionary[key]);
812
+ this.dictionary = mergeNamespaceLocalesCore(this.dictionary, {
813
+ [key]: { [locale]: template }
814
+ });
815
+ delete this.compiledCache[locale]?.[key];
816
+ }
817
+ };
818
+
819
+ // src/scope-multi.ts
820
+ var I18nScopeMultiImpl = class {
821
+ constructor(engine) {
822
+ this.engine = engine;
823
+ }
824
+ engine;
825
+ t = (namespace, key, locale, ...args) => {
826
+ const params = args[0];
827
+ return this.engine.getWithLocale(String(namespace), String(key), locale, params);
828
+ };
829
+ forLocale = (locale) => {
830
+ return new I18nScopeMultiForLocaleImpl(this.engine, locale);
831
+ };
832
+ };
833
+ var I18nScopeMultiForLocaleImpl = class _I18nScopeMultiForLocaleImpl {
834
+ constructor(engine, locale) {
835
+ this.engine = engine;
836
+ this.locale = locale;
837
+ }
838
+ engine;
839
+ locale;
840
+ t = (namespace, key, ...args) => {
841
+ const params = args[0];
842
+ return this.engine.getWithLocale(String(namespace), String(key), this.locale, params);
843
+ };
844
+ set = (namespace, key, template) => {
845
+ this.engine.patchKeyMulti(String(namespace), String(key), this.locale, template);
846
+ };
847
+ forLocale = (locale) => {
848
+ return new _I18nScopeMultiForLocaleImpl(this.engine, locale);
849
+ };
850
+ };
851
+
852
+ // src/IcuTranslationProviderMulti.ts
853
+ var IcuTranslationProviderMulti = class {
854
+ __i18nEngineMode = "multi";
855
+ dictionary;
856
+ compiledCache = {};
857
+ preloadedKeys = /* @__PURE__ */ new Set();
858
+ builderLoadRegistry = new BuilderLoadRegistry();
859
+ localeFallback;
860
+ onMissing;
861
+ constructor(dictionary, options) {
862
+ this.dictionary = structuredClone(dictionary);
863
+ seedPreloadedKeysMulti(this.dictionary, this.preloadedKeys);
864
+ if (options?.localeFallback) {
865
+ validateLocaleFallback(options.localeFallback);
866
+ this.localeFallback = options.localeFallback;
867
+ }
868
+ this.onMissing = options?.onMissing ?? "throw";
869
+ }
870
+ getWithLocale(namespace, key, locale, params) {
871
+ const localeByKey = this.dictionary[namespace]?.[key];
872
+ return resolveAndFormat({
873
+ localeByKey,
874
+ locale,
875
+ params,
876
+ getCache: (resolvedLocale) => {
877
+ if (!this.compiledCache[resolvedLocale]) {
878
+ this.compiledCache[resolvedLocale] = {};
879
+ }
880
+ if (!this.compiledCache[resolvedLocale][namespace]) {
881
+ this.compiledCache[resolvedLocale][namespace] = {};
882
+ }
883
+ return this.compiledCache[resolvedLocale][namespace];
884
+ },
885
+ context: {
886
+ namespace,
887
+ key,
888
+ locale,
889
+ localeFallback: this.localeFallback,
890
+ onMissing: this.onMissing
891
+ }
892
+ });
893
+ }
894
+ toScope(options) {
895
+ if (options.locale !== void 0) {
896
+ return new I18nScopeMultiForLocaleImpl(this, options.locale);
897
+ }
898
+ return new I18nScopeMultiImpl(this);
899
+ }
900
+ getAll() {
901
+ return cloneAndFreeze(this.dictionary);
902
+ }
903
+ hasBuilderResourceLoaded(namespace, partition) {
904
+ return this.builderLoadRegistry.has(namespace, partition);
905
+ }
906
+ markBuilderResourceLoaded(namespace, partition) {
907
+ this.builderLoadRegistry.mark(namespace, partition);
908
+ }
909
+ applyLoadMergeNamespace(namespace, values) {
910
+ const existing = this.dictionary[namespace];
911
+ const merged = mergeNamespaceLocalesCore(existing ?? {}, values);
912
+ this.dictionary = {
913
+ ...this.dictionary,
914
+ [namespace]: merged
915
+ };
916
+ recordPreloadedKeysMulti(namespace, values, this.preloadedKeys);
917
+ for (const [key, incomingLocales] of Object.entries(values)) {
918
+ if (incomingLocales === void 0 || typeof incomingLocales !== "object") {
919
+ continue;
920
+ }
921
+ for (const locale of Object.keys(incomingLocales)) {
922
+ delete this.compiledCache[locale]?.[namespace]?.[key];
923
+ }
924
+ }
925
+ }
926
+ applyLoadMergeAll(values) {
927
+ for (const namespace of Object.keys(values)) {
928
+ const incoming = values[namespace];
929
+ if (incoming !== void 0) {
930
+ this.applyLoadMergeNamespace(namespace, incoming);
931
+ }
932
+ }
933
+ }
934
+ patchKeyMulti(namespace, key, locale, template) {
935
+ const namespaceDictionary = this.dictionary[namespace];
936
+ const localeByKey = namespaceDictionary?.[key];
937
+ assertPatchKeyMulti(namespace, key, locale, template, this.preloadedKeys, localeByKey);
938
+ this.applyLoadMergeNamespace(
939
+ namespace,
940
+ { [key]: { [locale]: template } }
941
+ );
942
+ }
943
+ };
383
944
 
384
- export { IcuTranslationProviderMulti, IcuTranslationProviderSingle, formatLocaleFallbackChain, projectDictionaryForDeliveryAreaCore, projectDictionaryLocalesCore, projectNamespaceForDeliveryAreaCore, projectNamespaceLocalesCore, resolveLocaleTemplate, validateLocaleFallback };
945
+ export { IcuTranslationProviderMulti, IcuTranslationProviderSingle, createI18nBuilder, createI18nMultiBuilder, createI18nSingleBuilder, formatLocaleFallbackChain, invokeNamespaceLoader, mergeDictionaryLocalesCore, mergeNamespaceLocalesCore, projectDictionaryForDeliveryAreaCore, projectDictionaryLocalesCore, projectNamespaceForDeliveryAreaCore, projectNamespaceLocalesCore, resolveLocaleTemplate, validateLocaleFallback };
385
946
  //# sourceMappingURL=index.js.map
386
947
  //# sourceMappingURL=index.js.map