@xndrjs/i18n 0.8.1 → 0.8.2-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 (93) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/audit.js +739 -0
  3. package/dist/cli/audit.js.map +1 -0
  4. package/dist/cli/codegen.js +1471 -0
  5. package/dist/cli/codegen.js.map +1 -0
  6. package/dist/cli/setup.js +148 -0
  7. package/dist/cli/setup.js.map +1 -0
  8. package/dist/codegen/index.js +43 -40
  9. package/dist/codegen/index.js.map +1 -1
  10. package/package.json +6 -9
  11. package/bin/audit.mjs +0 -14
  12. package/bin/codegen.mjs +0 -17
  13. package/bin/setup.mjs +0 -14
  14. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  15. package/src/IcuTranslationProviderMulti.ts +0 -188
  16. package/src/audit/audit-dictionaries.test.ts +0 -134
  17. package/src/audit/audit-dictionaries.ts +0 -178
  18. package/src/audit/run-audit.test.ts +0 -169
  19. package/src/audit/run-audit.ts +0 -94
  20. package/src/builder-load-registry.test.ts +0 -44
  21. package/src/builder-load-registry.ts +0 -78
  22. package/src/builder-loaders.ts +0 -15
  23. package/src/builder-types.test.ts +0 -57
  24. package/src/builder-types.ts +0 -24
  25. package/src/builder.test.ts +0 -303
  26. package/src/builder.ts +0 -15
  27. package/src/codegen/codegen-config-schema.ts +0 -156
  28. package/src/codegen/config.test.ts +0 -231
  29. package/src/codegen/config.ts +0 -42
  30. package/src/codegen/constants.ts +0 -9
  31. package/src/codegen/delivery-artifacts.test.ts +0 -167
  32. package/src/codegen/delivery-artifacts.ts +0 -154
  33. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  34. package/src/codegen/dictionary-spec-contract.ts +0 -138
  35. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  36. package/src/codegen/emit/instance-file.test.ts +0 -71
  37. package/src/codegen/emit/instance-file.ts +0 -257
  38. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  39. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  40. package/src/codegen/emit/types-file.test.ts +0 -96
  41. package/src/codegen/emit/types-file.ts +0 -142
  42. package/src/codegen/fixtures/billing-namespace.json +0 -6
  43. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  44. package/src/codegen/generate-i18n-types.ts +0 -20
  45. package/src/codegen/icu-analysis.ts +0 -110
  46. package/src/codegen/locale-fallback.ts +0 -73
  47. package/src/codegen/locale-policy.test.ts +0 -35
  48. package/src/codegen/locale-policy.ts +0 -30
  49. package/src/codegen/paths.ts +0 -44
  50. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  51. package/src/codegen/read-dictionary.test.ts +0 -594
  52. package/src/codegen/read-dictionary.ts +0 -260
  53. package/src/codegen/regenerate-namespaces.ts +0 -180
  54. package/src/codegen/run-codegen.test.ts +0 -230
  55. package/src/codegen/run-codegen.ts +0 -252
  56. package/src/codegen/types.ts +0 -10
  57. package/src/codegen/write-file-if-changed.test.ts +0 -42
  58. package/src/codegen/write-file-if-changed.ts +0 -20
  59. package/src/codegen-config/build-config.ts +0 -15
  60. package/src/codegen-config/codegen-config.test.ts +0 -36
  61. package/src/codegen-config/index.ts +0 -37
  62. package/src/codegen-config/type-names.ts +0 -20
  63. package/src/codegen-config/write-config.ts +0 -8
  64. package/src/engine.ts +0 -47
  65. package/src/fetch-artifact.ts +0 -16
  66. package/src/format-core.ts +0 -91
  67. package/src/i18n-handle.ts +0 -216
  68. package/src/icu/extract-variables.test.ts +0 -199
  69. package/src/icu/extract-variables.ts +0 -227
  70. package/src/icu/parse-template.ts +0 -31
  71. package/src/index.ts +0 -63
  72. package/src/load-state.ts +0 -15
  73. package/src/project-locales.test.ts +0 -280
  74. package/src/project-locales.ts +0 -211
  75. package/src/resolve-locale.test.ts +0 -91
  76. package/src/resolve-locale.ts +0 -88
  77. package/src/scope-multi.ts +0 -105
  78. package/src/scope-types.ts +0 -35
  79. package/src/scope.test.ts +0 -172
  80. package/src/serialized-state.test.ts +0 -114
  81. package/src/serialized-state.ts +0 -28
  82. package/src/setup/setup-i18n.test.ts +0 -85
  83. package/src/setup/setup-i18n.ts +0 -156
  84. package/src/types.ts +0 -65
  85. package/src/validation/create-args-schema.ts +0 -76
  86. package/src/validation/create-normalized-schema.ts +0 -52
  87. package/src/validation/errors.ts +0 -43
  88. package/src/validation/index.ts +0 -202
  89. package/src/validation/normalize.ts +0 -299
  90. package/src/validation/to-dictionary.ts +0 -67
  91. package/src/validation/types.ts +0 -82
  92. package/src/validation/validate-normalized.ts +0 -133
  93. package/src/validation/validation.test.ts +0 -562
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xndrjs/i18n",
3
- "version": "0.8.1",
3
+ "version": "0.8.2-alpha.0",
4
4
  "description": "Compiler-first, type-safe ICU MessageFormat i18n with lazy delivery loading and external payload validation for content ingest.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -11,9 +11,9 @@
