@xndrjs/i18n 0.7.0 → 0.8.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 (69) hide show
  1. package/README.md +67 -872
  2. package/dist/codegen/index.d.ts +117 -51
  3. package/dist/codegen/index.js +1585 -93
  4. package/dist/codegen/index.js.map +1 -1
  5. package/dist/index.d.ts +100 -234
  6. package/dist/index.js +74 -638
  7. package/dist/index.js.map +1 -1
  8. package/dist/validation/index.d.ts +5 -0
  9. package/dist/validation/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/IcuTranslationProviderMulti.test.ts +18 -40
  12. package/src/IcuTranslationProviderMulti.ts +27 -37
  13. package/src/audit/audit-dictionaries.ts +1 -1
  14. package/src/audit/run-audit.test.ts +6 -7
  15. package/src/builder-load-registry.ts +26 -5
  16. package/src/builder-loaders.ts +8 -11
  17. package/src/builder-types.test.ts +11 -42
  18. package/src/builder.test.ts +82 -243
  19. package/src/builder.ts +8 -110
  20. package/src/codegen/codegen-config-schema.ts +82 -77
  21. package/src/codegen/config.test.ts +55 -143
  22. package/src/codegen/config.ts +10 -67
  23. package/src/codegen/dictionary-spec-contract.test.ts +28 -0
  24. package/src/codegen/dictionary-spec-contract.ts +138 -0
  25. package/src/codegen/emit/dictionary-schema-file.ts +7 -43
  26. package/src/codegen/emit/instance-file.test.ts +35 -52
  27. package/src/codegen/emit/instance-file.ts +170 -268
  28. package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
  29. package/src/codegen/emit/namespace-loaders-file.ts +147 -90
  30. package/src/codegen/emit/types-file.test.ts +20 -41
  31. package/src/codegen/emit/types-file.ts +48 -74
  32. package/src/codegen/generate-i18n-types.test.ts +163 -492
  33. package/src/codegen/generate-i18n-types.ts +7 -269
  34. package/src/codegen/paths.ts +0 -14
  35. package/src/codegen/project-locales-set-namespace.test.ts +44 -86
  36. package/src/codegen/read-dictionary.test.ts +27 -9
  37. package/src/codegen/read-dictionary.ts +18 -40
  38. package/src/codegen/regenerate-namespaces.ts +180 -0
  39. package/src/codegen/run-codegen.test.ts +230 -0
  40. package/src/codegen/run-codegen.ts +252 -0
  41. package/src/codegen/types.ts +0 -6
  42. package/src/codegen-config/build-config.ts +4 -23
  43. package/src/codegen-config/codegen-config.test.ts +10 -6
  44. package/src/codegen-config/index.ts +18 -3
  45. package/src/engine.ts +3 -27
  46. package/src/fetch-artifact.ts +16 -0
  47. package/src/i18n-handle.ts +202 -0
  48. package/src/index.ts +15 -34
  49. package/src/project-locales.ts +2 -2
  50. package/src/scope-multi.ts +1 -20
  51. package/src/scope-types.ts +14 -6
  52. package/src/scope.test.ts +3 -312
  53. package/src/serialized-state.test.ts +114 -0
  54. package/src/serialized-state.ts +28 -0
  55. package/src/setup/setup-i18n.test.ts +22 -33
  56. package/src/setup/setup-i18n.ts +22 -27
  57. package/src/types.ts +3 -3
  58. package/src/validation/index.ts +4 -0
  59. package/src/IcuTranslationProviderSingle.test.ts +0 -177
  60. package/src/IcuTranslationProviderSingle.ts +0 -114
  61. package/src/builder-multi.ts +0 -481
  62. package/src/codegen/emit/dictionary-file.test.ts +0 -215
  63. package/src/codegen/emit/dictionary-file.ts +0 -244
  64. package/src/deep-freeze.test.ts +0 -40
  65. package/src/deep-freeze.ts +0 -22
  66. package/src/patch-key.test.ts +0 -186
  67. package/src/patch-key.ts +0 -140
  68. package/src/scope-single.ts +0 -85
  69. package/src/single-builder.ts +0 -153
