@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.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -188
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.test.ts +0 -44
- package/src/builder-load-registry.ts +0 -78
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -57
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -303
- package/src/builder.ts +0 -15
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -216
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -63
- package/src/load-state.ts +0 -15
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- 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.
|
|
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": "./
|
|
15
|
-
"xndrjs-i18n-setup": "./
|
|
16
|
-
"xndrjs-i18n-audit": "./
|
|
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
|
-
});
|