11
11
  },
12
12
  "type": "module",
13
13
  "bin": {
14
- "xndrjs-i18n-codegen": "./bin/codegen.mjs",
15
- "xndrjs-i18n-setup": "./bin/setup.mjs",
16
- "xndrjs-i18n-audit": "./bin/audit.mjs"
14
+ "xndrjs-i18n-codegen": "./dist/cli/codegen.js",
15
+ "xndrjs-i18n-setup": "./dist/cli/setup.js",
16
+ "xndrjs-i18n-audit": "./dist/cli/audit.js"
17
17
  },
18
18
  "types": "./dist/index.d.ts",
19
19
  "exports": {
@@ -34,9 +34,7 @@
34
34
  }
35
35
  },
36
36
  "files": [
37
- "dist",
38
- "bin",
39
- "src"
37
+ "dist"
40
38
  ],
41
39
  "engines": {
42
40
  "node": ">=18"
@@ -47,7 +45,6 @@
47
45
  "yaml": "^2.8.1"
48
46
  },
49
47
  "peerDependencies": {
50
- "tsx": ">=4",
51
48
  "zod": "^4.0.0"
52
49
  },
53
50
  "devDependencies": {
@@ -61,7 +58,7 @@
61
58
  "pack": "pnpm run build && mkdir -p artifacts && npm pack --pack-destination artifacts",
62
59
  "lint": "eslint .",
63
60
  "lint:fix": "eslint . --fix",
64
- "test": "vitest run",
61
+ "test": "pnpm run build && vitest run",
65
62
  "typecheck": "tsc --noEmit"
66
63
  }
67
64
  }
