@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/src/builder.test.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
2
|
-
import { createI18nHandle } from "./builder.js";
|
|
3
|
-
import { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
|
|
4
|
-
|
|
5
|
-
type MultiSchema = {
|
|
6
|
-
default: {
|
|
7
|
-
login_button: { en: string; it: string };
|
|
8
|
-
welcome: { en: string };
|
|
9
|
-
};
|
|
10
|
-
billing: {
|
|
11
|
-
invoice_summary: { en: string; it: string };
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type TestMultiParams = {
|
|
16
|
-
default: {
|
|
17
|
-
login_button: never;
|
|
18
|
-
welcome: { name: string };
|
|
19
|
-
};
|
|
20
|
-
billing: {
|
|
21
|
-
invoice_summary: { count: number; name: string };
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const billingIt = {
|
|
26
|
-
invoice_summary: {
|
|
27
|
-
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const billingEn = {
|
|
32
|
-
invoice_summary: {
|
|
33
|
-
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const defaultIt = {
|
|
38
|
-
login_button: { it: "Accedi" },
|
|
39
|
-
welcome: { it: "Benvenuto {name}!" },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
describe("I18nHandle", () => {
|
|
43
|
-
it("load() invokes mock loaders, merges into the engine, and returns a bound view", async () => {
|
|
44
|
-
const billingLoader = vi.fn(async (locale: string) => {
|
|
45
|
-
return locale === "it" ? billingIt : billingEn;
|
|
46
|
-
});
|
|
47
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
48
|
-
const handle = createI18nHandle(engine, {
|
|
49
|
-
namespaceLoaders: {
|
|
50
|
-
billing: billingLoader,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const view = await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
55
|
-
|
|
56
|
-
expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
|
|
57
|
-
expect(view.locale).toBe("it");
|
|
58
|
-
expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
|
|
59
|
-
"Hai 2 fatture per Ada"
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("loads multiple namespaces in one call", async () => {
|
|
64
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
65
|
-
locale === "it" ? billingIt : billingEn
|
|
66
|
-
);
|
|
67
|
-
const defaultLoader = vi.fn(async () => defaultIt);
|
|
68
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
69
|
-
|
|
70
|
-
const view = await createI18nHandle(engine, {
|
|
71
|
-
namespaceLoaders: {
|
|
72
|
-
billing: billingLoader,
|
|
73
|
-
default: defaultLoader,
|
|
74
|
-
},
|
|
75
|
-
}).load({ namespaces: ["billing", "default"], locale: "it" });
|
|
76
|
-
|
|
77
|
-
expect(billingLoader).toHaveBeenCalledWith("it", { locale: "it" });
|
|
78
|
-
expect(defaultLoader).toHaveBeenCalledWith("it", { locale: "it" });
|
|
79
|
-
expect(view.t("default", "login_button")).toBe("Accedi");
|
|
80
|
-
expect(view.t("billing", "invoice_summary", { count: 1, name: "Bob" })).toBe(
|
|
81
|
-
"Hai 1 fattura per Bob"
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("accumulates engine data across repeated load() calls on the same handle", async () => {
|
|
86
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
87
|
-
locale === "it" ? billingIt : billingEn
|
|
88
|
-
);
|
|
89
|
-
const defaultLoader = vi.fn(async () => defaultIt);
|
|
90
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
91
|
-
const handle = createI18nHandle(engine, {
|
|
92
|
-
namespaceLoaders: {
|
|
93
|
-
billing: billingLoader,
|
|
94
|
-
default: defaultLoader,
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
99
|
-
const defaultView = await handle.load({ namespaces: ["default"], locale: "it" });
|
|
100
|
-
|
|
101
|
-
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
102
|
-
expect(defaultLoader).toHaveBeenCalledTimes(1);
|
|
103
|
-
expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
104
|
-
"Hai 1 fattura per Ada"
|
|
105
|
-
);
|
|
106
|
-
expect(defaultView.t("default", "login_button")).toBe("Accedi");
|
|
107
|
-
expect(engine.getAll().billing?.invoice_summary?.it).toBe(billingIt.invoice_summary.it);
|
|
108
|
-
expect(engine.getAll().default?.login_button?.it).toBe("Accedi");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns a view scoped to namespaces declared at load time", async () => {
|
|
112
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
113
|
-
locale === "it" ? billingIt : billingEn
|
|
114
|
-
);
|
|
115
|
-
const defaultLoader = vi.fn(async () => defaultIt);
|
|
116
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
117
|
-
const handle = createI18nHandle(engine, {
|
|
118
|
-
namespaceLoaders: {
|
|
119
|
-
billing: billingLoader,
|
|
120
|
-
default: defaultLoader,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const billingView = await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
125
|
-
await handle.load({ namespaces: ["default"], locale: "it" });
|
|
126
|
-
|
|
127
|
-
expect(billingView.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
128
|
-
"Hai 1 fattura per Ada"
|
|
129
|
-
);
|
|
130
|
-
// billingView is typed for the billing namespace only; default is excluded at compile time.
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("maps locale to delivery partition via partitionForLocale", async () => {
|
|
134
|
-
const billingLoader = vi.fn(async (area: string) => {
|
|
135
|
-
return area === "eu" ? billingIt : billingEn;
|
|
136
|
-
});
|
|
137
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
138
|
-
|
|
139
|
-
const view = await createI18nHandle(engine, {
|
|
140
|
-
namespaceLoaders: {
|
|
141
|
-
billing: billingLoader,
|
|
142
|
-
},
|
|
143
|
-
partitionForLocale: (locale) => (locale === "it" ? "eu" : "us"),
|
|
144
|
-
}).load({ namespaces: ["billing"], locale: "it" });
|
|
145
|
-
|
|
146
|
-
expect(billingLoader).toHaveBeenCalledWith("eu", { locale: "it" });
|
|
147
|
-
expect(view.locale).toBe("it");
|
|
148
|
-
expect(view.t("billing", "invoice_summary", { count: 2, name: "Ada" })).toBe(
|
|
149
|
-
"Hai 2 fatture per Ada"
|
|
150
|
-
);
|
|
151
|
-
expectTypeOf(view.t).toBeCallableWith("billing", "invoice_summary", {
|
|
152
|
-
count: 2,
|
|
153
|
-
name: "Ada",
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("loads partitioned namespace loaders with locale", async () => {
|
|
158
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
159
|
-
locale === "it"
|
|
160
|
-
? {
|
|
161
|
-
invoice_summary: {
|
|
162
|
-
it: "Hai {count, plural, one {1 fattura} other {{count} fatture}} per {name}",
|
|
163
|
-
},
|
|
164
|
-
}
|
|
165
|
-
: {
|
|
166
|
-
invoice_summary: {
|
|
167
|
-
en: "You have {count, plural, one {1 invoice} other {{count} invoices}} for {name}",
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
172
|
-
|
|
173
|
-
const view = await createI18nHandle(engine, {
|
|
174
|
-
namespaceLoaders: {
|
|
175
|
-
billing: billingLoader,
|
|
176
|
-
},
|
|
177
|
-
}).load({ namespaces: ["billing"], locale: "en" });
|
|
178
|
-
|
|
179
|
-
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
180
|
-
expect(billingLoader.mock.calls[0]).toEqual(["en", { locale: "en" }]);
|
|
181
|
-
expect(view.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
182
|
-
"You have 1 invoice for Ada"
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("rejects an empty namespaces array", async () => {
|
|
187
|
-
const billingLoader = vi.fn(async () => billingEn);
|
|
188
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
189
|
-
const handle = createI18nHandle(engine, {
|
|
190
|
-
namespaceLoaders: {
|
|
191
|
-
billing: billingLoader,
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
await expect(
|
|
196
|
-
handle.load({ namespaces: [] as unknown as ["billing"], locale: "en" })
|
|
197
|
-
).rejects.toThrow("load() requires a non-empty namespaces array");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("skips a repeated load for the same namespace partition", async () => {
|
|
201
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
202
|
-
locale === "it" ? billingIt : billingEn
|
|
203
|
-
);
|
|
204
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
205
|
-
const handle = createI18nHandle(engine, {
|
|
206
|
-
namespaceLoaders: { billing: billingLoader },
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
210
|
-
await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
211
|
-
|
|
212
|
-
expect(billingLoader).toHaveBeenCalledTimes(1);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("returns a destructuring-safe locale-bound scope from load()", async () => {
|
|
216
|
-
const billingLoader = vi.fn(async () => billingEn);
|
|
217
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
218
|
-
const { t } = await createI18nHandle(engine, {
|
|
219
|
-
namespaceLoaders: { billing: billingLoader },
|
|
220
|
-
}).load({ namespaces: ["billing"], locale: "en" });
|
|
221
|
-
|
|
222
|
-
expect(t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
223
|
-
"You have 1 invoice for Ada"
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("still loads a different partition for the same namespace", async () => {
|
|
228
|
-
const billingLoader = vi.fn(async (locale: string) =>
|
|
229
|
-
locale === "it" ? billingIt : billingEn
|
|
230
|
-
);
|
|
231
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
232
|
-
const handle = createI18nHandle(engine, {
|
|
233
|
-
namespaceLoaders: { billing: billingLoader },
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await handle.load({ namespaces: ["billing"], locale: "it" });
|
|
237
|
-
await handle.load({ namespaces: ["billing"], locale: "en" });
|
|
238
|
-
|
|
239
|
-
expect(billingLoader).toHaveBeenCalledTimes(2);
|
|
240
|
-
expect(billingLoader).toHaveBeenNthCalledWith(1, "it", { locale: "it" });
|
|
241
|
-
expect(billingLoader).toHaveBeenNthCalledWith(2, "en", { locale: "en" });
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("peek() returns null until resources are loaded, then a sync scope", async () => {
|
|
245
|
-
const billingLoader = vi.fn(async () => billingEn);
|
|
246
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
247
|
-
const handle = createI18nHandle(engine, {
|
|
248
|
-
namespaceLoaders: { billing: billingLoader },
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
expect(handle.peek({ namespaces: ["billing"], locale: "en" })).toBeNull();
|
|
252
|
-
|
|
253
|
-
await handle.load({ namespaces: ["billing"], locale: "en" });
|
|
254
|
-
const sync = handle.peek({ namespaces: ["billing"], locale: "en" });
|
|
255
|
-
expect(sync).not.toBeNull();
|
|
256
|
-
expect(sync!.t("billing", "invoice_summary", { count: 1, name: "Ada" })).toBe(
|
|
257
|
-
"You have 1 invoice for Ada"
|
|
258
|
-
);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("getLoadState() tracks pending, loaded, and error resources", async () => {
|
|
262
|
-
let resolveBilling!: () => void;
|
|
263
|
-
const billingPending = new Promise<typeof billingEn>((resolve) => {
|
|
264
|
-
resolveBilling = () => resolve(billingEn);
|
|
265
|
-
});
|
|
266
|
-
const billingLoader = vi.fn(() => billingPending);
|
|
267
|
-
const failingLoader = vi.fn(async () => {
|
|
268
|
-
throw new Error("load failed");
|
|
269
|
-
});
|
|
270
|
-
const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
|
|
271
|
-
const handle = createI18nHandle(engine, {
|
|
272
|
-
namespaceLoaders: {
|
|
273
|
-
billing: billingLoader,
|
|
274
|
-
default: failingLoader,
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
expect(handle.getLoadState()).toEqual({ resources: [] });
|
|
279
|
-
|
|
280
|
-
const pending = handle.load({ namespaces: ["billing"], locale: "en" });
|
|
281
|
-
expect(handle.getLoadState()).toEqual({
|
|
282
|
-
resources: [{ namespace: "billing", partition: "en", status: "pending" }],
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
resolveBilling();
|
|
286
|
-
await pending;
|
|
287
|
-
|
|
288
|
-
expect(handle.getLoadState()).toEqual({
|
|
289
|
-
resources: [{ namespace: "billing", partition: "en", status: "loaded" }],
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await expect(handle.load({ namespaces: ["default"], locale: "it" })).rejects.toThrow(
|
|
293
|
-
"load failed"
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
expect(handle.getLoadState()).toEqual({
|
|
297
|
-
resources: [
|
|
298
|
-
{ namespace: "billing", partition: "en", status: "loaded" },
|
|
299
|
-
{ namespace: "default", partition: "it", status: "error", error: expect.any(Error) },
|
|
300
|
-
],
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
});
|
package/src/builder.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export type { NamespaceLoader, PartitionedLoader } from "./builder-loaders.js";
|
|
2
|
-
export { invokeNamespaceLoader } from "./builder-loaders.js";
|
|
3
|
-
export type {
|
|
4
|
-
I18nHandle,
|
|
5
|
-
I18nHandleOptions,
|
|
6
|
-
LoadNamespacesInput,
|
|
7
|
-
PartitionForLocale,
|
|
8
|
-
ScopeForLocale,
|
|
9
|
-
} from "./i18n-handle.js";
|
|
10
|
-
export { createI18nHandle } from "./i18n-handle.js";
|
|
11
|
-
export type {
|
|
12
|
-
I18nLoadState,
|
|
13
|
-
I18nResourceLoadRecord,
|
|
14
|
-
I18nResourceLoadStatus,
|
|
15
|
-
} from "./load-state.js";
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { IDENTIFIER_NAME_PATTERN, IDENTIFIER_NAME_REQUIREMENT } from "./constants.js";
|
|
4
|
-
import { getDeliveryArtifactsStructureIssues } from "./delivery-artifacts.js";
|
|
5
|
-
import { typeNamesForProject } from "../codegen-config/type-names.js";
|
|
6
|
-
|
|
7
|
-
const localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));
|
|
8
|
-
const deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));
|
|
9
|
-
|
|
10
|
-
export const DELIVERY_MODES = ["split-by-locale", "custom"] as const;
|
|
11
|
-
export type DeliveryMode = (typeof DELIVERY_MODES)[number];
|
|
12
|
-
|
|
13
|
-
export const LOADER_STRATEGIES = ["import", "fetch"] as const;
|
|
14
|
-
export type LoaderStrategy = (typeof LOADER_STRATEGIES)[number];
|
|
15
|
-
|
|
16
|
-
const PROJECT_NAME_PATTERN = /^[A-Z][a-zA-Z0-9]*$/;
|
|
17
|
-
|
|
18
|
-
const codegenConfigShape = {
|
|
19
|
-
/** PascalCase project id — generates `{project}Params`, `{project}Schema`, `{project}Locale`. */
|
|
20
|
-
projectName: z
|
|
21
|
-
.string()
|
|
22
|
-
.min(1)
|
|
23
|
-
.regex(PROJECT_NAME_PATTERN, 'projectName must be PascalCase (e.g. "MyApp")'),
|
|
24
|
-
namespaces: z.record(z.string(), z.string().min(1)),
|
|
25
|
-
/**
|
|
26
|
-
* Directory for generated TypeScript modules:
|
|
27
|
-
* `i18n-types.generated.ts`, `instance.generated.ts`,
|
|
28
|
-
* `namespace-loaders.generated.ts`, `dictionary-schema.generated.ts`.
|
|
29
|
-
*/
|
|
30
|
-
codegenPath: z.string().min(1),
|
|
31
|
-
localeFallback: localeFallbackSchema.optional(),
|
|
32
|
-
delivery: z.enum(DELIVERY_MODES).optional().default("split-by-locale"),
|
|
33
|
-
deliveryArtifacts: deliveryArtifactsSchema.optional(),
|
|
34
|
-
/** Directory for delivery JSON (`translations/`). Defaults to {@link codegenPath}. */
|
|
35
|
-
artifactsPath: z.string().min(1).optional(),
|
|
36
|
-
/**
|
|
37
|
-
* How generated `namespaceLoaders` resolve artifacts.
|
|
38
|
-
* - `import` (default): dynamic `import()` — JSON is bundled; content updates need a rebuild.
|
|
39
|
-
* - `fetch`: runtime `fetchImpl({ locale, namespace, area? })` via required `createI18n({ fetchImpl })`.
|
|
40
|
-
* Codegen does not know URLs — mapping id → transport is an application concern.
|
|
41
|
-
*/
|
|
42
|
-
loaderStrategy: z.enum(LOADER_STRATEGIES).optional().default("import"),
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const codegenConfigKeys = Object.keys(
|
|
46
|
-
codegenConfigShape
|
|
47
|
-
) as (keyof typeof codegenConfigShape)[];
|
|
48
|
-
|
|
49
|
-
export const codegenConfigSchema = z
|
|
50
|
-
.object(codegenConfigShape)
|
|
51
|
-
.strict()
|
|
52
|
-
.superRefine((config, ctx) => {
|
|
53
|
-
for (const namespace of Object.keys(config.namespaces)) {
|
|
54
|
-
if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
|
|
55
|
-
ctx.addIssue({
|
|
56
|
-
code: "custom",
|
|
57
|
-
path: ["namespaces", namespace],
|
|
58
|
-
message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (config.delivery === "custom") {
|
|
64
|
-
if (!config.deliveryArtifacts) {
|
|
65
|
-
ctx.addIssue({
|
|
66
|
-
code: "custom",
|
|
67
|
-
path: ["deliveryArtifacts"],
|
|
68
|
-
message: 'deliveryArtifacts is required when delivery is "custom".',
|
|
69
|
-
});
|
|
70
|
-
} else {
|
|
71
|
-
for (const issue of getDeliveryArtifactsStructureIssues(config.deliveryArtifacts)) {
|
|
72
|
-
ctx.addIssue({
|
|
73
|
-
code: "custom",
|
|
74
|
-
path: issue.path,
|
|
75
|
-
message: issue.message,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} else if (config.deliveryArtifacts !== undefined) {
|
|
80
|
-
ctx.addIssue({
|
|
81
|
-
code: "custom",
|
|
82
|
-
path: ["deliveryArtifacts"],
|
|
83
|
-
message: 'deliveryArtifacts is only allowed when delivery is "custom".',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
export type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
|
|
89
|
-
export type CodegenConfig = z.infer<typeof codegenConfigSchema>;
|
|
90
|
-
|
|
91
|
-
export const GENERATED_BASENAMES = {
|
|
92
|
-
types: "i18n-types.generated.ts",
|
|
93
|
-
instance: "instance.generated.ts",
|
|
94
|
-
namespaceLoaders: "namespace-loaders.generated.ts",
|
|
95
|
-
dictionarySchema: "dictionary-schema.generated.ts",
|
|
96
|
-
} as const;
|
|
97
|
-
|
|
98
|
-
export const DEFAULT_FACTORY_NAME = "createI18n";
|
|
99
|
-
export const DEFAULT_LOCALE_FALLBACK_CONST_NAME = "LOCALE_FALLBACK";
|
|
100
|
-
|
|
101
|
-
/** Derived paths and symbol names from `projectName` + `codegenPath`. */
|
|
102
|
-
export interface ResolvedCodegenPaths {
|
|
103
|
-
codegenPath: string;
|
|
104
|
-
typesOutput: string;
|
|
105
|
-
instanceOutput: string;
|
|
106
|
-
namespaceLoadersOutput: string;
|
|
107
|
-
dictionarySchemaOutput: string;
|
|
108
|
-
artifactsPath: string;
|
|
109
|
-
paramsTypeName: string;
|
|
110
|
-
schemaTypeName: string;
|
|
111
|
-
localeTypeName: string;
|
|
112
|
-
localeFallbackConstName: string;
|
|
113
|
-
factoryName: string;
|
|
114
|
-
deliveryAreaTypeName: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function resolveCodegenPaths(
|
|
118
|
-
config: Pick<CodegenConfig, "projectName" | "codegenPath" | "artifactsPath">
|
|
119
|
-
): ResolvedCodegenPaths {
|
|
120
|
-
const typeNames = typeNamesForProject(config.projectName);
|
|
121
|
-
const codegenPath = config.codegenPath;
|
|
122
|
-
return {
|
|
123
|
-
codegenPath,
|
|
124
|
-
typesOutput: path.join(codegenPath, GENERATED_BASENAMES.types),
|
|
125
|
-
instanceOutput: path.join(codegenPath, GENERATED_BASENAMES.instance),
|
|
126
|
-
namespaceLoadersOutput: path.join(codegenPath, GENERATED_BASENAMES.namespaceLoaders),
|
|
127
|
-
dictionarySchemaOutput: path.join(codegenPath, GENERATED_BASENAMES.dictionarySchema),
|
|
128
|
-
artifactsPath: config.artifactsPath ?? codegenPath,
|
|
129
|
-
paramsTypeName: typeNames.paramsTypeName,
|
|
130
|
-
schemaTypeName: typeNames.schemaTypeName,
|
|
131
|
-
localeTypeName: typeNames.localeTypeName,
|
|
132
|
-
localeFallbackConstName: DEFAULT_LOCALE_FALLBACK_CONST_NAME,
|
|
133
|
-
factoryName: DEFAULT_FACTORY_NAME,
|
|
134
|
-
deliveryAreaTypeName: `${config.projectName}DeliveryArea`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function resolveArtifactsPath(
|
|
139
|
-
config: Pick<CodegenConfig, "codegenPath" | "artifactsPath">
|
|
140
|
-
): string {
|
|
141
|
-
return config.artifactsPath ?? config.codegenPath;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function formatCodegenConfigIssues(error: z.ZodError): string {
|
|
145
|
-
const issueLines = error.issues.map((issue) => {
|
|
146
|
-
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
147
|
-
return ` - ${path}: ${issue.message}`;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return [
|
|
151
|
-
"[Codegen Error] Invalid i18n.codegen.json:",
|
|
152
|
-
...issueLines,
|
|
153
|
-
"",
|
|
154
|
-
`Allowed keys: ${codegenConfigKeys.join(", ")}`,
|
|
155
|
-
].join("\n");
|
|
156
|
-
}
|