@@ -1,244 +0,0 @@
1
- import path from "node:path";
2
- import {
3
- GENERATED_FILE_BANNER,
4
- toImportIdentifier,
5
- toImportPath,
6
- toLocaleObjectKey,
7
- toModuleBasename,
8
- toRelativeModuleImport,
9
- } from "../paths.js";
10
- import type { DeliveryMode } from "../codegen-config-schema.js";
11
- import type { ImportExtension, NamespaceEntry } from "../types.js";
12
-
13
- /**
14
- * Emits the generated `dictionary.generated.ts` module: eager translation imports
15
- * wired into `defaultDictionary` or `defaultDictionaryFor`, depending on delivery mode.
16
- */
17
- export interface DictionaryFileOptions {
18
- isSingle: boolean;
19
- hasLazy: boolean;
20
- entries: NamespaceEntry[];
21
- eagerEntries: NamespaceEntry[];
22
- projectRoot: string;
23
- dictionaryOutputPath: string;
24
- typesOutputPath: string;
25
- schemaTypeName: string;
26
- localeTypeName: string;
27
- importExtension: ImportExtension;
28
- delivery?: DeliveryMode;
29
- splitPathsByNamespace?: Record<string, Record<string, string>>;
30
- requestLocales?: readonly string[];
31
- deliveryAreaTypeName?: string;
32
- deliveryAreaNames?: readonly string[];
33
- }
34
-
35
- function toArtifactIdentifierSuffix(artifactKey: string): string {
36
- return artifactKey
37
- .split(/[^a-zA-Z0-9]+/)
38
- .filter(Boolean)
39
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
40
- .join("");
41
- }
42
-
43
- function toSplitImportIdentifier(namespace: string, artifactKey: string): string {
44
- const namespaceId = toImportIdentifier(namespace).replace(/Ns$/, "");
45
- return `${namespaceId}${toArtifactIdentifierSuffix(artifactKey)}`;
46
- }
47
-
48
- function toNamespaceBaseIdentifier(namespace: string): string {
49
- return toImportIdentifier(namespace).replace(/Ns$/, "");
50
- }
51
-
52
- interface PartitionedDictionaryParams {
53
- partitionKeys: readonly string[];
54
- paramName: string;
55
- paramTypeName: string;
56
- }
57
-
58
- function toPartitionSuffix(paramName: string): string {
59
- return `By${paramName.charAt(0).toUpperCase()}${paramName.slice(1)}`;
60
- }
61
-
62
- /**
63
- * Emits dictionary source for split-by-locale and custom delivery.
64
- * Imports one JSON file per namespace × partition key (locale or area) and
65
- * exposes `defaultDictionaryFor(partition)` that selects the matching slice.
66
- */
67
- function formatPartitionedDictionaryFile(
68
- options: DictionaryFileOptions,
69
- { partitionKeys, paramName, paramTypeName }: PartitionedDictionaryParams
70
- ): string | null {
71
- const {
72
- isSingle,
73
- hasLazy,
74
- eagerEntries,
75
- projectRoot,
76
- dictionaryOutputPath,
77
- typesOutputPath,
78
- schemaTypeName,
79
- importExtension,
80
- splitPathsByNamespace = {},
81
- } = options;
82
-
83
- const partitionSuffix = toPartitionSuffix(paramName);
84
- const typesModule = toModuleBasename(typesOutputPath);
85
- const typesImport = toRelativeModuleImport(typesModule, importExtension);
86
- const dictionaryTypeName = hasLazy ? "InitialSchema" : schemaTypeName;
87
- const schemaTypeImport = hasLazy ? `, ${schemaTypeName}` : "";
88
-
89
- if (eagerEntries.length === 0) {
90
- return null;
91
- }
92
-
93
- const imports = eagerEntries
94
- .flatMap((entry) =>
95
- partitionKeys.map((partitionKey) => {
96
- const splitRelativePath = splitPathsByNamespace[entry.namespace]?.[partitionKey];
97
- if (!splitRelativePath) {
98
- throw new Error(
99
- `[Codegen Error] Missing split path for namespace "${entry.namespace}", ${paramName} "${partitionKey}".`
100
- );
101
- }
102
- const importPath = toImportPath(
103
- dictionaryOutputPath,
104
- path.resolve(projectRoot, splitRelativePath)
105
- );
106
- const importId = toSplitImportIdentifier(entry.namespace, partitionKey);
107
- return `import ${importId} from '${importPath}.json';`;
108
- })
109
- )
110
- .join("\n");
111
-
112
- if (isSingle) {
113
- const entry = eagerEntries[0]!;
114
- const baseId = toNamespaceBaseIdentifier(entry.namespace);
115
- const partitionEntries = partitionKeys
116
- .map(
117
- (partitionKey) =>
118
- ` ${toLocaleObjectKey(partitionKey)}: ${toSplitImportIdentifier(entry.namespace, partitionKey)},`
119
- )
120
- .join("\n");
121
-
122
- return (
123
- `${GENERATED_FILE_BANNER}` +
124
- `${imports}\n` +
125
- `import type { ${schemaTypeName}, ${paramTypeName} } from '${typesImport}';\n\n` +
126
- `const ${baseId}${partitionSuffix} = {\n${partitionEntries}\n} as const;\n\n` +
127
- `export function defaultDictionaryFor(${paramName}: ${paramTypeName}): ${schemaTypeName} {\n` +
128
- ` return ${baseId}${partitionSuffix}[${paramName}];\n` +
129
- `}\n`
130
- );
131
- }
132
-
133
- const byPartitionBlocks = eagerEntries
134
- .map((entry) => {
135
- const baseId = toNamespaceBaseIdentifier(entry.namespace);
136
- const partitionEntries = partitionKeys
137
- .map(
138
- (partitionKey) =>
139
- ` ${toLocaleObjectKey(partitionKey)}: ${toSplitImportIdentifier(entry.namespace, partitionKey)},`
140
- )
141
- .join("\n");
142
- return `const ${baseId}${partitionSuffix} = {\n${partitionEntries}\n} as const;`;
143
- })
144
- .join("\n\n");
145
-
146
- const objectEntries = eagerEntries
147
- .map((entry) => {
148
- const baseId = toNamespaceBaseIdentifier(entry.namespace);
149
- return ` ${entry.namespace}: ${baseId}${partitionSuffix}[${paramName}],`;
150
- })
151
- .join("\n");
152
-
153
- const importsBlock = imports.length > 0 ? `${imports}\n` : "";
154
- const byPartitionBlock = byPartitionBlocks.length > 0 ? `${byPartitionBlocks}\n\n` : "";
155
-
156
- return (
157
- `${GENERATED_FILE_BANNER}` +
158
- `${importsBlock}` +
159
- `import type { ${dictionaryTypeName}, ${paramTypeName}${schemaTypeImport} } from '${typesImport}';\n\n` +
160
- `${byPartitionBlock}` +
161
- `export function defaultDictionaryFor(${paramName}: ${paramTypeName}): ${dictionaryTypeName} {\n` +
162
- ` return {\n${objectEntries}\n };\n` +
163
- `}\n`
164
- );
165
- }
166
-
167
- /**
168
- * Builds `dictionary.generated.ts` for the configured delivery mode:
169
- * canonical (single bundled JSON per namespace), split-by-locale, or custom areas.
170
- */
171
- export function formatDictionaryFile(options: DictionaryFileOptions): string | null {
172
- const delivery = options.delivery ?? "canonical";
173
- if (delivery === "split-by-locale") {
174
- return formatPartitionedDictionaryFile(options, {
175
- partitionKeys: options.requestLocales ?? [],
176
- paramName: "locale",
177
- paramTypeName: options.localeTypeName,
178
- });
179
- }
180
- if (delivery === "custom") {
181
- if (!options.deliveryAreaTypeName) {
182
- throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
183
- }
184
- return formatPartitionedDictionaryFile(options, {
185
- partitionKeys: options.deliveryAreaNames ?? [],
186
- paramName: "area",
187
- paramTypeName: options.deliveryAreaTypeName,
188
- });
189
- }
190
-
191
- const {
192
- isSingle,
193
- hasLazy,
194
- entries,
195
- eagerEntries,
196
- projectRoot,
197
- dictionaryOutputPath,
198
- typesOutputPath,
199
- schemaTypeName,
200
- importExtension,
201
- } = options;
202
-
203
- const typesModule = toModuleBasename(typesOutputPath);
204
- const typesImport = toRelativeModuleImport(typesModule, importExtension);
205
-
206
- if (isSingle) {
207
- const entry = entries[0]!;
208
- const importPath = toImportPath(
209
- dictionaryOutputPath,
210
- path.resolve(projectRoot, entry.filePath)
211
- );
212
- const importId = toImportIdentifier(entry.namespace);
213
-
214
- return (
215
- `${GENERATED_FILE_BANNER}` +
216
- `import ${importId} from '${importPath}.json';\n` +
217
- `import type { ${schemaTypeName} } from '${typesImport}';\n\n` +
218
- `export const defaultDictionary: ${schemaTypeName} = ${importId};\n`
219
- );
220
- }
221
-
222
- const dictionaryTypeName = hasLazy ? "InitialSchema" : schemaTypeName;
223
-
224
- const imports = eagerEntries
225
- .map((entry) => {
226
- const importPath = toImportPath(
227
- dictionaryOutputPath,
228
- path.resolve(projectRoot, entry.filePath)
229
- );
230
- return `import ${toImportIdentifier(entry.namespace)} from '${importPath}.json';`;
231
- })
232
- .join("\n");
233
-
234
- const objectEntries = eagerEntries
235
- .map((entry) => ` ${entry.namespace}: ${toImportIdentifier(entry.namespace)},`)
236
- .join("\n");
237
-
238
- return (
239
- `${GENERATED_FILE_BANNER}` +
240
- `${imports}\n` +
241
- `import type { ${dictionaryTypeName} } from '${typesImport}';\n\n` +
242
- `export const defaultDictionary: ${dictionaryTypeName} = {\n${objectEntries}\n};\n`
243
- );
244
- }
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { cloneAndFreeze, deepFreeze } from "./deep-freeze.js";
3
-
4
- describe("deepFreeze", () => {
5
- it("freezes nested objects", () => {
6
- const value = { welcome: { en: "Hello" } };
7
- deepFreeze(value);
8
-
9
- expect(Object.isFrozen(value)).toBe(true);
10
- expect(Object.isFrozen(value.welcome)).toBe(true);
11
- expect(() => {
12
- value.welcome.en = "Hacked";
13
- }).toThrow(TypeError);
14
- });
15
-
16
- it("does not recurse forever on cyclic structures", () => {
17
- const value: { name: string; self?: unknown; child: { parent?: unknown } } = {
18
- name: "root",
19
- child: {},
20
- };
21
- value.self = value;
22
- value.child.parent = value;
23
-
24
- expect(() => deepFreeze(value)).not.toThrow();
25
- expect(Object.isFrozen(value)).toBe(true);
26
- expect(Object.isFrozen(value.child)).toBe(true);
27
- });
28
- });
29
-
30
- describe("cloneAndFreeze", () => {
31
- it("returns a deep-frozen clone", () => {
32
- const source = { welcome: { en: "Hello" } };
33
- const snapshot = cloneAndFreeze(source);
34
-
35
- expect(snapshot).toEqual(source);
36
- expect(snapshot).not.toBe(source);
37
- expect(Object.isFrozen(snapshot)).toBe(true);
38
- expect(Object.isFrozen(snapshot.welcome)).toBe(true);
39
- });
40
- });
@@ -1,22 +0,0 @@
1
- export function deepFreeze<T>(value: T): T {
2
- if (value === null || typeof value !== "object") {
3
- return value;
4
- }
5
-
6
- // Guards against infinite recursion on cyclic structures.
7
- if (Object.isFrozen(value)) {
8
- return value;
9
- }
10
-
11
- Object.freeze(value);
12
-
13
- for (const child of Object.values(value)) {
14
- deepFreeze(child);
15
- }
16
-
17
- return value;
18
- }
19
-
20
- export function cloneAndFreeze<T>(value: T): T {
21
- return deepFreeze(structuredClone(value));
22
- }
@@ -1,186 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- assertPatchKeyMulti,
4
- assertPatchKeySingle,
5
- preloadKeyMulti,
6
- preloadKeySingle,
7
- recordPreloadedKeysMulti,
8
- recordPreloadedKeysSingle,
9
- seedPreloadedKeysMulti,
10
- seedPreloadedKeysSingle,
11
- } from "./patch-key.js";
12
-
13
- describe("preload key helpers", () => {
14
- it("formats single preload keys as key:locale", () => {
15
- expect(preloadKeySingle("welcome", "en")).toBe("welcome:en");
16
- });
17
-
18
- it("formats multi preload keys as namespace:key:locale", () => {
19
- expect(preloadKeyMulti("billing", "invoice_summary", "it")).toBe("billing:invoice_summary:it");
20
- });
21
- });
22
-
23
- describe("seedPreloadedKeysSingle", () => {
24
- it("seeds every key/locale pair from the initial dictionary", () => {
25
- const preloadedKeys = new Set<string>();
26
- seedPreloadedKeysSingle(
27
- {
28
- login_button: { en: "Login", it: "Accedi" },
29
- welcome: { en: "Welcome {name}!" },
30
- },
31
- preloadedKeys
32
- );
33
-
34
- expect(preloadedKeys).toEqual(new Set(["login_button:en", "login_button:it", "welcome:en"]));
35
- });
36
- });
37
-
38
- describe("seedPreloadedKeysMulti", () => {
39
- it("seeds every namespace/key/locale triple from the initial dictionary", () => {
40
- const preloadedKeys = new Set<string>();
41
- seedPreloadedKeysMulti(
42
- {
43
- default: { login_button: { en: "Login" } },
44
- billing: { invoice_summary: { en: "One", it: "Uno" } },
45
- },
46
- preloadedKeys
47
- );
48
-
49
- expect(preloadedKeys).toEqual(
50
- new Set([
51
- "default:login_button:en",
52
- "billing:invoice_summary:en",
53
- "billing:invoice_summary:it",
54
- ])
55
- );
56
- });
57
- });
58
-
59
- describe("recordPreloadedKeysSingle", () => {
60
- it("records locales from a load merge payload", () => {
61
- const preloadedKeys = new Set<string>();
62
- recordPreloadedKeysSingle(
63
- {
64
- welcome: { it: "Benvenuto {name}!" },
65
- },
66
- preloadedKeys
67
- );
68
-
69
- expect(preloadedKeys).toEqual(new Set(["welcome:it"]));
70
- });
71
- });
72
-
73
- describe("recordPreloadedKeysMulti", () => {
74
- it("records locales from a namespace load merge payload", () => {
75
- const preloadedKeys = new Set<string>();
76
- recordPreloadedKeysMulti(
77
- "billing",
78
- {
79
- invoice_summary: { it: "Hai {count} fatture" },
80
- },
81
- preloadedKeys
82
- );
83
-
84
- expect(preloadedKeys).toEqual(new Set(["billing:invoice_summary:it"]));
85
- });
86
- });
87
-
88
- describe("assertPatchKeySingle", () => {
89
- const preloadedKeys = new Set(["welcome:en", "welcome:it", "invoice_count:en"]);
90
-
91
- it("accepts a valid patch for a preloaded key", () => {
92
- expect(() =>
93
- assertPatchKeySingle("welcome", "en", "Hello {name}!", preloadedKeys, {
94
- en: "Welcome {name}!",
95
- it: "Benvenuto {name}!",
96
- })
97
- ).not.toThrow();
98
- });
99
-
100
- it("rejects a key that was not preloaded", () => {
101
- expect(() =>
102
- assertPatchKeySingle("welcome", "fr", "Bonjour {name}!", preloadedKeys, {
103
- en: "Welcome {name}!",
104
- })
105
- ).toThrow("[i18n] Key not preloaded: welcome (fr)");
106
- });
107
-
108
- it("rejects invalid ICU syntax", () => {
109
- expect(() =>
110
- assertPatchKeySingle("welcome", "en", "Hi {name", preloadedKeys, {
111
- en: "Welcome {name}!",
112
- })
113
- ).toThrow("[i18n] ICU syntax error on patch:");
114
- });
115
-
116
- it("rejects incompatible ICU args against other locales for the same key", () => {
117
- expect(() =>
118
- assertPatchKeySingle(
119
- "invoice_count",
120
- "en",
121
- "{count, select, other {{count}}}",
122
- preloadedKeys,
123
- {
124
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}}",
125
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}}",
126
- }
127
- )
128
- ).toThrow("[i18n] ICU args mismatch on patch:");
129
- });
130
- });
131
-
132
- describe("assertPatchKeyMulti", () => {
133
- const preloadedKeys = new Set([
134
- "billing:invoice_summary:en",
135
- "billing:invoice_summary:it",
136
- "default:login_button:en",
137
- ]);
138
-
139
- it("accepts a valid patch for a preloaded namespace key", () => {
140
- expect(() =>
141
- assertPatchKeyMulti(
142
- "billing",
143
- "invoice_summary",
144
- "en",
145
- "You have {count, plural, one {1 bill} other {{count} bills}} for {name}",
146
- preloadedKeys,
147
- {
148
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
149
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
150
- }
151
- )
152
- ).not.toThrow();
153
- });
154
-
155
- it("rejects a namespace key that was not preloaded", () => {
156
- expect(() =>
157
- assertPatchKeyMulti("billing", "invoice_summary", "fr", "Facture {count}", preloadedKeys, {
158
- en: "You have {count} invoices",
159
- })
160
- ).toThrow("[i18n] Key not preloaded: billing.invoice_summary (fr)");
161
- });
162
-
163
- it("rejects invalid ICU syntax", () => {
164
- expect(() =>
165
- assertPatchKeyMulti("default", "login_button", "en", "Sign {in", preloadedKeys, {
166
- en: "Login",
167
- })
168
- ).toThrow("[i18n] ICU syntax error on patch:");
169
- });
170
-
171
- it("rejects incompatible ICU args against other locales for the same key", () => {
172
- expect(() =>
173
- assertPatchKeyMulti(
174
- "billing",
175
- "invoice_summary",
176
- "en",
177
- "{count, select, other {{count}}}",
178
- preloadedKeys,
179
- {
180
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
181
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
182
- }
183
- )
184
- ).toThrow("[i18n] ICU args mismatch on patch:");
185
- });
186
- });
package/src/patch-key.ts DELETED
@@ -1,140 +0,0 @@
1
- import { mergeVariableMetaAcrossLocales, type VariableMetaSpec } from "./icu/extract-variables.js";
2
- import { parseTemplate } from "./icu/parse-template.js";
3
- import type { KeyDictionary, PartialKeyDictionary } from "./types.js";
4
-
5
- export function preloadKeySingle(key: string, locale: string): string {
6
- return `${key}:${locale}`;
7
- }
8
-
9
- export function preloadKeyMulti(namespace: string, key: string, locale: string): string {
10
- return `${namespace}:${key}:${locale}`;
11
- }
12
-
13
- export function seedPreloadedKeysSingle(
14
- dictionary: KeyDictionary,
15
- preloadedKeys: Set<string>
16
- ): void {
17
- for (const [key, localeByKey] of Object.entries(dictionary)) {
18
- if (localeByKey === undefined || typeof localeByKey !== "object") {
19
- continue;
20
- }
21
- for (const locale of Object.keys(localeByKey)) {
22
- preloadedKeys.add(preloadKeySingle(key, locale));
23
- }
24
- }
25
- }
26
-
27
- export function seedPreloadedKeysMulti(
28
- dictionary: Record<string, KeyDictionary>,
29
- preloadedKeys: Set<string>
30
- ): void {
31
- for (const [namespace, keyDictionary] of Object.entries(dictionary)) {
32
- if (keyDictionary === undefined || typeof keyDictionary !== "object") {
33
- continue;
34
- }
35
- for (const [key, localeByKey] of Object.entries(keyDictionary)) {
36
- if (localeByKey === undefined || typeof localeByKey !== "object") {
37
- continue;
38
- }
39
- for (const locale of Object.keys(localeByKey)) {
40
- preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
41
- }
42
- }
43
- }
44
- }
45
-
46
- export function recordPreloadedKeysSingle(
47
- partial: PartialKeyDictionary<KeyDictionary, string>,
48
- preloadedKeys: Set<string>
49
- ): void {
50
- for (const [key, incomingLocales] of Object.entries(partial)) {
51
- if (incomingLocales === undefined || typeof incomingLocales !== "object") {
52
- continue;
53
- }
54
- for (const locale of Object.keys(incomingLocales)) {
55
- preloadedKeys.add(preloadKeySingle(key, locale));
56
- }
57
- }
58
- }
59
-
60
- export function recordPreloadedKeysMulti(
61
- namespace: string,
62
- partial: PartialKeyDictionary<KeyDictionary, string>,
63
- preloadedKeys: Set<string>
64
- ): void {
65
- for (const [key, incomingLocales] of Object.entries(partial)) {
66
- if (incomingLocales === undefined || typeof incomingLocales !== "object") {
67
- continue;
68
- }
69
- for (const locale of Object.keys(incomingLocales)) {
70
- preloadedKeys.add(preloadKeyMulti(namespace, key, locale));
71
- }
72
- }
73
- }
74
-
75
- function collectLocaleMetasForKey(
76
- localeByKey: Record<string, string>,
77
- patchedLocale: string,
78
- patchedMeta: VariableMetaSpec
79
- ): VariableMetaSpec[] {
80
- const localeMetas: VariableMetaSpec[] = [patchedMeta];
81
-
82
- for (const [locale, template] of Object.entries(localeByKey)) {
83
- if (locale === patchedLocale) {
84
- continue;
85
- }
86
- const parsed = parseTemplate(template);
87
- if (parsed.ok) {
88
- localeMetas.push(parsed.meta);
89
- }
90
- }
91
-
92
- return localeMetas;
93
- }
94
-
95
- export function assertPatchKeySingle(
96
- key: string,
97
- locale: string,
98
- template: string,
99
- preloadedKeys: Set<string>,
100
- localeByKey: Record<string, string> | undefined
101
- ): void {
102
- if (!preloadedKeys.has(preloadKeySingle(key, locale))) {
103
- throw new Error(`[i18n] Key not preloaded: ${key} (${locale})`);
104
- }
105
-
106
- const parsed = parseTemplate(template);
107
- if (!parsed.ok) {
108
- throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
109
- }
110
-
111
- const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
112
- const merged = mergeVariableMetaAcrossLocales(localeMetas);
113
- if (!merged.ok) {
114
- throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
115
- }
116
- }
117
-
118
- export function assertPatchKeyMulti(
119
- namespace: string,
120
- key: string,
121
- locale: string,
122
- template: string,
123
- preloadedKeys: Set<string>,
124
- localeByKey: Record<string, string> | undefined
125
- ): void {
126
- if (!preloadedKeys.has(preloadKeyMulti(namespace, key, locale))) {
127
- throw new Error(`[i18n] Key not preloaded: ${namespace}.${key} (${locale})`);
128
- }
129
-
130
- const parsed = parseTemplate(template);
131
- if (!parsed.ok) {
132
- throw new Error(`[i18n] ICU syntax error on patch: ${parsed.message}`);
133
- }
134
-
135
- const localeMetas = collectLocaleMetasForKey(localeByKey ?? {}, locale, parsed.meta);
136
- const merged = mergeVariableMetaAcrossLocales(localeMetas);
137
- if (!merged.ok) {
138
- throw new Error(`[i18n] ICU args mismatch on patch: ${merged.message}`);
139
- }
140
- }