package/bin/audit.mjs DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, join } from "node:path";
5
-
6
- const script = join(dirname(fileURLToPath(import.meta.url)), "../src/audit/run-audit.ts");
7
-
8
- const result = spawnSync("tsx", [script, ...process.argv.slice(2)], {
9
- stdio: "inherit",
10
- cwd: process.cwd(),
11
- env: process.env,
12
- });
13
-
14
- process.exit(result.status ?? 1);
package/bin/codegen.mjs DELETED
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, join } from "node:path";
5
-
6
- const script = join(
7
- dirname(fileURLToPath(import.meta.url)),
8
- "../src/codegen/generate-i18n-types.ts"
9
- );
10
-
11
- const result = spawnSync("tsx", [script, ...process.argv.slice(2)], {
12
- stdio: "inherit",
13
- cwd: process.cwd(),
14
- env: process.env,
15
- });
16
-
17
- process.exit(result.status ?? 1);
package/bin/setup.mjs DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, join } from "node:path";
5
-
6
- const script = join(dirname(fileURLToPath(import.meta.url)), "../src/setup/setup-i18n.ts");
7
-
8
- const result = spawnSync("tsx", [script, ...process.argv.slice(2)], {
9
- stdio: "inherit",
10
- cwd: process.cwd(),
11
- env: process.env,
12
- });
13
-
14
- process.exit(result.status ?? 1);
@@ -1,215 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
3
-
4
- type TestSchema = {
5
- default: {
6
- login_button: { en: string; it: string };
7
- welcome: { en: string };
8
- dashboard_status: { en: string; it: string };
9
- inbox_owner: { en: string; it: string };
10
- ranking_position: { en: string; it: string };
11
- };
12
- billing: {
13
- invoice_summary: { en: string; it: string };
14
- account_balance: { en: string; it: string };
15
- appointment_summary: { en: string; it: string };
16
- empty_label: { en: string };
17
- };
18
- };
19
-
20
- type TestParams = {
21
- default: {
22
- login_button: never;
23
- welcome: { name: string };
24
- dashboard_status: { msgCount: number; chatCount: number };
25
- inbox_owner: { gender: string; name: string };
26
- ranking_position: { position: number };
27
- };
28
- billing: {
29
- invoice_summary: { count: number; name: string };
30
- account_balance: { amount: number };
31
- appointment_summary: { dueDate: Date | number; startTime: Date | number };
32
- empty_label: never;
33
- };
34
- };
35
-
36
- const dictionary: TestSchema = {
37
- default: {
38
- login_button: { en: "Login", it: "Accedi" },
39
- welcome: { en: "Welcome {name}!" },
40
- dashboard_status: {
41
- en: "You have {msgCount, plural, one {1 message} other {{msgCount} messages}} in {chatCount, plural, one {one chat} other {{chatCount} chats}}",
42
- it: "Hai {msgCount, plural, one {1 messaggio} other {{msgCount} messaggi}} in {chatCount, plural, one {una chat} other {{chatCount} chat}}",
43
- },
44
- inbox_owner: {
45
- en: "{gender, select, female {{name} owns her inbox} male {{name} owns his inbox} other {{name} owns their inbox}}",
46
- it: "{gender, select, female {{name} gestisce la sua casella} male {{name} gestisce la sua casella} other {{name} gestisce la propria casella}}",
47
- },
48
- ranking_position: {
49
- en: "You finished {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
50
- it: "Hai concluso al {position, selectordinal, one {#°} other {#°}} posto",
51
- },
52
- },
53
- billing: {
54
- invoice_summary: {
55
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
56
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
57
- },
58
- account_balance: {
59
- en: "Balance: {amount, number, ::currency/EUR}",
60
- it: "Saldo: {amount, number, ::currency/EUR}",
61
- },
62
- appointment_summary: {
63
- en: "Due {dueDate, date, short} at {startTime, time, short}",
64
- it: "Scade il {dueDate, date, short} alle {startTime, time, short}",
65
- },
66
- empty_label: { en: "" },
67
- },
68
- };
69
-
70
- describe("IcuTranslationProviderMulti", () => {
71
- const engine = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
72
-
73
- it("tracks namespaces via dictionary keys", () => {
74
- const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
75
- default: dictionary.default,
76
- });
77
-
78
- expect("default" in partial.getAll()).toBe(true);
79
- expect("billing" in partial.getAll()).toBe(false);
80
- });
81
-
82
- it("supports partial initialization", () => {
83
- const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
84
- default: dictionary.default,
85
- });
86
-
87
- expect(partial.toScope({ namespaces: ["default"] }).t("default", "login_button", "en")).toBe(
88
- "Login"
89
- );
90
- });
91
-
92
- it("adds namespace to dictionary with applyLoadMergeNamespace", () => {
93
- const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({
94
- default: dictionary.default,
95
- });
96
- expect("billing" in partial.getAll()).toBe(false);
97
-
98
- partial.applyLoadMergeNamespace("billing", dictionary.billing);
99
- expect("billing" in partial.getAll()).toBe(true);
100
- });
101
-
102
- it("adds namespace to dictionary on first applyLoadMergeNamespace", () => {
103
- const partial = new IcuTranslationProviderMulti<TestSchema, TestParams>({});
104
- expect("billing" in partial.getAll()).toBe(false);
105
-
106
- partial.applyLoadMergeNamespace("billing", dictionary.billing);
107
- expect("billing" in partial.getAll()).toBe(true);
108
- });
109
-
110
- it("accumulates a locale via applyLoadMergeNamespace when the namespace already exists", () => {
111
- const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
112
- billing: {
113
- invoice_summary: {
114
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
115
- },
116
- },
117
- });
118
- const view = () => local.toScope({ namespaces: ["billing"] });
119
-
120
- expect("billing" in local.getAll()).toBe(true);
121
- expect(view().t("billing", "invoice_summary", "en", { count: 1, name: "Ada" })).toBe(
122
- "You have 1 invoice for Ada"
123
- );
124
-
125
- local.applyLoadMergeNamespace("billing", {
126
- invoice_summary: {
127
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
128
- },
129
- });
130
-
131
- expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
132
- "You have 2 invoices for Bob"
133
- );
134
- expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
135
- "Hai 2 fatture per Bob"
136
- );
137
- });
138
-
139
- it("merges locales per key with applyLoadMergeNamespace without dropping existing locales", () => {
140
- const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
141
- billing: {
142
- invoice_summary: {
143
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
144
- },
145
- },
146
- });
147
- const view = () => local.toScope({ namespaces: ["billing"] });
148
-
149
- local.applyLoadMergeNamespace("billing", {
150
- invoice_summary: {
151
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
152
- },
153
- });
154
-
155
- expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
156
- "You have 2 invoices for Bob"
157
- );
158
- expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
159
- "Hai 2 fatture per Bob"
160
- );
161
- });
162
-
163
- it("merges namespaces with applyLoadMergeAll without dropping existing locales", () => {
164
- const local = new IcuTranslationProviderMulti<TestSchema, TestParams>({
165
- billing: {
166
- invoice_summary: {
167
- en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
168
- },
169
- },
170
- });
171
- const view = () => local.toScope({ namespaces: ["default", "billing"] });
172
-
173
- local.applyLoadMergeAll({
174
- billing: {
175
- invoice_summary: {
176
- it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
177
- },
178
- },
179
- default: dictionary.default,
180
- });
181
-
182
- expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
183
- "You have 2 invoices for Bob"
184
- );
185
- expect(view().t("billing", "invoice_summary", "it", { count: 2, name: "Bob" })).toBe(
186
- "Hai 2 fatture per Bob"
187
- );
188
- expect(view().t("default", "login_button", "en")).toBe("Login");
189
- });
190
-
191
- it("returns the live dictionary from getAll (constructor clones the input)", () => {
192
- expect(engine.getAll()).toEqual(dictionary);
193
- expect(engine.getAll()).not.toBe(dictionary);
194
- expect(engine.getAll()).toBe(engine.getAll());
195
- });
196
-
197
- it("invalidates compiled cache when applyLoadMergeNamespace overwrites a key", () => {
198
- const local = new IcuTranslationProviderMulti<TestSchema, TestParams>(dictionary);
199
- const view = () => local.toScope({ namespaces: ["billing"] });
200
-
201
- expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
202
- "You have 2 invoices for Bob"
203
- );
204
-
205
- local.applyLoadMergeNamespace("billing", {
206
- invoice_summary: {
207
- en: "{count, plural, one {1 bill} other {{count} bills}} for {name}",
208
- },
209
- });
210
-
211
- expect(view().t("billing", "invoice_summary", "en", { count: 2, name: "Bob" })).toBe(
212
- "2 bills for Bob"
213
- );
214
- });
215
- });
@@ -1,188 +0,0 @@
1
- import { BuilderLoadRegistry, type BuilderResourceEntry } from "./builder-load-registry.js";
2
- import type { I18nEngineMulti } from "./engine.js";
3
- import type { I18nSerializedState } from "./serialized-state.js";
4
- import { resolveAndFormat } from "./format-core.js";
5
- import { mergeNamespaceLocalesCore } from "./project-locales.js";
6
- import { validateLocaleFallback } from "./resolve-locale.js";
7
- import type {
8
- IcuTranslationProviderOptions,
9
- LocaleFallbackMap,
10
- LocaleOfMulti,
11
- MultiCompiledCache,
12
- MultiDictionary,
13
- OnMissingTranslation,
14
- PartialKeyDictionary,
15
- PartialMultiDictionary,
16
- } from "./types.js";
17
- import { I18nScopeMultiForLocaleImpl, I18nScopeMultiImpl } from "./scope-multi.js";
18
- import type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
19
- import type { MultiParams, ParamsForNamespaces, SchemaForNamespaces } from "./scope-types.js";
20
-
21
- export class IcuTranslationProviderMulti<
22
- Schema extends MultiDictionary,
23
- Params extends MultiParams<Schema>,
24
- RequestLocales extends string = LocaleOfMulti<Schema>,
25
- Fallback extends LocaleFallbackMap | undefined = undefined,
26
- > implements I18nEngineMulti<Schema, Params, RequestLocales> {
27
- readonly __i18nEngineMode = "multi" as const;
28
-
29
- /** Loaded namespaces only — cold start is `{}`; never claim full Schema until loaded. */
30
- private dictionary: PartialMultiDictionary<Schema, RequestLocales>;
31
- private compiledCache: MultiCompiledCache = {};
32
- private readonly builderLoadRegistry = new BuilderLoadRegistry();
33
- private readonly localeFallback?: Fallback;
34
- private readonly onMissing: OnMissingTranslation;
35
-
36
- constructor(
37
- dictionary: PartialMultiDictionary<Schema, RequestLocales> = {},
38
- options?: IcuTranslationProviderOptions<Fallback>
39
- ) {
40
- this.dictionary = structuredClone(dictionary);
41
- if (options?.localeFallback) {
42
- validateLocaleFallback(options.localeFallback);
43
- this.localeFallback = options.localeFallback;
44
- }
45
- this.onMissing = options?.onMissing ?? "throw";
46
- }
47
-
48
- getWithLocale(
49
- namespace: string,
50
- key: string,
51
- locale: string,
52
- params?: Record<string, unknown>
53
- ): string {
54
- const localeByKey = (
55
- this.dictionary[namespace as keyof Schema] as
56
- | Record<string, Record<string, string>>
57
- | undefined
58
- )?.[key];
59
-
60
- return resolveAndFormat({
61
- localeByKey,
62
- locale,
63
- params,
64
- getCache: (resolvedLocale) => {
65
- if (!this.compiledCache[resolvedLocale]) {
66
- this.compiledCache[resolvedLocale] = {};
67
- }
68
- if (!this.compiledCache[resolvedLocale]![namespace]) {
69
- this.compiledCache[resolvedLocale]![namespace] = {};
70
- }
71
- return this.compiledCache[resolvedLocale]![namespace]!;
72
- },
73
- context: {
74
- namespace,
75
- key,
76
- locale,
77
- localeFallback: this.localeFallback,
78
- onMissing: this.onMissing,
79
- },
80
- });
81
- }
82
-
83
- toScope<
84
- NsList extends readonly (keyof Schema & string)[],
85
- Locale extends RequestLocales,
86
- >(options: {
87
- namespaces: NsList;
88
- locale: Locale;
89
- }): I18nScopeMultiForLocale<
90
- SchemaForNamespaces<Schema, NsList>,
91
- ParamsForNamespaces<Schema, Params, NsList>,
92
- RequestLocales,
93
- Locale
94
- >;
95
- toScope<NsList extends readonly (keyof Schema & string)[]>(options: {
96
- namespaces: NsList;
97
- }): I18nScopeMulti<
98
- SchemaForNamespaces<Schema, NsList>,
99
- ParamsForNamespaces<Schema, Params, NsList>,
100
- RequestLocales
101
- >;
102
- toScope<
103
- NsList extends readonly (keyof Schema & string)[],
104
- Locale extends RequestLocales,
105
- >(options: { namespaces: NsList; locale?: Locale }) {
106
- if (options.locale !== undefined) {
107
- return new I18nScopeMultiForLocaleImpl(this, options.locale) as I18nScopeMultiForLocale<
108
- SchemaForNamespaces<Schema, NsList>,
109
- ParamsForNamespaces<Schema, Params, NsList>,
110
- RequestLocales,
111
- Locale
112
- >;
113
- }
114
- return new I18nScopeMultiImpl(this) as I18nScopeMulti<
115
- SchemaForNamespaces<Schema, NsList>,
116
- ParamsForNamespaces<Schema, Params, NsList>,
117
- RequestLocales
118
- >;
119
- }
120
-
121
- /** Live dictionary reference (loaded namespaces only). Do not mutate. */
122
- getAll(): PartialMultiDictionary<Schema, RequestLocales> {
123
- return this.dictionary;
124
- }
125
-
126
- hasBuilderResourceLoaded(namespace: string, partition: string): boolean {
127
- return this.builderLoadRegistry.has(namespace, partition);
128
- }
129
-
130
- markBuilderResourcePending(namespace: string, partition: string): void {
131
- this.builderLoadRegistry.markPending(namespace, partition);
132
- }
133
-
134
- markBuilderResourceLoaded(namespace: string, partition: string): void {
135
- this.builderLoadRegistry.mark(namespace, partition);
136
- }
137
-
138
- markBuilderResourceError(namespace: string, partition: string, error: unknown): void {
139
- this.builderLoadRegistry.markError(namespace, partition, error);
140
- }
141
-
142
- getBuilderLoadState() {
143
- return this.builderLoadRegistry.getLoadState();
144
- }
145
-
146
- seedBuilderResources(resources: readonly BuilderResourceEntry[]): void {
147
- this.builderLoadRegistry.seed(resources);
148
- }
149
-
150
- serialize(): I18nSerializedState<PartialMultiDictionary<Schema, RequestLocales>> {
151
- return {
152
- dictionary: this.dictionary,
153
- resources: this.builderLoadRegistry.entries(),
154
- };
155
- }
156
-
157
- applyLoadMergeNamespace<NS extends keyof Schema>(
158
- namespace: NS,
159
- values: PartialKeyDictionary<Schema[NS], RequestLocales>
160
- ): void {
161
- const existing = (this.dictionary[namespace] ?? {}) as Schema[NS];
162
- const merged = mergeNamespaceLocalesCore(existing, values);
163
-
164
- this.dictionary = {
165
- ...this.dictionary,
166
- [namespace]: merged,
167
- };
168
-
169
- for (const [key, incomingLocales] of Object.entries(values)) {
170
- if (incomingLocales === undefined || typeof incomingLocales !== "object") {
171
- continue;
172
- }
173
-
174
- for (const locale of Object.keys(incomingLocales)) {
175
- delete this.compiledCache[locale]?.[namespace as string]?.[key];
176
- }
177
- }
178
- }
179
-
180
- applyLoadMergeAll(values: PartialMultiDictionary<Schema, RequestLocales>): void {
181
- for (const namespace of Object.keys(values) as (keyof Schema & string)[]) {
182
- const incoming = values[namespace];
183
- if (incoming !== undefined) {
184
- this.applyLoadMergeNamespace(namespace, incoming);
185
- }
186
- }
187
- }
188
- }
@@ -1,134 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { auditDictionaries, reportHasGaps } from "./audit-dictionaries.js";
3
-
4
- describe("auditDictionaries", () => {
5
- it("reports missingDirect for fallback-only locales while missingEffective stays empty when en covers", () => {
6
- const report = auditDictionaries({
7
- namespaces: {
8
- default: {
9
- login_button: { en: "Login", it: "Accedi" },
10
- },
11
- },
12
- config: {
13
- localeFallback: {
14
- en: null,
15
- "de-DE": "en",
16
- "de-CH": "de-DE",
17
- it: "en",
18
- },
19
- },
20
- });
21
-
22
- expect(report.requiredLocales).toEqual(["de-CH", "de-DE", "en", "it"]);
23
- expect(report.missingDirectByLocale.default?.["de-CH"]).toEqual(["login_button"]);
24
- expect(report.missingEffectiveByLocale.default?.["de-CH"]).toEqual([]);
25
- expect(report.missingDirectByLocale.default?.en).toEqual([]);
26
- });
27
-
28
- it("treats empty strings as missing by default", () => {
29
- const report = auditDictionaries({
30
- namespaces: {
31
- default: {
32
- empty_label: { en: "" },
33
- },
34
- },
35
- config: {},
36
- });
37
-
38
- expect(report.missingDirectByLocale.default?.en).toEqual(["empty_label"]);
39
- expect(report.missingEffectiveByLocale.default?.en).toEqual(["empty_label"]);
40
- });
41
-
42
- it("allows empty strings when treatEmptyAsMissing is false", () => {
43
- const report = auditDictionaries({
44
- namespaces: {
45
- default: {
46
- empty_label: { en: "" },
47
- },
48
- },
49
- config: {},
50
- treatEmptyAsMissing: false,
51
- });
52
-
53
- expect(report.missingDirectByLocale.default?.en).toEqual([]);
54
- expect(report.missingEffectiveByLocale.default?.en).toEqual([]);
55
- });
56
-
57
- it("uses enriched locale fallback for effective gaps", () => {
58
- const report = auditDictionaries({
59
- namespaces: {
60
- default: {
61
- welcome: { en: "Welcome {name}!" },
62
- },
63
- },
64
- config: {
65
- localeFallback: {
66
- en: null,
67
- it: "en",
68
- },
69
- },
70
- });
71
-
72
- expect(report.localeFallback).toEqual({
73
- en: null,
74
- it: "en",
75
- });
76
- expect(report.missingEffectiveByLocale.default?.it).toEqual([]);
77
- });
78
-
79
- it("matches direct and effective when localeFallback is absent", () => {
80
- const report = auditDictionaries({
81
- namespaces: {
82
- default: {
83
- login_button: { en: "Login" },
84
- },
85
- },
86
- config: {},
87
- });
88
-
89
- expect(report.localeFallback).toBeUndefined();
90
- expect(report.requiredLocales).toEqual(["en"]);
91
- expect(report.missingDirectByLocale.default?.en).toEqual([]);
92
- expect(report.missingEffectiveByLocale.default?.en).toEqual([]);
93
- });
94
-
95
- it("reports effective gaps when fallback chain cannot resolve", () => {
96
- const report = auditDictionaries({
97
- namespaces: {
98
- default: {
99
- login_button: { it: "Accedi" },
100
- },
101
- },
102
- config: {
103
- localeFallback: {
104
- en: null,
105
- it: "en",
106
- },
107
- },
108
- });
109
-
110
- expect(report.missingEffectiveByLocale.default?.en).toEqual(["login_button"]);
111
- });
112
- });
113
-
114
- describe("reportHasGaps", () => {
115
- const report = auditDictionaries({
116
- namespaces: {
117
- default: {
118
- login_button: { en: "Login", it: "Accedi" },
119
- },
120
- },
121
- config: {
122
- localeFallback: {
123
- en: null,
124
- "de-CH": "en",
125
- },
126
- },
127
- });
128
-
129
- it("detects direct gaps", () => {
130
- expect(reportHasGaps(report, "direct")).toBe(true);
131
- expect(reportHasGaps(report, "effective")).toBe(false);
132
- expect(reportHasGaps(report, "any")).toBe(true);
133
- });
134
- });