@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.
- package/README.md +67 -872
- package/dist/codegen/index.d.ts +117 -51
- package/dist/codegen/index.js +1585 -93
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +100 -234
- package/dist/index.js +74 -638
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +2 -2
- package/src/IcuTranslationProviderMulti.test.ts +18 -40
- package/src/IcuTranslationProviderMulti.ts +27 -37
- package/src/audit/audit-dictionaries.ts +1 -1
- package/src/audit/run-audit.test.ts +6 -7
- package/src/builder-load-registry.ts +26 -5
- package/src/builder-loaders.ts +8 -11
- package/src/builder-types.test.ts +11 -42
- package/src/builder.test.ts +82 -243
- package/src/builder.ts +8 -110
- package/src/codegen/codegen-config-schema.ts +82 -77
- package/src/codegen/config.test.ts +55 -143
- package/src/codegen/config.ts +10 -67
- package/src/codegen/dictionary-spec-contract.test.ts +28 -0
- package/src/codegen/dictionary-spec-contract.ts +138 -0
- package/src/codegen/emit/dictionary-schema-file.ts +7 -43
- package/src/codegen/emit/instance-file.test.ts +35 -52
- package/src/codegen/emit/instance-file.ts +170 -268
- package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
- package/src/codegen/emit/namespace-loaders-file.ts +147 -90
- package/src/codegen/emit/types-file.test.ts +20 -41
- package/src/codegen/emit/types-file.ts +48 -74
- package/src/codegen/generate-i18n-types.test.ts +163 -492
- package/src/codegen/generate-i18n-types.ts +7 -269
- package/src/codegen/paths.ts +0 -14
- package/src/codegen/project-locales-set-namespace.test.ts +44 -86
- package/src/codegen/read-dictionary.test.ts +27 -9
- package/src/codegen/read-dictionary.ts +18 -40
- package/src/codegen/regenerate-namespaces.ts +180 -0
- package/src/codegen/run-codegen.test.ts +230 -0
- package/src/codegen/run-codegen.ts +252 -0
- package/src/codegen/types.ts +0 -6
- package/src/codegen-config/build-config.ts +4 -23
- package/src/codegen-config/codegen-config.test.ts +10 -6
- package/src/codegen-config/index.ts +18 -3
- package/src/engine.ts +3 -27
- package/src/fetch-artifact.ts +16 -0
- package/src/i18n-handle.ts +202 -0
- package/src/index.ts +15 -34
- package/src/project-locales.ts +2 -2
- package/src/scope-multi.ts +1 -20
- package/src/scope-types.ts +14 -6
- package/src/scope.test.ts +3 -312
- package/src/serialized-state.test.ts +114 -0
- package/src/serialized-state.ts +28 -0
- package/src/setup/setup-i18n.test.ts +22 -33
- package/src/setup/setup-i18n.ts +22 -27
- package/src/types.ts +3 -3
- package/src/validation/index.ts +4 -0
- package/src/IcuTranslationProviderSingle.test.ts +0 -177
- package/src/IcuTranslationProviderSingle.ts +0 -114
- package/src/builder-multi.ts +0 -481
- package/src/codegen/emit/dictionary-file.test.ts +0 -215
- package/src/codegen/emit/dictionary-file.ts +0 -244
- package/src/deep-freeze.test.ts +0 -40
- package/src/deep-freeze.ts +0 -22
- package/src/patch-key.test.ts +0 -186
- package/src/patch-key.ts +0 -140
- package/src/scope-single.ts +0 -85
- package/src/single-builder.ts +0 -153
package/dist/codegen/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import path8 from 'path';
|
|
3
|
+
import fs3 from 'fs';
|
|
4
|
+
import { parse } from '@formatjs/icu-messageformat-parser';
|
|
5
|
+
import { parse as parse$1 } from 'yaml';
|
|
4
6
|
|
|
5
7
|
// src/codegen/codegen-config-schema.ts
|
|
6
8
|
|
|
@@ -43,64 +45,135 @@ function getDeliveryArtifactsStructureIssues(deliveryArtifacts) {
|
|
|
43
45
|
}
|
|
44
46
|
return issues;
|
|
45
47
|
}
|
|
48
|
+
function getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales) {
|
|
49
|
+
const issues = [];
|
|
50
|
+
const artifactLocales = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const locales of Object.values(deliveryArtifacts)) {
|
|
52
|
+
for (const locale of locales) {
|
|
53
|
+
artifactLocales.add(locale);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const missing = [...requestLocales].filter((locale) => !artifactLocales.has(locale)).sort();
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
issues.push({
|
|
59
|
+
path: ["deliveryArtifacts"],
|
|
60
|
+
message: `deliveryArtifacts is missing locales required by dictionaries and localeFallback: ${missing.join(", ")}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const excess = [...artifactLocales].filter((locale) => !requestLocales.has(locale)).sort();
|
|
64
|
+
if (excess.length > 0) {
|
|
65
|
+
issues.push({
|
|
66
|
+
path: ["deliveryArtifacts"],
|
|
67
|
+
message: `deliveryArtifacts includes locales not required by dictionaries and localeFallback: ${excess.join(", ")}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return issues;
|
|
71
|
+
}
|
|
72
|
+
function getDeliveryArtifactsIssues(deliveryArtifacts, requestLocales) {
|
|
73
|
+
return [
|
|
74
|
+
...getDeliveryArtifactsStructureIssues(deliveryArtifacts),
|
|
75
|
+
...getDeliveryArtifactsPartitionIssues(deliveryArtifacts, requestLocales)
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function getDeliveryAreaNames(deliveryArtifacts) {
|
|
79
|
+
return Object.keys(deliveryArtifacts).sort();
|
|
80
|
+
}
|
|
81
|
+
function getLocaleDeliveryAreaMap(deliveryArtifacts) {
|
|
82
|
+
const localeToArea = {};
|
|
83
|
+
for (const [area, locales] of Object.entries(deliveryArtifacts)) {
|
|
84
|
+
for (const locale of locales) {
|
|
85
|
+
localeToArea[locale] = area;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return localeToArea;
|
|
89
|
+
}
|
|
90
|
+
function formatLocaleDeliveryAreaBlock(deliveryArtifacts, constName, localeTypeName, deliveryAreaTypeName) {
|
|
91
|
+
const localeToArea = getLocaleDeliveryAreaMap(deliveryArtifacts);
|
|
92
|
+
const lines = Object.keys(localeToArea).sort().map((locale) => ` ${JSON.stringify(locale)}: ${JSON.stringify(localeToArea[locale])},`).join("\n");
|
|
93
|
+
return `export const ${constName} = {
|
|
94
|
+
${lines}
|
|
95
|
+
} as const satisfies Record<${localeTypeName}, ${deliveryAreaTypeName}>;
|
|
96
|
+
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
function getDeliveryArtifactsTypeName(deliveryAreaTypeName) {
|
|
100
|
+
return deliveryAreaTypeName.endsWith("DeliveryArea") ? deliveryAreaTypeName.replace(/DeliveryArea$/, "DeliveryArtifacts") : `${deliveryAreaTypeName}Artifacts`;
|
|
101
|
+
}
|
|
102
|
+
function formatDeliveryArtifactsBlock(deliveryArtifacts, constName, localeTypeName, deliveryAreaTypeName) {
|
|
103
|
+
const deliveryArtifactsTypeName = getDeliveryArtifactsTypeName(deliveryAreaTypeName);
|
|
104
|
+
const areaEntries = getDeliveryAreaNames(deliveryArtifacts).map((area) => {
|
|
105
|
+
const locales = [...deliveryArtifacts[area]].sort().map((locale) => JSON.stringify(locale)).join(", ");
|
|
106
|
+
return ` ${JSON.stringify(area)}: [${locales}] as const,`;
|
|
107
|
+
}).join("\n");
|
|
108
|
+
return `export const ${constName} = {
|
|
109
|
+
${areaEntries}
|
|
110
|
+
} as const satisfies Record<${deliveryAreaTypeName}, readonly ${localeTypeName}[]>;
|
|
111
|
+
|
|
112
|
+
export type ${deliveryArtifactsTypeName} = typeof ${constName};
|
|
113
|
+
|
|
114
|
+
export type LocalesForDeliveryArea<A extends ${deliveryAreaTypeName}> = ${deliveryArtifactsTypeName}[A][number];
|
|
115
|
+
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/codegen-config/type-names.ts
|
|
120
|
+
function inferProjectName(dirName) {
|
|
121
|
+
const parts = dirName.split(/[-_]+/).filter(Boolean);
|
|
122
|
+
if (parts.length === 0) {
|
|
123
|
+
return "App";
|
|
124
|
+
}
|
|
125
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
126
|
+
}
|
|
127
|
+
function typeNamesForProject(project) {
|
|
128
|
+
return {
|
|
129
|
+
paramsTypeName: `${project}Params`,
|
|
130
|
+
schemaTypeName: `${project}Schema`,
|
|
131
|
+
localeTypeName: `${project}Locale`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
46
134
|
|
|
47
135
|
// src/codegen/codegen-config-schema.ts
|
|
48
136
|
var localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));
|
|
49
137
|
var deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));
|
|
50
|
-
var DELIVERY_MODES = ["
|
|
138
|
+
var DELIVERY_MODES = ["split-by-locale", "custom"];
|
|
139
|
+
var LOADER_STRATEGIES = ["import", "fetch"];
|
|
140
|
+
var PROJECT_NAME_PATTERN = /^[A-Z][a-zA-Z0-9]*$/;
|
|
51
141
|
var codegenConfigShape = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
loadOnInit: z.array(z.string().min(1)).optional(),
|
|
62
|
-
namespaceLoadersOutput: z.string().min(1).optional(),
|
|
63
|
-
importExtension: z.enum(SUPPORTED_IMPORT_EXTENSIONS).optional(),
|
|
64
|
-
paramsTypeName: z.string().min(1),
|
|
65
|
-
schemaTypeName: z.string().min(1),
|
|
66
|
-
localeTypeName: z.string().min(1).optional(),
|
|
67
|
-
localeFallbackConstName: z.string().min(1).optional(),
|
|
142
|
+
/** PascalCase project id — generates `{project}Params`, `{project}Schema`, `{project}Locale`. */
|
|
143
|
+
projectName: z.string().min(1).regex(PROJECT_NAME_PATTERN, 'projectName must be PascalCase (e.g. "MyApp")'),
|
|
144
|
+
namespaces: z.record(z.string(), z.string().min(1)),
|
|
145
|
+
/**
|
|
146
|
+
* Directory for generated TypeScript modules:
|
|
147
|
+
* `i18n-types.generated.ts`, `instance.generated.ts`,
|
|
148
|
+
* `namespace-loaders.generated.ts`, `dictionary-schema.generated.ts`.
|
|
149
|
+
*/
|
|
150
|
+
codegenPath: z.string().min(1),
|
|
68
151
|
localeFallback: localeFallbackSchema.optional(),
|
|
69
|
-
|
|
70
|
-
delivery: z.enum(DELIVERY_MODES).optional().default("canonical"),
|
|
152
|
+
delivery: z.enum(DELIVERY_MODES).optional().default("split-by-locale"),
|
|
71
153
|
deliveryArtifacts: deliveryArtifactsSchema.optional(),
|
|
72
|
-
|
|
154
|
+
/** Directory for delivery JSON (`translations/`). Defaults to {@link codegenPath}. */
|
|
155
|
+
artifactsPath: z.string().min(1).optional(),
|
|
156
|
+
/**
|
|
157
|
+
* How generated `namespaceLoaders` resolve artifacts.
|
|
158
|
+
* - `import` (default): dynamic `import()` — JSON is bundled; content updates need a rebuild.
|
|
159
|
+
* - `fetch`: runtime `fetchImpl({ locale, namespace, area? })` via required `createI18n({ fetchImpl })`.
|
|
160
|
+
* Codegen does not know URLs — mapping id → transport is an application concern.
|
|
161
|
+
*/
|
|
162
|
+
loaderStrategy: z.enum(LOADER_STRATEGIES).optional().default("import")
|
|
73
163
|
};
|
|
74
164
|
var codegenConfigKeys = Object.keys(
|
|
75
165
|
codegenConfigShape
|
|
76
166
|
);
|
|
77
|
-
z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (config.namespaces) {
|
|
87
|
-
for (const namespace of Object.keys(config.namespaces)) {
|
|
88
|
-
if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
|
|
89
|
-
ctx.addIssue({
|
|
90
|
-
code: "custom",
|
|
91
|
-
path: ["namespaces", namespace],
|
|
92
|
-
message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`
|
|
93
|
-
});
|
|
94
|
-
}
|
|
167
|
+
var codegenConfigSchema = z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
|
|
168
|
+
for (const namespace of Object.keys(config.namespaces)) {
|
|
169
|
+
if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: "custom",
|
|
172
|
+
path: ["namespaces", namespace],
|
|
173
|
+
message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`
|
|
174
|
+
});
|
|
95
175
|
}
|
|
96
176
|
}
|
|
97
|
-
if (config.defaultNamespace !== void 0 && !IDENTIFIER_NAME_PATTERN.test(config.defaultNamespace)) {
|
|
98
|
-
ctx.addIssue({
|
|
99
|
-
code: "custom",
|
|
100
|
-
path: ["defaultNamespace"],
|
|
101
|
-
message: `Invalid namespace name "${config.defaultNamespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
177
|
if (config.delivery === "custom") {
|
|
105
178
|
if (!config.deliveryArtifacts) {
|
|
106
179
|
ctx.addIssue({
|
|
@@ -124,71 +197,1490 @@ z.object(codegenConfigShape).strict().superRefine((config, ctx) => {
|
|
|
124
197
|
message: 'deliveryArtifacts is only allowed when delivery is "custom".'
|
|
125
198
|
});
|
|
126
199
|
}
|
|
127
|
-
if (config.loadOnInit !== void 0 && config.delivery !== "canonical") {
|
|
128
|
-
ctx.addIssue({
|
|
129
|
-
code: "custom",
|
|
130
|
-
path: ["loadOnInit"],
|
|
131
|
-
message: 'loadOnInit is only allowed when delivery is "canonical".'
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
200
|
});
|
|
135
|
-
|
|
136
|
-
|
|
201
|
+
var GENERATED_BASENAMES = {
|
|
202
|
+
types: "i18n-types.generated.ts",
|
|
203
|
+
instance: "instance.generated.ts",
|
|
204
|
+
namespaceLoaders: "namespace-loaders.generated.ts",
|
|
205
|
+
dictionarySchema: "dictionary-schema.generated.ts"
|
|
206
|
+
};
|
|
207
|
+
var DEFAULT_FACTORY_NAME = "createI18n";
|
|
208
|
+
var DEFAULT_LOCALE_FALLBACK_CONST_NAME = "LOCALE_FALLBACK";
|
|
209
|
+
function resolveCodegenPaths(config) {
|
|
210
|
+
const typeNames = typeNamesForProject(config.projectName);
|
|
211
|
+
const codegenPath = config.codegenPath;
|
|
212
|
+
return {
|
|
213
|
+
codegenPath,
|
|
214
|
+
typesOutput: path8.join(codegenPath, GENERATED_BASENAMES.types),
|
|
215
|
+
instanceOutput: path8.join(codegenPath, GENERATED_BASENAMES.instance),
|
|
216
|
+
namespaceLoadersOutput: path8.join(codegenPath, GENERATED_BASENAMES.namespaceLoaders),
|
|
217
|
+
dictionarySchemaOutput: path8.join(codegenPath, GENERATED_BASENAMES.dictionarySchema),
|
|
218
|
+
artifactsPath: config.artifactsPath ?? codegenPath,
|
|
219
|
+
paramsTypeName: typeNames.paramsTypeName,
|
|
220
|
+
schemaTypeName: typeNames.schemaTypeName,
|
|
221
|
+
localeTypeName: typeNames.localeTypeName,
|
|
222
|
+
localeFallbackConstName: DEFAULT_LOCALE_FALLBACK_CONST_NAME,
|
|
223
|
+
factoryName: DEFAULT_FACTORY_NAME,
|
|
224
|
+
deliveryAreaTypeName: `${config.projectName}DeliveryArea`
|
|
225
|
+
};
|
|
137
226
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return config.dictionaryOutput ?? path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME);
|
|
227
|
+
function resolveArtifactsPath(config) {
|
|
228
|
+
return config.artifactsPath ?? config.codegenPath;
|
|
141
229
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
230
|
+
function formatCodegenConfigIssues(error) {
|
|
231
|
+
const issueLines = error.issues.map((issue) => {
|
|
232
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
233
|
+
return ` - ${path10}: ${issue.message}`;
|
|
234
|
+
});
|
|
235
|
+
return [
|
|
236
|
+
"[Codegen Error] Invalid i18n.codegen.json:",
|
|
237
|
+
...issueLines,
|
|
238
|
+
"",
|
|
239
|
+
`Allowed keys: ${codegenConfigKeys.join(", ")}`
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
function loadConfig(configPath) {
|
|
243
|
+
let raw;
|
|
244
|
+
try {
|
|
245
|
+
raw = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
+
throw new Error(`[Codegen Error] Failed to parse config JSON (${configPath}): ${message}`);
|
|
148
249
|
}
|
|
149
|
-
|
|
250
|
+
const result = codegenConfigSchema.safeParse(raw);
|
|
251
|
+
if (!result.success) {
|
|
252
|
+
throw new Error(formatCodegenConfigIssues(result.error));
|
|
253
|
+
}
|
|
254
|
+
return result.data;
|
|
150
255
|
}
|
|
151
|
-
function
|
|
152
|
-
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
256
|
+
function resolveNamespaces(config) {
|
|
257
|
+
return Object.entries(config.namespaces).map(([namespace, filePath]) => ({
|
|
258
|
+
namespace,
|
|
259
|
+
filePath
|
|
260
|
+
}));
|
|
157
261
|
}
|
|
158
262
|
|
|
159
263
|
// src/codegen-config/build-config.ts
|
|
160
264
|
var GENERATED_DIR = "generated";
|
|
161
265
|
var TRANSLATIONS_DIR = "translations";
|
|
162
|
-
function buildCodegenConfig(
|
|
163
|
-
const typeNames = typeNamesForProject(project);
|
|
164
|
-
const base = {
|
|
165
|
-
typesOutput: `${GENERATED_DIR}/i18n-types.generated.ts`,
|
|
166
|
-
dictionaryOutput: `${GENERATED_DIR}/dictionary.generated.ts`,
|
|
167
|
-
instanceOutput: `${GENERATED_DIR}/instance.generated.ts`,
|
|
168
|
-
paramsTypeName: typeNames.paramsTypeName,
|
|
169
|
-
schemaTypeName: typeNames.schemaTypeName,
|
|
170
|
-
localeTypeName: typeNames.localeTypeName,
|
|
171
|
-
factoryName: "createI18n"
|
|
172
|
-
};
|
|
173
|
-
if (mode === "single") {
|
|
174
|
-
return {
|
|
175
|
-
dictionary: `${TRANSLATIONS_DIR}/translations.json`,
|
|
176
|
-
...base
|
|
177
|
-
};
|
|
178
|
-
}
|
|
266
|
+
function buildCodegenConfig(project) {
|
|
179
267
|
return {
|
|
268
|
+
projectName: project,
|
|
180
269
|
namespaces: {
|
|
181
270
|
default: `${TRANSLATIONS_DIR}/default.json`
|
|
182
271
|
},
|
|
183
|
-
|
|
272
|
+
delivery: "split-by-locale",
|
|
273
|
+
codegenPath: GENERATED_DIR
|
|
184
274
|
};
|
|
185
275
|
}
|
|
186
276
|
function writeCodegenConfig(configPath, config) {
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
fs3.mkdirSync(path8.dirname(configPath), { recursive: true });
|
|
278
|
+
fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
279
|
+
`);
|
|
280
|
+
}
|
|
281
|
+
var GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
|
|
282
|
+
var DEFAULT_IMPORT_EXTENSION = "none";
|
|
283
|
+
function reportCodegenIssues(issues) {
|
|
284
|
+
for (const issue of issues) {
|
|
285
|
+
console.error(`[Codegen Error] ${issue.message}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function toImportPath(fromFile, toFile) {
|
|
289
|
+
const relative = path8.relative(path8.dirname(fromFile), toFile).replace(/\\/g, "/");
|
|
290
|
+
const withoutExt = relative.replace(/\.(json|ya?ml)$/i, "");
|
|
291
|
+
return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
|
|
292
|
+
}
|
|
293
|
+
function toModuleBasename(filePath) {
|
|
294
|
+
return path8.basename(filePath).replace(/\.ts$/, "");
|
|
295
|
+
}
|
|
296
|
+
function importExtensionSuffix(importExtension) {
|
|
297
|
+
return importExtension === "none" ? "" : importExtension;
|
|
298
|
+
}
|
|
299
|
+
function toRelativeModuleImport(moduleBasename, importExtension) {
|
|
300
|
+
return `./${moduleBasename}${importExtensionSuffix(importExtension)}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/codegen/emit/dictionary-schema-file.ts
|
|
304
|
+
function formatVariableSpecObject(spec) {
|
|
305
|
+
const entries = Object.entries(spec);
|
|
306
|
+
if (entries.length === 0) {
|
|
307
|
+
return "{}";
|
|
308
|
+
}
|
|
309
|
+
const lines = entries.map(
|
|
310
|
+
([name, type]) => ` ${JSON.stringify(name)}: ${JSON.stringify(type)},`
|
|
311
|
+
);
|
|
312
|
+
return `{
|
|
313
|
+
${lines.join("\n")}
|
|
314
|
+
}`;
|
|
315
|
+
}
|
|
316
|
+
function formatDictionarySpecBlock(entries, argsSpecByNamespace) {
|
|
317
|
+
const requiredKeysLines = entries.map((entry) => {
|
|
318
|
+
const keys = Object.keys(argsSpecByNamespace[entry.namespace] ?? {}).map((key) => JSON.stringify(key)).join(", ");
|
|
319
|
+
return ` ${JSON.stringify(entry.namespace)}: [${keys}] as const,`;
|
|
320
|
+
}).join("\n");
|
|
321
|
+
const argsByKeyLines = entries.map((entry) => {
|
|
322
|
+
const argsByKey = argsSpecByNamespace[entry.namespace] ?? {};
|
|
323
|
+
const keyLines = Object.entries(argsByKey).map(([key, spec]) => ` ${JSON.stringify(key)}: ${formatVariableSpecObject(spec)},`).join("\n");
|
|
324
|
+
return ` ${JSON.stringify(entry.namespace)}: {
|
|
325
|
+
${keyLines}
|
|
326
|
+
},`;
|
|
327
|
+
}).join("\n");
|
|
328
|
+
return `export const DICTIONARY_SPEC = {
|
|
329
|
+
mode: 'multi' as const,
|
|
330
|
+
requiredKeys: {
|
|
331
|
+
${requiredKeysLines}
|
|
332
|
+
},
|
|
333
|
+
argsByKey: {
|
|
334
|
+
${argsByKeyLines}
|
|
335
|
+
},
|
|
336
|
+
} satisfies DictionarySpec;
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
function formatDictionarySchemaFile(schemaTypeName, typesModule, dictionarySpecBlock, importExtension) {
|
|
340
|
+
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
341
|
+
const partialImport = ` validateExternalDictionaryPartial as validateExternalDictionaryPartialCore,
|
|
342
|
+
validateExternalNamespacePartial as validateExternalNamespacePartialCore,
|
|
343
|
+
validateExternalKey as validateExternalKeyCore,
|
|
344
|
+
`;
|
|
345
|
+
const multiValidators = `export function validateExternalNamespacePartial<NS extends keyof ${schemaTypeName}>(
|
|
346
|
+
namespace: NS,
|
|
347
|
+
input: unknown,
|
|
348
|
+
): ValidationResult<Partial<${schemaTypeName}[NS]>> {
|
|
349
|
+
return validateExternalNamespacePartialCore<Partial<${schemaTypeName}[NS]>>(
|
|
350
|
+
namespace as string,
|
|
351
|
+
input,
|
|
352
|
+
DICTIONARY_SPEC,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function validateExternalKey<
|
|
357
|
+
NS extends keyof ${schemaTypeName},
|
|
358
|
+
K extends keyof ${schemaTypeName}[NS],
|
|
359
|
+
>(
|
|
360
|
+
namespace: NS,
|
|
361
|
+
key: K,
|
|
362
|
+
input: unknown,
|
|
363
|
+
): ValidationResult<Pick<${schemaTypeName}[NS], K>> {
|
|
364
|
+
return validateExternalKeyCore<Pick<${schemaTypeName}[NS], K>>(
|
|
365
|
+
namespace as string,
|
|
366
|
+
key as string,
|
|
367
|
+
input,
|
|
368
|
+
DICTIONARY_SPEC,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
`;
|
|
373
|
+
return `${GENERATED_FILE_BANNER}import {
|
|
374
|
+
` + partialImport + ` type DictionarySpec,
|
|
375
|
+
type ValidationResult,
|
|
376
|
+
} from '@xndrjs/i18n/validation';
|
|
377
|
+
import type { ${schemaTypeName} } from '${typesImport}';
|
|
378
|
+
|
|
379
|
+
${dictionarySpecBlock}
|
|
380
|
+
export function validateExternalDictionaryPartial(
|
|
381
|
+
input: unknown,
|
|
382
|
+
): ValidationResult<Partial<${schemaTypeName}>> {
|
|
383
|
+
return validateExternalDictionaryPartialCore<Partial<${schemaTypeName}>>(
|
|
384
|
+
input,
|
|
385
|
+
DICTIONARY_SPEC,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
` + multiValidators;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/codegen/emit/instance-file.ts
|
|
393
|
+
var STATE_TYPE = `{ dictionary: InitialSchema; resources?: readonly (readonly [string, string])[] }`;
|
|
394
|
+
function formatInstanceFile(options) {
|
|
395
|
+
const {
|
|
396
|
+
typesOutputPath,
|
|
397
|
+
namespaceLoadersOutputPath,
|
|
398
|
+
paramsTypeName,
|
|
399
|
+
schemaTypeName,
|
|
400
|
+
localeTypeName,
|
|
401
|
+
localeFallbackConstName,
|
|
402
|
+
factoryName,
|
|
403
|
+
hasLocaleFallback,
|
|
404
|
+
hasLocaleType,
|
|
405
|
+
importExtension,
|
|
406
|
+
delivery,
|
|
407
|
+
localeDeliveryAreaConstName = "LOCALE_DELIVERY_AREA",
|
|
408
|
+
loaderStrategy = "import"
|
|
409
|
+
} = options;
|
|
410
|
+
const providerClass = "IcuTranslationProviderMulti";
|
|
411
|
+
const typesModule = toModuleBasename(typesOutputPath);
|
|
412
|
+
const typesImport = toRelativeModuleImport(typesModule, importExtension);
|
|
413
|
+
const loadersModule = toModuleBasename(namespaceLoadersOutputPath);
|
|
414
|
+
const loadersImport = toRelativeModuleImport(loadersModule, importExtension);
|
|
415
|
+
const schemaTypesImport = `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from '${typesImport}';
|
|
416
|
+
`;
|
|
417
|
+
const typesImportLine = formatTypesValueImport({
|
|
418
|
+
hasLocaleType,
|
|
419
|
+
hasLocaleFallback,
|
|
420
|
+
localeTypeName,
|
|
421
|
+
localeFallbackConstName,
|
|
422
|
+
localeDeliveryAreaConstName,
|
|
423
|
+
delivery,
|
|
424
|
+
typesImport
|
|
425
|
+
});
|
|
426
|
+
const packageImports = formatPackageImports(providerClass, loaderStrategy);
|
|
427
|
+
const loadersImportLine = loaderStrategy === "fetch" ? `import { createNamespaceLoaders } from '${loadersImport}';
|
|
428
|
+
` : `import { namespaceLoaders } from '${loadersImport}';
|
|
429
|
+
`;
|
|
430
|
+
const createFactoryBlock = formatCreateI18nFactory({
|
|
431
|
+
providerClass,
|
|
432
|
+
schemaTypeName,
|
|
433
|
+
paramsTypeName,
|
|
434
|
+
localeTypeName,
|
|
435
|
+
localeFallbackConstName,
|
|
436
|
+
factoryName,
|
|
437
|
+
hasLocaleFallback,
|
|
438
|
+
delivery,
|
|
439
|
+
localeDeliveryAreaConstName,
|
|
440
|
+
loaderStrategy
|
|
441
|
+
});
|
|
442
|
+
return `${GENERATED_FILE_BANNER}import {
|
|
443
|
+
${packageImports},
|
|
444
|
+
} from '@xndrjs/i18n';
|
|
445
|
+
` + schemaTypesImport + typesImportLine + loadersImportLine + `
|
|
446
|
+
` + createFactoryBlock;
|
|
447
|
+
}
|
|
448
|
+
function formatTypesValueImport(options) {
|
|
449
|
+
const {
|
|
450
|
+
hasLocaleType,
|
|
451
|
+
hasLocaleFallback,
|
|
452
|
+
localeTypeName,
|
|
453
|
+
localeFallbackConstName,
|
|
454
|
+
localeDeliveryAreaConstName,
|
|
455
|
+
delivery,
|
|
456
|
+
typesImport
|
|
457
|
+
} = options;
|
|
458
|
+
if (!hasLocaleType && delivery !== "custom") {
|
|
459
|
+
return "";
|
|
460
|
+
}
|
|
461
|
+
const valueNames = [];
|
|
462
|
+
if (hasLocaleFallback) {
|
|
463
|
+
valueNames.push(localeFallbackConstName);
|
|
464
|
+
}
|
|
465
|
+
if (delivery === "custom") {
|
|
466
|
+
valueNames.push(localeDeliveryAreaConstName);
|
|
467
|
+
}
|
|
468
|
+
if (valueNames.length > 0 && hasLocaleType) {
|
|
469
|
+
return `import { ${valueNames.join(", ")}, type ${localeTypeName} } from '${typesImport}';
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
if (valueNames.length > 0) {
|
|
473
|
+
return `import { ${valueNames.join(", ")} } from '${typesImport}';
|
|
474
|
+
`;
|
|
475
|
+
}
|
|
476
|
+
if (hasLocaleType) {
|
|
477
|
+
return `import type { ${localeTypeName} } from '${typesImport}';
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
function formatCreateI18nFactory(options) {
|
|
483
|
+
const {
|
|
484
|
+
providerClass,
|
|
485
|
+
schemaTypeName,
|
|
486
|
+
paramsTypeName,
|
|
487
|
+
localeTypeName,
|
|
488
|
+
localeFallbackConstName,
|
|
489
|
+
factoryName,
|
|
490
|
+
hasLocaleFallback,
|
|
491
|
+
delivery,
|
|
492
|
+
localeDeliveryAreaConstName,
|
|
493
|
+
loaderStrategy
|
|
494
|
+
} = options;
|
|
495
|
+
const providerTypeArgs = hasLocaleFallback ? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}` : `${schemaTypeName}, ${paramsTypeName}`;
|
|
496
|
+
const handleReturnType = `I18nHandle<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
497
|
+
const handleTypeArgs = `<${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}>`;
|
|
498
|
+
const partitionForLocale = delivery === "custom" ? ` partitionForLocale: (locale) => ${localeDeliveryAreaConstName}[locale],
|
|
499
|
+
` : ` partitionForLocale: (locale) => locale,
|
|
500
|
+
`;
|
|
501
|
+
const optionsType = loaderStrategy === "fetch" ? `{ fetchImpl: FetchArtifact; state?: ${STATE_TYPE}; onMissing?: OnMissingTranslation }` : `{ state?: ${STATE_TYPE}; onMissing?: OnMissingTranslation }`;
|
|
502
|
+
const optionsParam = loaderStrategy === "fetch" ? ` options: ${optionsType},
|
|
503
|
+
` : ` options?: ${optionsType},
|
|
504
|
+
`;
|
|
505
|
+
const namespaceLoadersLine = loaderStrategy === "fetch" ? ` namespaceLoaders: createNamespaceLoaders(fetchImpl),
|
|
506
|
+
` : ` namespaceLoaders,
|
|
507
|
+
`;
|
|
508
|
+
return `export function ${factoryName}(
|
|
509
|
+
` + optionsParam + `): ${handleReturnType} {
|
|
510
|
+
` + formatBody({
|
|
511
|
+
providerClass,
|
|
512
|
+
providerTypeArgs,
|
|
513
|
+
hasLocaleFallback,
|
|
514
|
+
localeFallbackConstName,
|
|
515
|
+
loaderStrategy
|
|
516
|
+
}) + ` engine.seedBuilderResources(normalized.resources);
|
|
517
|
+
return createI18nHandle${handleTypeArgs}(engine, {
|
|
518
|
+
` + namespaceLoadersLine + partitionForLocale + ` } as I18nHandleOptions<${schemaTypeName}, ${localeTypeName}>);
|
|
519
|
+
}
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
function formatBody(options) {
|
|
523
|
+
const {
|
|
524
|
+
providerClass,
|
|
525
|
+
providerTypeArgs,
|
|
526
|
+
hasLocaleFallback,
|
|
527
|
+
localeFallbackConstName,
|
|
528
|
+
loaderStrategy
|
|
529
|
+
} = options;
|
|
530
|
+
if (loaderStrategy === "fetch") {
|
|
531
|
+
const engineBlock2 = hasLocaleFallback ? ` const engine = new ${providerClass}<${providerTypeArgs}>(normalized.dictionary, {
|
|
532
|
+
localeFallback: ${localeFallbackConstName},
|
|
533
|
+
...providerOptions,
|
|
534
|
+
});
|
|
535
|
+
` : ` const engine = new ${providerClass}<${providerTypeArgs}>(normalized.dictionary, providerOptions);
|
|
536
|
+
`;
|
|
537
|
+
return ` const { fetchImpl, state, ...providerOptions } = options;
|
|
538
|
+
const normalized = normalizeI18nCreateInput(state);
|
|
539
|
+
` + engineBlock2;
|
|
540
|
+
}
|
|
541
|
+
const engineBlock = hasLocaleFallback ? ` const engine = new ${providerClass}<${providerTypeArgs}>(normalized.dictionary, {
|
|
542
|
+
localeFallback: ${localeFallbackConstName},
|
|
543
|
+
...providerOptions,
|
|
544
|
+
});
|
|
545
|
+
` : ` const engine = new ${providerClass}<${providerTypeArgs}>(normalized.dictionary, providerOptions);
|
|
546
|
+
`;
|
|
547
|
+
return ` const { state, ...providerOptions } = options ?? {};
|
|
548
|
+
const normalized = normalizeI18nCreateInput(state);
|
|
549
|
+
` + engineBlock;
|
|
550
|
+
}
|
|
551
|
+
function formatPackageImports(providerClass, loaderStrategy) {
|
|
552
|
+
const names = [
|
|
553
|
+
providerClass,
|
|
554
|
+
"createI18nHandle",
|
|
555
|
+
"normalizeI18nCreateInput",
|
|
556
|
+
"type I18nHandle",
|
|
557
|
+
"type I18nHandleOptions",
|
|
558
|
+
"type OnMissingTranslation"
|
|
559
|
+
];
|
|
560
|
+
if (loaderStrategy === "fetch") {
|
|
561
|
+
names.push("type FetchArtifact");
|
|
562
|
+
}
|
|
563
|
+
return names.join(",\n ");
|
|
564
|
+
}
|
|
565
|
+
function formatDefaultNamespacesLiteral(lazyEntries) {
|
|
566
|
+
return [...lazyEntries].map((entry) => entry.namespace).sort().map((namespace) => JSON.stringify(namespace)).join(", ");
|
|
567
|
+
}
|
|
568
|
+
function formatPartitionedTypesImport(options, paramTypeName) {
|
|
569
|
+
const { schemaTypeName, typesModule, importExtension } = options;
|
|
570
|
+
return `import type { ${schemaTypeName}, LazyNamespace, ${paramTypeName} } from '${toRelativeModuleImport(typesModule, importExtension)}';
|
|
571
|
+
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
function formatImportCase(loadersOutputPath, projectRoot, splitRelativePath) {
|
|
575
|
+
const importPath = toImportPath(loadersOutputPath, path8.resolve(projectRoot, splitRelativePath));
|
|
576
|
+
return `return import('${importPath}.json').then((m) => m.default);`;
|
|
577
|
+
}
|
|
578
|
+
function indentBlock(text, spaces) {
|
|
579
|
+
const pad = " ".repeat(spaces);
|
|
580
|
+
return text.split("\n").map((line) => line.length === 0 ? line : `${pad}${line}`).join("\n");
|
|
581
|
+
}
|
|
582
|
+
function formatImportNamespaceLoaderEntry(options) {
|
|
583
|
+
const {
|
|
584
|
+
namespace,
|
|
585
|
+
paramName,
|
|
586
|
+
partitionKeys,
|
|
587
|
+
splitPathsByNamespace,
|
|
588
|
+
loadersOutputPath,
|
|
589
|
+
projectRoot
|
|
590
|
+
} = options;
|
|
591
|
+
const caseBodies = partitionKeys.map((partitionKey) => {
|
|
592
|
+
const splitRelativePath = splitPathsByNamespace[namespace]?.[partitionKey];
|
|
593
|
+
if (!splitRelativePath) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`[Codegen Error] Missing split path for namespace "${namespace}", ${paramName} "${partitionKey}".`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const body = formatImportCase(loadersOutputPath, projectRoot, splitRelativePath);
|
|
599
|
+
return [`case ${JSON.stringify(partitionKey)}:`, ` ${body}`].join("\n");
|
|
600
|
+
});
|
|
601
|
+
const switchBody = [
|
|
602
|
+
...caseBodies,
|
|
603
|
+
`default:`,
|
|
604
|
+
` throw new Error(\`[i18n] No translation artifact for namespace ${JSON.stringify(namespace)} and ${paramName} "\${String(${paramName})}".\`);`
|
|
605
|
+
].join("\n");
|
|
606
|
+
const entry = [
|
|
607
|
+
`${namespace}: (${paramName}) => {`,
|
|
608
|
+
` switch (${paramName}) {`,
|
|
609
|
+
indentBlock(switchBody, 4),
|
|
610
|
+
` }`,
|
|
611
|
+
`},`
|
|
612
|
+
].join("\n");
|
|
613
|
+
return indentBlock(entry, 2);
|
|
614
|
+
}
|
|
615
|
+
function formatFetchNamespaceLoaderEntry(options) {
|
|
616
|
+
const { namespace, paramName, schemaTypeName, delivery } = options;
|
|
617
|
+
const nsLiteral = JSON.stringify(namespace);
|
|
618
|
+
const resourceId = delivery === "custom" ? `{ locale, namespace: ${nsLiteral}, area }` : `{ locale, namespace: ${nsLiteral} }`;
|
|
619
|
+
const signature = delivery === "custom" ? `(${paramName}, { locale })` : `(${paramName})`;
|
|
620
|
+
return indentBlock(
|
|
621
|
+
`${namespace}: ${signature} =>
|
|
622
|
+
fetchImpl(${resourceId}) as Promise<${schemaTypeName}[${nsLiteral}]>,`,
|
|
623
|
+
4
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
function formatPartitionedNamespaceLoadersFile(options, params) {
|
|
627
|
+
const { partitionKeys, paramName, paramTypeName } = params;
|
|
628
|
+
const {
|
|
629
|
+
loadersOutputPath,
|
|
630
|
+
lazyEntries,
|
|
631
|
+
schemaTypeName,
|
|
632
|
+
projectRoot,
|
|
633
|
+
delivery,
|
|
634
|
+
splitPathsByNamespace = {}
|
|
635
|
+
} = options;
|
|
636
|
+
const loaderStrategy = options.loaderStrategy ?? "import";
|
|
637
|
+
const loaderValueType = `${schemaTypeName}[K]`;
|
|
638
|
+
const typesImport = formatPartitionedTypesImport(options, paramTypeName);
|
|
639
|
+
if (loaderStrategy === "fetch") {
|
|
640
|
+
const fetchLoadersType = `{
|
|
641
|
+
[K in LazyNamespace]: (${delivery === "custom" ? `${paramName}: ${paramTypeName}, context: { locale: string }` : `${paramName}: ${paramTypeName}`}) => Promise<${loaderValueType}>;
|
|
642
|
+
}`;
|
|
643
|
+
const loaderEntries2 = lazyEntries.map(
|
|
644
|
+
(entry) => formatFetchNamespaceLoaderEntry({
|
|
645
|
+
namespace: entry.namespace,
|
|
646
|
+
paramName,
|
|
647
|
+
schemaTypeName,
|
|
648
|
+
delivery
|
|
649
|
+
})
|
|
650
|
+
).join("\n");
|
|
651
|
+
return `${GENERATED_FILE_BANNER}import type { FetchArtifact } from '@xndrjs/i18n';
|
|
652
|
+
` + typesImport + `export type NamespaceLoaders = ${fetchLoadersType};
|
|
653
|
+
|
|
654
|
+
/** Build loaders that resolve artifacts via the injected {@link FetchArtifact} (resource id only). */
|
|
655
|
+
export function createNamespaceLoaders(fetchImpl: FetchArtifact): NamespaceLoaders {
|
|
656
|
+
return {
|
|
657
|
+
${loaderEntries2}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;
|
|
662
|
+
`;
|
|
663
|
+
}
|
|
664
|
+
const loaderEntries = lazyEntries.map(
|
|
665
|
+
(entry) => formatImportNamespaceLoaderEntry({
|
|
666
|
+
namespace: entry.namespace,
|
|
667
|
+
paramName,
|
|
668
|
+
partitionKeys,
|
|
669
|
+
splitPathsByNamespace,
|
|
670
|
+
loadersOutputPath,
|
|
671
|
+
projectRoot
|
|
672
|
+
})
|
|
673
|
+
).join("\n");
|
|
674
|
+
const importLoadersType = `{
|
|
675
|
+
[K in LazyNamespace]: (${paramName}: ${paramTypeName}) => Promise<${loaderValueType}>;
|
|
676
|
+
}`;
|
|
677
|
+
return `${GENERATED_FILE_BANNER}` + typesImport + `export const namespaceLoaders: ${importLoadersType} = {
|
|
678
|
+
${loaderEntries}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
export const defaultLazyNamespaces = [${formatDefaultNamespacesLiteral(lazyEntries)}] as const;
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
function formatNamespaceLoadersFile(options) {
|
|
685
|
+
if (options.delivery === "split-by-locale") {
|
|
686
|
+
return formatPartitionedNamespaceLoadersFile(options, {
|
|
687
|
+
partitionKeys: options.requestLocales ?? [],
|
|
688
|
+
paramName: "locale",
|
|
689
|
+
paramTypeName: options.localeTypeName
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (!options.deliveryAreaTypeName) {
|
|
693
|
+
throw new Error("[Codegen Error] deliveryAreaTypeName is required for custom delivery.");
|
|
694
|
+
}
|
|
695
|
+
return formatPartitionedNamespaceLoadersFile(options, {
|
|
696
|
+
partitionKeys: options.deliveryAreaNames ?? [],
|
|
697
|
+
paramName: "area",
|
|
698
|
+
paramTypeName: options.deliveryAreaTypeName
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/resolve-locale.ts
|
|
703
|
+
function validateLocaleFallback(map) {
|
|
704
|
+
for (const startLocale of Object.keys(map)) {
|
|
705
|
+
const visited = /* @__PURE__ */ new Set();
|
|
706
|
+
let current = startLocale;
|
|
707
|
+
while (current !== void 0) {
|
|
708
|
+
if (visited.has(current)) {
|
|
709
|
+
throw new Error(`[i18n] Circular locale fallback detected involving "${current}"`);
|
|
710
|
+
}
|
|
711
|
+
visited.add(current);
|
|
712
|
+
const next = map[current];
|
|
713
|
+
if (next === void 0 || next === null) {
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
current = next;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function resolveLocaleTemplate(localeByKey, requestedLocale, fallbackMap) {
|
|
721
|
+
const chain = [requestedLocale];
|
|
722
|
+
let currentLocale = requestedLocale;
|
|
723
|
+
while (true) {
|
|
724
|
+
const template = localeByKey?.[currentLocale];
|
|
725
|
+
if (template !== void 0) {
|
|
726
|
+
return { template, resolvedLocale: currentLocale, fallbackChain: chain };
|
|
727
|
+
}
|
|
728
|
+
if (!fallbackMap) {
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
const fallback = fallbackMap[currentLocale];
|
|
732
|
+
if (fallback === void 0 || fallback === null) {
|
|
733
|
+
return void 0;
|
|
734
|
+
}
|
|
735
|
+
if (chain.includes(fallback)) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`[i18n] Circular locale fallback detected: ${[...chain, fallback].join(" \u2192 ")}`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
chain.push(fallback);
|
|
741
|
+
currentLocale = fallback;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/codegen/locale-fallback.ts
|
|
746
|
+
function collectRequestLocales(dictionaryLocales, fallback) {
|
|
747
|
+
const all = new Set(dictionaryLocales);
|
|
748
|
+
if (!fallback) {
|
|
749
|
+
return all;
|
|
750
|
+
}
|
|
751
|
+
for (const [locale, target] of Object.entries(fallback)) {
|
|
752
|
+
all.add(locale);
|
|
753
|
+
if (target !== null) {
|
|
754
|
+
all.add(target);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return all;
|
|
758
|
+
}
|
|
759
|
+
function getCodegenLocaleFallbackIssues(fallback, dictionaryLocales) {
|
|
760
|
+
const issues = [];
|
|
761
|
+
try {
|
|
762
|
+
validateLocaleFallback(fallback);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
765
|
+
issues.push({ path: ["localeFallback"], message });
|
|
766
|
+
}
|
|
767
|
+
for (const [locale, target] of Object.entries(fallback)) {
|
|
768
|
+
if (target !== null && !(target in fallback) && !dictionaryLocales.has(target)) {
|
|
769
|
+
issues.push({
|
|
770
|
+
path: ["localeFallback", locale],
|
|
771
|
+
message: `localeFallback: "${locale}" points to "${target}" which is not defined in the fallback map or dictionary locales`
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return issues;
|
|
776
|
+
}
|
|
777
|
+
function formatLocaleFallbackBlock(fallback, constName, typeName) {
|
|
778
|
+
const lines = Object.entries(fallback).map(
|
|
779
|
+
([locale, target]) => ` ${JSON.stringify(locale)}: ${target === null ? "null" : JSON.stringify(target)},`
|
|
780
|
+
).join("\n");
|
|
781
|
+
return `export const ${constName} = {
|
|
782
|
+
${lines}
|
|
783
|
+
} as const satisfies Record<string, string | null>;
|
|
784
|
+
|
|
785
|
+
export type ${typeName} = typeof ${constName};
|
|
786
|
+
|
|
787
|
+
`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/codegen/emit/types-file.ts
|
|
791
|
+
function formatLazyTypesBlock(lazyEntries) {
|
|
792
|
+
const lazyUnion = lazyEntries.length > 0 ? lazyEntries.map((entry) => `'${entry.namespace}'`).join(" | ") : "never";
|
|
793
|
+
return `export type LazyNamespace = ${lazyUnion};
|
|
794
|
+
/** Empty cold-start schema \u2014 namespaces arrive via \`namespaceLoaders\`. */
|
|
795
|
+
export type InitialSchema = Record<string, never>;
|
|
796
|
+
|
|
797
|
+
`;
|
|
798
|
+
}
|
|
799
|
+
function formatLocaleTemplateType(localeTypeName, hasLocaleUnion) {
|
|
800
|
+
if (!hasLocaleUnion) {
|
|
801
|
+
return "Partial<Record<string, string>>";
|
|
802
|
+
}
|
|
803
|
+
return `Partial<Record<${localeTypeName}, string>>`;
|
|
804
|
+
}
|
|
805
|
+
function formatTypesFile(options) {
|
|
806
|
+
const {
|
|
807
|
+
entries,
|
|
808
|
+
paramsTypeName,
|
|
809
|
+
schemaTypeName,
|
|
810
|
+
localeTypeName,
|
|
811
|
+
localeFallbackConstName,
|
|
812
|
+
localeFallbackTypeName,
|
|
813
|
+
localeFallback,
|
|
814
|
+
paramsByNamespace,
|
|
815
|
+
requestLocales,
|
|
816
|
+
deliveryAreaTypeName,
|
|
817
|
+
deliveryAreaNames,
|
|
818
|
+
deliveryArtifacts,
|
|
819
|
+
localeDeliveryAreaConstName = "LOCALE_DELIVERY_AREA",
|
|
820
|
+
lazyEntries
|
|
821
|
+
} = options;
|
|
822
|
+
const hasLocaleUnion = requestLocales.length > 0;
|
|
823
|
+
const localeTemplateType = formatLocaleTemplateType(localeTypeName, hasLocaleUnion);
|
|
824
|
+
const localesConstName = `${localeTypeName}s`;
|
|
825
|
+
const localeBlock = hasLocaleUnion ? `${localeFallback ? formatLocaleFallbackBlock(localeFallback, localeFallbackConstName, localeFallbackTypeName) : ""}export const ${localesConstName} = [${requestLocales.map((locale) => JSON.stringify(locale)).join(", ")}] as const;
|
|
826
|
+
export type ${localeTypeName} = (typeof ${localesConstName})[number];
|
|
827
|
+
|
|
828
|
+
` : "";
|
|
829
|
+
const deliveryAreasConstName = deliveryAreaTypeName ? `${deliveryAreaTypeName}s` : void 0;
|
|
830
|
+
const deliveryAreaBlock = deliveryAreaTypeName && deliveryAreasConstName && deliveryAreaNames && deliveryAreaNames.length > 0 ? `export const ${deliveryAreasConstName} = [${deliveryAreaNames.map((area) => JSON.stringify(area)).join(", ")}] as const;
|
|
831
|
+
export type ${deliveryAreaTypeName} = (typeof ${deliveryAreasConstName})[number];
|
|
832
|
+
|
|
833
|
+
` + (deliveryArtifacts ? formatDeliveryArtifactsBlock(
|
|
834
|
+
deliveryArtifacts,
|
|
835
|
+
"DELIVERY_ARTIFACTS",
|
|
836
|
+
localeTypeName,
|
|
837
|
+
deliveryAreaTypeName
|
|
838
|
+
) + formatLocaleDeliveryAreaBlock(
|
|
839
|
+
deliveryArtifacts,
|
|
840
|
+
localeDeliveryAreaConstName,
|
|
841
|
+
localeTypeName,
|
|
842
|
+
deliveryAreaTypeName
|
|
843
|
+
) : "") : "";
|
|
844
|
+
const namespaceBlocks = entries.map((entry) => {
|
|
845
|
+
const keyTypes = paramsByNamespace[entry.namespace] ?? {};
|
|
846
|
+
const lines = Object.entries(keyTypes).map(([key, type]) => ` ${key}: ${type};`).join("\n");
|
|
847
|
+
return ` ${entry.namespace}: {
|
|
848
|
+
${lines}
|
|
849
|
+
};`;
|
|
850
|
+
}).join("\n");
|
|
851
|
+
const paramsBlock = `export type ${paramsTypeName} = {
|
|
852
|
+
${namespaceBlocks}
|
|
853
|
+
};`;
|
|
854
|
+
const schemaLines = entries.map((entry) => {
|
|
855
|
+
const keyTypes = paramsByNamespace[entry.namespace] ?? {};
|
|
856
|
+
const lines = Object.keys(keyTypes).map((key) => ` ${key}: ${localeTemplateType};`).join("\n");
|
|
857
|
+
return ` ${entry.namespace}: {
|
|
858
|
+
${lines}
|
|
859
|
+
};`;
|
|
860
|
+
}).join("\n");
|
|
861
|
+
const schemaBlock = `export type ${schemaTypeName} = {
|
|
862
|
+
${schemaLines}
|
|
863
|
+
};`;
|
|
864
|
+
const lazyTypesBlock = formatLazyTypesBlock(lazyEntries);
|
|
865
|
+
return `${GENERATED_FILE_BANNER}export const I18N_MODE = 'multi' as const;
|
|
866
|
+
|
|
867
|
+
${localeBlock}${deliveryAreaBlock}${paramsBlock}
|
|
868
|
+
|
|
869
|
+
${schemaBlock}
|
|
870
|
+
${lazyTypesBlock}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/icu/extract-variables.ts
|
|
874
|
+
function isPluralOrOrdinalNode(node) {
|
|
875
|
+
return node.type === 6 && "pluralType" in node && Boolean(node.pluralType);
|
|
876
|
+
}
|
|
877
|
+
function hasVariableName(node) {
|
|
878
|
+
return "value" in node && typeof node.value === "string";
|
|
879
|
+
}
|
|
880
|
+
function addVariableMeta(variables, name, type, role) {
|
|
881
|
+
const existing = variables[name];
|
|
882
|
+
if (!existing) {
|
|
883
|
+
variables[name] = { type, roles: /* @__PURE__ */ new Set([role]) };
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
existing.roles.add(role);
|
|
887
|
+
existing.type = mergeVariableTypes(existing.type, type);
|
|
888
|
+
}
|
|
889
|
+
function mergeVariableTypes(left, right) {
|
|
890
|
+
if (left === right) {
|
|
891
|
+
return left;
|
|
892
|
+
}
|
|
893
|
+
if (left === "string" && right === "number" || left === "number" && right === "string") {
|
|
894
|
+
return "number";
|
|
895
|
+
}
|
|
896
|
+
return right;
|
|
897
|
+
}
|
|
898
|
+
function extractVariableMeta(nodes) {
|
|
899
|
+
const variables = {};
|
|
900
|
+
const walk = (walkNodes) => {
|
|
901
|
+
for (const node of walkNodes) {
|
|
902
|
+
if (node.type === 1 && hasVariableName(node)) {
|
|
903
|
+
addVariableMeta(variables, node.value, "string", "simple");
|
|
904
|
+
} else if (node.type === 2 && hasVariableName(node)) {
|
|
905
|
+
addVariableMeta(variables, node.value, "number", "number");
|
|
906
|
+
} else if (isPluralOrOrdinalNode(node) && hasVariableName(node)) {
|
|
907
|
+
const role = node.pluralType === "ordinal" ? "selectordinal" : "plural";
|
|
908
|
+
addVariableMeta(variables, node.value, "number", role);
|
|
909
|
+
} else if (node.type === 3 && hasVariableName(node)) {
|
|
910
|
+
addVariableMeta(variables, node.value, "date", "date");
|
|
911
|
+
} else if (node.type === 4 && hasVariableName(node)) {
|
|
912
|
+
addVariableMeta(variables, node.value, "date", "time");
|
|
913
|
+
} else if (node.type === 5 && hasVariableName(node)) {
|
|
914
|
+
addVariableMeta(variables, node.value, "string", "select");
|
|
915
|
+
}
|
|
916
|
+
if ("options" in node && node.options) {
|
|
917
|
+
for (const option of Object.values(node.options)) {
|
|
918
|
+
walk(option.value);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
922
|
+
walk(node.children);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
walk(nodes);
|
|
927
|
+
return variables;
|
|
928
|
+
}
|
|
929
|
+
function inferMergedVariableType(roles) {
|
|
930
|
+
const hasPlural = roles.has("plural");
|
|
931
|
+
const hasSelectordinal = roles.has("selectordinal");
|
|
932
|
+
const hasSelect = roles.has("select");
|
|
933
|
+
const hasSimple = roles.has("simple");
|
|
934
|
+
const hasNumber = roles.has("number");
|
|
935
|
+
const hasDate = roles.has("date");
|
|
936
|
+
const hasTime = roles.has("time");
|
|
937
|
+
if (hasPlural && hasSelectordinal) {
|
|
938
|
+
return "CONFLICT";
|
|
939
|
+
}
|
|
940
|
+
if (hasSelect && (hasPlural || hasSelectordinal || hasNumber)) {
|
|
941
|
+
return "CONFLICT";
|
|
942
|
+
}
|
|
943
|
+
if (hasPlural && hasSelect) {
|
|
944
|
+
return "CONFLICT";
|
|
945
|
+
}
|
|
946
|
+
if (hasSelectordinal && hasSelect) {
|
|
947
|
+
return "CONFLICT";
|
|
948
|
+
}
|
|
949
|
+
if (hasDate || hasTime) {
|
|
950
|
+
if (hasPlural || hasSelect || hasSelectordinal || hasSimple || hasNumber) {
|
|
951
|
+
return "CONFLICT";
|
|
952
|
+
}
|
|
953
|
+
return "date";
|
|
954
|
+
}
|
|
955
|
+
if (hasPlural || hasSelectordinal || hasNumber) {
|
|
956
|
+
return "number";
|
|
957
|
+
}
|
|
958
|
+
if (hasSelect || hasSimple) {
|
|
959
|
+
return "string";
|
|
960
|
+
}
|
|
961
|
+
return "string";
|
|
962
|
+
}
|
|
963
|
+
function mergeVariableMetaAcrossLocales(localeMetas) {
|
|
964
|
+
const keySignatures = localeMetas.map((meta) => Object.keys(meta).sort().join(","));
|
|
965
|
+
if (new Set(keySignatures).size > 1) {
|
|
966
|
+
return {
|
|
967
|
+
ok: false,
|
|
968
|
+
message: `Inconsistent ICU variable names across locales (keys: ${keySignatures.join(" vs ")})`
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const merged = {};
|
|
972
|
+
const variableNames = new Set(localeMetas.flatMap((meta) => Object.keys(meta)));
|
|
973
|
+
for (const variableName of variableNames) {
|
|
974
|
+
const combinedRoles = /* @__PURE__ */ new Set();
|
|
975
|
+
for (const meta of localeMetas) {
|
|
976
|
+
for (const role of meta[variableName]?.roles ?? []) {
|
|
977
|
+
combinedRoles.add(role);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const mergedType = inferMergedVariableType(combinedRoles);
|
|
981
|
+
if (mergedType === "CONFLICT") {
|
|
982
|
+
const rolesByLocale = localeMetas.map((meta) => {
|
|
983
|
+
const entry = meta[variableName];
|
|
984
|
+
if (!entry) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
return [...entry.roles].sort().join("|");
|
|
988
|
+
}).filter((value) => value !== null);
|
|
989
|
+
return {
|
|
990
|
+
ok: false,
|
|
991
|
+
message: `Incompatible ICU variable "${variableName}" across locales (roles: ${rolesByLocale.join(" vs ")})`
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
merged[variableName] = mergedType;
|
|
995
|
+
}
|
|
996
|
+
return { ok: true, merged };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/project-locales.ts
|
|
1000
|
+
function classifyAreaLocales(areaLocales, localeFallback) {
|
|
1001
|
+
const areaSet = new Set(areaLocales);
|
|
1002
|
+
const full = [];
|
|
1003
|
+
const preserve = [];
|
|
1004
|
+
for (const locale of areaLocales) {
|
|
1005
|
+
const fallback = localeFallback?.[locale];
|
|
1006
|
+
if (fallback !== null && fallback !== void 0 && areaSet.has(fallback)) {
|
|
1007
|
+
preserve.push(locale);
|
|
1008
|
+
} else {
|
|
1009
|
+
full.push(locale);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return { full, preserve };
|
|
1013
|
+
}
|
|
1014
|
+
function projectKeyDictionary(dictionary, locales, localeFallback) {
|
|
1015
|
+
const targetLocales = [...new Set(locales)];
|
|
1016
|
+
const result = {};
|
|
1017
|
+
for (const [key, localesByKey] of Object.entries(dictionary)) {
|
|
1018
|
+
const projected = {};
|
|
1019
|
+
for (const locale of targetLocales) {
|
|
1020
|
+
const resolved = resolveLocaleTemplate(localesByKey, locale, localeFallback);
|
|
1021
|
+
if (resolved !== void 0) {
|
|
1022
|
+
projected[locale] = resolved.template;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (Object.keys(projected).length > 0) {
|
|
1026
|
+
result[key] = projected;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
function projectNamespaceLocalesCore(dictionary, locales, localeFallback) {
|
|
1032
|
+
return projectKeyDictionary(dictionary, locales, localeFallback);
|
|
1033
|
+
}
|
|
1034
|
+
function projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, localeFallback) {
|
|
1035
|
+
const { full, preserve } = classifyAreaLocales(areaLocales, localeFallback);
|
|
1036
|
+
const fullProjection = full.length > 0 ? projectKeyDictionary(dictionary, full, localeFallback) : void 0;
|
|
1037
|
+
const result = {};
|
|
1038
|
+
for (const [key, localesByKey] of Object.entries(dictionary)) {
|
|
1039
|
+
const projected = {};
|
|
1040
|
+
if (fullProjection !== void 0) {
|
|
1041
|
+
const fullEntry = fullProjection[key];
|
|
1042
|
+
if (fullEntry !== void 0) {
|
|
1043
|
+
Object.assign(projected, fullEntry);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
for (const locale of preserve) {
|
|
1047
|
+
const template = localesByKey[locale];
|
|
1048
|
+
if (template !== void 0) {
|
|
1049
|
+
projected[locale] = template;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (Object.keys(projected).length > 0) {
|
|
1053
|
+
result[key] = projected;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
1058
|
+
function writeFileIfChanged(absolutePath, content) {
|
|
1059
|
+
if (fs3.existsSync(absolutePath)) {
|
|
1060
|
+
const currentContent = fs3.readFileSync(absolutePath, "utf8");
|
|
1061
|
+
if (currentContent === content) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
fs3.mkdirSync(path8.dirname(absolutePath), { recursive: true });
|
|
1066
|
+
fs3.writeFileSync(absolutePath, content);
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/codegen/read-dictionary.ts
|
|
1071
|
+
function getDictionaryFormat(filePath) {
|
|
1072
|
+
const extension = path8.extname(filePath).toLowerCase();
|
|
1073
|
+
if (extension === ".json") {
|
|
1074
|
+
return "json";
|
|
1075
|
+
}
|
|
1076
|
+
if (extension === ".yaml" || extension === ".yml") {
|
|
1077
|
+
return "yaml";
|
|
1078
|
+
}
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
function resolveSplitJsonPath(sourcePath, locale, generatedDirRelative) {
|
|
1082
|
+
const baseName = path8.basename(sourcePath, path8.extname(sourcePath));
|
|
1083
|
+
return path8.join(generatedDirRelative, "translations", `${baseName}.${locale}.json`).replace(/\\/g, "/");
|
|
1084
|
+
}
|
|
1085
|
+
function resolveAreaJsonPath(sourcePath, area, generatedDirRelative) {
|
|
1086
|
+
const baseName = path8.basename(sourcePath, path8.extname(sourcePath));
|
|
1087
|
+
return path8.join(generatedDirRelative, "translations", `${baseName}.${area}.json`).replace(/\\/g, "/");
|
|
1088
|
+
}
|
|
1089
|
+
function splitDictionaryByLocale(dictionary, locales, localeFallback) {
|
|
1090
|
+
const byLocale = {};
|
|
1091
|
+
for (const locale of locales) {
|
|
1092
|
+
byLocale[locale] = projectNamespaceLocalesCore(dictionary, [locale], localeFallback);
|
|
1093
|
+
}
|
|
1094
|
+
return byLocale;
|
|
1095
|
+
}
|
|
1096
|
+
function splitDictionaryByDeliveryArea(dictionary, deliveryArtifacts, localeFallback) {
|
|
1097
|
+
const byArea = {};
|
|
1098
|
+
for (const [area, areaLocales] of Object.entries(deliveryArtifacts)) {
|
|
1099
|
+
byArea[area] = projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, localeFallback);
|
|
1100
|
+
}
|
|
1101
|
+
return byArea;
|
|
1102
|
+
}
|
|
1103
|
+
function assertDictionaryShape(value, context) {
|
|
1104
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1105
|
+
throw new Error(`[Codegen Error] ${context} must be a plain object.`);
|
|
1106
|
+
}
|
|
1107
|
+
for (const [key, localesByKey] of Object.entries(value)) {
|
|
1108
|
+
if (!IDENTIFIER_NAME_PATTERN.test(key)) {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`[Codegen Error] ${context}: invalid key "${key}" (${IDENTIFIER_NAME_REQUIREMENT}).`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (localesByKey === null || typeof localesByKey !== "object" || Array.isArray(localesByKey)) {
|
|
1114
|
+
throw new Error(`[Codegen Error] ${context}: key "${key}" must map locales to strings.`);
|
|
1115
|
+
}
|
|
1116
|
+
for (const [locale, template] of Object.entries(localesByKey)) {
|
|
1117
|
+
if (typeof template !== "string") {
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
`[Codegen Error] ${context}: key "${key}", locale "${locale}" must be a string.`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function readDictionaryFile(absolutePath) {
|
|
1126
|
+
const format = getDictionaryFormat(absolutePath);
|
|
1127
|
+
if (!format) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
`[Codegen Error] Unsupported dictionary format "${path8.extname(absolutePath)}" for ${absolutePath}. Use .json, .yaml, or .yml.`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const source = fs3.readFileSync(absolutePath, "utf8");
|
|
1133
|
+
let parsed;
|
|
1134
|
+
try {
|
|
1135
|
+
parsed = format === "json" ? JSON.parse(source) : parse$1(source);
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1138
|
+
throw new Error(`[Codegen Error] Failed to parse dictionary ${absolutePath}: ${message}`);
|
|
1139
|
+
}
|
|
1140
|
+
assertDictionaryShape(parsed, `Dictionary file ${absolutePath}`);
|
|
1141
|
+
return parsed;
|
|
1142
|
+
}
|
|
1143
|
+
function writeCompiledJson(absoluteJsonPath, dictionary) {
|
|
1144
|
+
return writeFileIfChanged(absoluteJsonPath, `${JSON.stringify(dictionary, null, 2)}
|
|
189
1145
|
`);
|
|
190
1146
|
}
|
|
1147
|
+
function prepareDictionaryEntries(projectRoot, entries, generatedDirRelative, options) {
|
|
1148
|
+
const delivery = options.delivery;
|
|
1149
|
+
const { dictionariesByNamespace, localeFallback, requestLocales, deliveryArtifacts } = options;
|
|
1150
|
+
const resolvedEntries = [];
|
|
1151
|
+
const splitPathsByNamespace = {};
|
|
1152
|
+
const compiledFiles = [];
|
|
1153
|
+
if (delivery === "split-by-locale" && (!requestLocales || requestLocales.length === 0)) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
"[Codegen Error] split-by-locale delivery requires at least one request locale."
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (delivery === "custom" && (!deliveryArtifacts || Object.keys(deliveryArtifacts).length === 0)) {
|
|
1159
|
+
throw new Error("[Codegen Error] custom delivery requires deliveryArtifacts.");
|
|
1160
|
+
}
|
|
1161
|
+
for (const entry of entries) {
|
|
1162
|
+
const sourceAbsolutePath = path8.resolve(projectRoot, entry.filePath);
|
|
1163
|
+
const format = getDictionaryFormat(entry.filePath);
|
|
1164
|
+
if (!format) {
|
|
1165
|
+
throw new Error(
|
|
1166
|
+
`[Codegen Error] Namespace "${entry.namespace}" uses unsupported dictionary extension in "${entry.filePath}". Use .json, .yaml, or .yml.`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
const dictionary = dictionariesByNamespace[entry.namespace];
|
|
1170
|
+
if (!dictionary) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (delivery === "split-by-locale") {
|
|
1176
|
+
const splitPaths2 = {};
|
|
1177
|
+
const dictionariesByLocale = splitDictionaryByLocale(
|
|
1178
|
+
dictionary,
|
|
1179
|
+
requestLocales,
|
|
1180
|
+
localeFallback
|
|
1181
|
+
);
|
|
1182
|
+
for (const locale of requestLocales) {
|
|
1183
|
+
const splitRelativePath = resolveSplitJsonPath(
|
|
1184
|
+
entry.filePath,
|
|
1185
|
+
locale,
|
|
1186
|
+
generatedDirRelative
|
|
1187
|
+
);
|
|
1188
|
+
const splitAbsolutePath = path8.resolve(projectRoot, splitRelativePath);
|
|
1189
|
+
const wroteFile = writeCompiledJson(splitAbsolutePath, dictionariesByLocale[locale]);
|
|
1190
|
+
if (wroteFile) {
|
|
1191
|
+
compiledFiles.push(
|
|
1192
|
+
`${path8.relative(projectRoot, sourceAbsolutePath)} \u2192 ${splitRelativePath}`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
splitPaths2[locale] = splitRelativePath;
|
|
1196
|
+
}
|
|
1197
|
+
splitPathsByNamespace[entry.namespace] = splitPaths2;
|
|
1198
|
+
resolvedEntries.push(entry);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const splitPaths = {};
|
|
1202
|
+
const dictionariesByArea = splitDictionaryByDeliveryArea(
|
|
1203
|
+
dictionary,
|
|
1204
|
+
deliveryArtifacts,
|
|
1205
|
+
localeFallback
|
|
1206
|
+
);
|
|
1207
|
+
for (const area of Object.keys(deliveryArtifacts).sort()) {
|
|
1208
|
+
const areaRelativePath = resolveAreaJsonPath(entry.filePath, area, generatedDirRelative);
|
|
1209
|
+
const areaAbsolutePath = path8.resolve(projectRoot, areaRelativePath);
|
|
1210
|
+
const wroteFile = writeCompiledJson(areaAbsolutePath, dictionariesByArea[area]);
|
|
1211
|
+
if (wroteFile) {
|
|
1212
|
+
compiledFiles.push(
|
|
1213
|
+
`${path8.relative(projectRoot, sourceAbsolutePath)} \u2192 ${areaRelativePath}`
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
splitPaths[area] = areaRelativePath;
|
|
1217
|
+
}
|
|
1218
|
+
splitPathsByNamespace[entry.namespace] = splitPaths;
|
|
1219
|
+
resolvedEntries.push(entry);
|
|
1220
|
+
}
|
|
1221
|
+
return { resolvedEntries, splitPathsByNamespace, compiledFiles };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/codegen/icu-analysis.ts
|
|
1225
|
+
function paramsTypeForVariables(variables) {
|
|
1226
|
+
const keys = Object.keys(variables);
|
|
1227
|
+
if (keys.length === 0) {
|
|
1228
|
+
return "never";
|
|
1229
|
+
}
|
|
1230
|
+
const props = keys.map((key) => {
|
|
1231
|
+
const type = variables[key] === "date" ? "Date | number" : variables[key];
|
|
1232
|
+
return `${key}: ${type}`;
|
|
1233
|
+
});
|
|
1234
|
+
return `{ ${props.join("; ")} }`;
|
|
1235
|
+
}
|
|
1236
|
+
function analyzeDictionaries(projectRoot, entries) {
|
|
1237
|
+
const paramsByNamespace = {};
|
|
1238
|
+
const argsSpecByNamespace = {};
|
|
1239
|
+
const locales = /* @__PURE__ */ new Set();
|
|
1240
|
+
const dictionariesByNamespace = {};
|
|
1241
|
+
let hasErrors = false;
|
|
1242
|
+
for (const entry of entries) {
|
|
1243
|
+
const absolutePath = path8.resolve(projectRoot, entry.filePath);
|
|
1244
|
+
if (!fs3.existsSync(absolutePath)) {
|
|
1245
|
+
console.error(
|
|
1246
|
+
`[Codegen Error] Dictionary file not found for namespace "${entry.namespace}": ${absolutePath}`
|
|
1247
|
+
);
|
|
1248
|
+
hasErrors = true;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
const dictionary = readDictionaryFile(absolutePath);
|
|
1252
|
+
dictionariesByNamespace[entry.namespace] = dictionary;
|
|
1253
|
+
paramsByNamespace[entry.namespace] = {};
|
|
1254
|
+
argsSpecByNamespace[entry.namespace] = {};
|
|
1255
|
+
for (const [key, localesByKey] of Object.entries(dictionary)) {
|
|
1256
|
+
const localeMetas = [];
|
|
1257
|
+
for (const locale of Object.keys(localesByKey)) {
|
|
1258
|
+
locales.add(locale);
|
|
1259
|
+
}
|
|
1260
|
+
for (const [locale, template] of Object.entries(localesByKey)) {
|
|
1261
|
+
try {
|
|
1262
|
+
const ast = parse(template);
|
|
1263
|
+
localeMetas.push(extractVariableMeta(ast));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
hasErrors = true;
|
|
1266
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1267
|
+
console.error(
|
|
1268
|
+
`[Codegen Error] ICU syntax error \u2014 namespace "${entry.namespace}", key "${key}", locale "${locale}": ${message}`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (localeMetas.length === 0) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const mergedVariables = mergeVariableMetaAcrossLocales(localeMetas);
|
|
1276
|
+
if (!mergedVariables.ok) {
|
|
1277
|
+
hasErrors = true;
|
|
1278
|
+
console.error(
|
|
1279
|
+
`[Codegen Error] ${mergedVariables.message} \u2014 namespace "${entry.namespace}", key "${key}"`
|
|
1280
|
+
);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const variables = mergedVariables.merged;
|
|
1284
|
+
paramsByNamespace[entry.namespace][key] = paramsTypeForVariables(variables);
|
|
1285
|
+
argsSpecByNamespace[entry.namespace][key] = variables;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (hasErrors) {
|
|
1289
|
+
return { ok: false };
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
analysis: { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace }
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/codegen/locale-policy.ts
|
|
1298
|
+
function buildRequiredLocales(dictionaryLocales, configFallback) {
|
|
1299
|
+
return [...collectRequestLocales(dictionaryLocales, configFallback)].sort();
|
|
1300
|
+
}
|
|
1301
|
+
function enrichLocaleFallback(dictionaryLocales, configFallback) {
|
|
1302
|
+
const enriched = { ...configFallback };
|
|
1303
|
+
for (const locale of buildRequiredLocales(dictionaryLocales, configFallback)) {
|
|
1304
|
+
if (!(locale in enriched)) {
|
|
1305
|
+
enriched[locale] = null;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
const sortedEntries = Object.entries(enriched).sort(
|
|
1309
|
+
([left], [right]) => left.localeCompare(right)
|
|
1310
|
+
);
|
|
1311
|
+
return Object.fromEntries(sortedEntries);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/codegen/run-codegen.ts
|
|
1315
|
+
function resolveRunCodegenInput(input) {
|
|
1316
|
+
if (typeof input === "string" || input === void 0) {
|
|
1317
|
+
const configPath2 = path8.resolve(
|
|
1318
|
+
process.cwd(),
|
|
1319
|
+
typeof input === "string" ? input : "i18n/i18n.codegen.json"
|
|
1320
|
+
);
|
|
1321
|
+
if (!fs3.existsSync(configPath2)) {
|
|
1322
|
+
throw new Error(`[Codegen Error] Config file not found: ${configPath2}`);
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
projectRoot: path8.dirname(configPath2),
|
|
1326
|
+
config: loadConfig(configPath2),
|
|
1327
|
+
log: true
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const log = input.log !== false;
|
|
1331
|
+
if (input.config !== void 0) {
|
|
1332
|
+
const projectRoot = path8.resolve(input.projectRoot ?? process.cwd());
|
|
1333
|
+
return { projectRoot, config: input.config, log };
|
|
1334
|
+
}
|
|
1335
|
+
const configPath = path8.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
|
|
1336
|
+
if (!fs3.existsSync(configPath)) {
|
|
1337
|
+
throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
projectRoot: path8.dirname(configPath),
|
|
1341
|
+
config: loadConfig(configPath),
|
|
1342
|
+
log
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function runCodegen(input) {
|
|
1346
|
+
const { projectRoot, config, log } = resolveRunCodegenInput(input);
|
|
1347
|
+
const sourceEntries = resolveNamespaces(config);
|
|
1348
|
+
const artifactsPathRelative = resolveArtifactsPath(config);
|
|
1349
|
+
const delivery = config.delivery ?? "split-by-locale";
|
|
1350
|
+
const loaderStrategy = config.loaderStrategy ?? "import";
|
|
1351
|
+
const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
|
|
1352
|
+
if (!analysisResult.ok) {
|
|
1353
|
+
throw new Error("[Codegen Error] Dictionary ICU analysis failed. See messages above.");
|
|
1354
|
+
}
|
|
1355
|
+
const { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace } = analysisResult.analysis;
|
|
1356
|
+
if (config.localeFallback) {
|
|
1357
|
+
const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
|
|
1358
|
+
if (localeFallbackIssues.length > 0) {
|
|
1359
|
+
reportCodegenIssues(localeFallbackIssues);
|
|
1360
|
+
throw new Error("[Codegen Error] Invalid localeFallback configuration.");
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const paths = resolveCodegenPaths(config);
|
|
1364
|
+
const {
|
|
1365
|
+
paramsTypeName,
|
|
1366
|
+
schemaTypeName,
|
|
1367
|
+
localeTypeName,
|
|
1368
|
+
localeFallbackConstName,
|
|
1369
|
+
factoryName,
|
|
1370
|
+
deliveryAreaTypeName: resolvedDeliveryAreaTypeName
|
|
1371
|
+
} = paths;
|
|
1372
|
+
const localeFallbackTypeName = `${localeTypeName}Fallback`;
|
|
1373
|
+
const importExtension = DEFAULT_IMPORT_EXTENSION;
|
|
1374
|
+
const requestLocales = collectRequestLocales(locales, config.localeFallback);
|
|
1375
|
+
const requestLocalesList = [...requestLocales].sort();
|
|
1376
|
+
if (delivery === "custom" && config.deliveryArtifacts) {
|
|
1377
|
+
const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
|
|
1378
|
+
config.deliveryArtifacts,
|
|
1379
|
+
requestLocales
|
|
1380
|
+
);
|
|
1381
|
+
if (deliveryArtifactsIssues.length > 0) {
|
|
1382
|
+
reportCodegenIssues(deliveryArtifactsIssues);
|
|
1383
|
+
throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const {
|
|
1387
|
+
resolvedEntries: entries,
|
|
1388
|
+
splitPathsByNamespace,
|
|
1389
|
+
compiledFiles
|
|
1390
|
+
} = prepareDictionaryEntries(projectRoot, sourceEntries, artifactsPathRelative, {
|
|
1391
|
+
dictionariesByNamespace,
|
|
1392
|
+
delivery,
|
|
1393
|
+
localeFallback: config.localeFallback,
|
|
1394
|
+
requestLocales: delivery === "split-by-locale" ? requestLocalesList : void 0,
|
|
1395
|
+
deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : void 0
|
|
1396
|
+
});
|
|
1397
|
+
const typesOutputPath = path8.resolve(projectRoot, paths.typesOutput);
|
|
1398
|
+
const instanceOutputPath = path8.resolve(projectRoot, paths.instanceOutput);
|
|
1399
|
+
const namespaceLoadersOutputPath = path8.resolve(projectRoot, paths.namespaceLoadersOutput);
|
|
1400
|
+
const dictionarySchemaOutputPath = path8.resolve(projectRoot, paths.dictionarySchemaOutput);
|
|
1401
|
+
const typesModule = toModuleBasename(typesOutputPath);
|
|
1402
|
+
const deliveryAreaNames = delivery === "custom" && config.deliveryArtifacts ? Object.keys(config.deliveryArtifacts).sort() : void 0;
|
|
1403
|
+
const deliveryAreaTypeName = delivery === "custom" ? resolvedDeliveryAreaTypeName : void 0;
|
|
1404
|
+
const localeFallbackForEmit = config.localeFallback ? enrichLocaleFallback(locales, config.localeFallback) : void 0;
|
|
1405
|
+
const typesContent = formatTypesFile({
|
|
1406
|
+
entries,
|
|
1407
|
+
paramsTypeName,
|
|
1408
|
+
schemaTypeName,
|
|
1409
|
+
localeTypeName,
|
|
1410
|
+
localeFallbackConstName,
|
|
1411
|
+
localeFallbackTypeName,
|
|
1412
|
+
localeFallback: localeFallbackForEmit,
|
|
1413
|
+
paramsByNamespace,
|
|
1414
|
+
requestLocales: requestLocalesList,
|
|
1415
|
+
...deliveryAreaTypeName && deliveryAreaNames ? {
|
|
1416
|
+
deliveryAreaTypeName,
|
|
1417
|
+
deliveryAreaNames,
|
|
1418
|
+
...delivery === "custom" && config.deliveryArtifacts ? { deliveryArtifacts: config.deliveryArtifacts } : {}
|
|
1419
|
+
} : {},
|
|
1420
|
+
lazyEntries: entries
|
|
1421
|
+
});
|
|
1422
|
+
const instanceContent = formatInstanceFile({
|
|
1423
|
+
typesOutputPath,
|
|
1424
|
+
namespaceLoadersOutputPath,
|
|
1425
|
+
paramsTypeName,
|
|
1426
|
+
schemaTypeName,
|
|
1427
|
+
localeTypeName,
|
|
1428
|
+
localeFallbackConstName,
|
|
1429
|
+
factoryName,
|
|
1430
|
+
hasLocaleFallback: Boolean(config.localeFallback),
|
|
1431
|
+
hasLocaleType: requestLocalesList.length > 0,
|
|
1432
|
+
importExtension,
|
|
1433
|
+
delivery,
|
|
1434
|
+
loaderStrategy,
|
|
1435
|
+
...delivery === "custom" ? { localeDeliveryAreaConstName: "LOCALE_DELIVERY_AREA" } : {}
|
|
1436
|
+
});
|
|
1437
|
+
writeFileIfChanged(typesOutputPath, typesContent);
|
|
1438
|
+
const generatedFiles = [
|
|
1439
|
+
path8.relative(projectRoot, typesOutputPath),
|
|
1440
|
+
path8.relative(projectRoot, instanceOutputPath)
|
|
1441
|
+
];
|
|
1442
|
+
writeFileIfChanged(instanceOutputPath, instanceContent);
|
|
1443
|
+
const dictionarySpecBlock = formatDictionarySpecBlock(entries, argsSpecByNamespace);
|
|
1444
|
+
const dictionarySchemaContent = formatDictionarySchemaFile(
|
|
1445
|
+
schemaTypeName,
|
|
1446
|
+
typesModule,
|
|
1447
|
+
dictionarySpecBlock,
|
|
1448
|
+
importExtension
|
|
1449
|
+
);
|
|
1450
|
+
writeFileIfChanged(dictionarySchemaOutputPath, dictionarySchemaContent);
|
|
1451
|
+
generatedFiles.push(path8.relative(projectRoot, dictionarySchemaOutputPath));
|
|
1452
|
+
const lazyEntriesWithPaths = entries.map((entry) => ({
|
|
1453
|
+
...entry,
|
|
1454
|
+
absolutePath: path8.resolve(projectRoot, entry.filePath)
|
|
1455
|
+
}));
|
|
1456
|
+
const namespaceLoadersContent = formatNamespaceLoadersFile({
|
|
1457
|
+
loadersOutputPath: namespaceLoadersOutputPath,
|
|
1458
|
+
lazyEntries: lazyEntriesWithPaths,
|
|
1459
|
+
schemaTypeName,
|
|
1460
|
+
localeTypeName,
|
|
1461
|
+
typesModule,
|
|
1462
|
+
importExtension,
|
|
1463
|
+
projectRoot,
|
|
1464
|
+
delivery,
|
|
1465
|
+
splitPathsByNamespace,
|
|
1466
|
+
loaderStrategy,
|
|
1467
|
+
...delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {},
|
|
1468
|
+
...delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames ? { deliveryAreaTypeName, deliveryAreaNames } : {}
|
|
1469
|
+
});
|
|
1470
|
+
writeFileIfChanged(namespaceLoadersOutputPath, namespaceLoadersContent);
|
|
1471
|
+
generatedFiles.push(path8.relative(projectRoot, namespaceLoadersOutputPath));
|
|
1472
|
+
if (log) {
|
|
1473
|
+
console.log(`\u2705 Generated: ${generatedFiles.join(", ")}`);
|
|
1474
|
+
if (compiledFiles.length > 0) {
|
|
1475
|
+
console.log(`\u2705 Compiled: ${compiledFiles.join(", ")}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return { projectRoot, generatedFiles, compiledFiles };
|
|
1479
|
+
}
|
|
1480
|
+
function buildDictionarySpecFromAnalysis(entries, argsSpecByNamespace) {
|
|
1481
|
+
const requiredKeys = {};
|
|
1482
|
+
const argsByKey = {};
|
|
1483
|
+
for (const entry of entries) {
|
|
1484
|
+
const argsByKeyForNs = argsSpecByNamespace[entry.namespace] ?? {};
|
|
1485
|
+
requiredKeys[entry.namespace] = Object.keys(argsByKeyForNs).sort();
|
|
1486
|
+
argsByKey[entry.namespace] = argsByKeyForNs;
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
mode: "multi",
|
|
1490
|
+
requiredKeys,
|
|
1491
|
+
argsByKey
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function loadDictionarySpecFromSchemaFile(absoluteSchemaPath) {
|
|
1495
|
+
if (!fs3.existsSync(absoluteSchemaPath)) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`[Codegen Error] Missing dictionary schema at ${absoluteSchemaPath}. Run runCodegen first.`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
const source = fs3.readFileSync(absoluteSchemaPath, "utf8");
|
|
1501
|
+
const match = source.match(
|
|
1502
|
+
/export const DICTIONARY_SPEC = (\{[\s\S]*?\n\}) satisfies DictionarySpec;/
|
|
1503
|
+
);
|
|
1504
|
+
if (!match?.[1]) {
|
|
1505
|
+
throw new Error(
|
|
1506
|
+
`[Codegen Error] Could not parse DICTIONARY_SPEC from ${absoluteSchemaPath}. Re-run runCodegen.`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
const cleaned = match[1].replace(/ as const/g, "");
|
|
1510
|
+
let parsed;
|
|
1511
|
+
try {
|
|
1512
|
+
parsed = new Function(`"use strict"; return (${cleaned});`)();
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1515
|
+
throw new Error(
|
|
1516
|
+
`[Codegen Error] Failed to evaluate DICTIONARY_SPEC from ${absoluteSchemaPath}: ${message}`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
if (!parsed || typeof parsed !== "object" || parsed.mode !== "multi" || !("requiredKeys" in parsed) || !("argsByKey" in parsed)) {
|
|
1520
|
+
throw new Error(
|
|
1521
|
+
`[Codegen Error] DICTIONARY_SPEC in ${absoluteSchemaPath} is not a multi-mode DictionarySpec.`
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return parsed;
|
|
1525
|
+
}
|
|
1526
|
+
function sortedKeys(keys) {
|
|
1527
|
+
return [...keys].sort();
|
|
1528
|
+
}
|
|
1529
|
+
function variableSpecsEqual(a, b) {
|
|
1530
|
+
const aKeys = Object.keys(a).sort();
|
|
1531
|
+
const bKeys = Object.keys(b).sort();
|
|
1532
|
+
if (aKeys.length !== bKeys.length) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
1536
|
+
const key = aKeys[i];
|
|
1537
|
+
if (key !== bKeys[i] || a[key] !== b[key]) {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return true;
|
|
1542
|
+
}
|
|
1543
|
+
function namespaceArgsEqual(a, b) {
|
|
1544
|
+
const aKeys = Object.keys(a ?? {}).sort();
|
|
1545
|
+
const bKeys = Object.keys(b ?? {}).sort();
|
|
1546
|
+
if (aKeys.length !== bKeys.length) {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
for (const key of aKeys) {
|
|
1550
|
+
if (!bKeys.includes(key)) {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
if (!variableSpecsEqual(a[key], b[key])) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
function namespaceContractsMatch(namespaces, current, established) {
|
|
1560
|
+
for (const namespace of namespaces) {
|
|
1561
|
+
const currentKeys = sortedKeys(current.requiredKeys[namespace] ?? []);
|
|
1562
|
+
const establishedKeys = sortedKeys(established.requiredKeys[namespace] ?? []);
|
|
1563
|
+
if (currentKeys.length !== establishedKeys.length || currentKeys.some((key, index) => key !== establishedKeys[index])) {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
if (!namespaceArgsEqual(current.argsByKey[namespace], established.argsByKey[namespace])) {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/codegen/regenerate-namespaces.ts
|
|
1574
|
+
function resolveRegenerateInput(input) {
|
|
1575
|
+
const log = input.log !== false;
|
|
1576
|
+
const namespaces = input.namespaces;
|
|
1577
|
+
if (input.config !== void 0) {
|
|
1578
|
+
return {
|
|
1579
|
+
projectRoot: path8.resolve(input.projectRoot ?? process.cwd()),
|
|
1580
|
+
config: input.config,
|
|
1581
|
+
namespaces,
|
|
1582
|
+
log
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
const configPath = path8.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
|
|
1586
|
+
if (!fs3.existsSync(configPath)) {
|
|
1587
|
+
throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
projectRoot: path8.dirname(configPath),
|
|
1591
|
+
config: loadConfig(configPath),
|
|
1592
|
+
namespaces,
|
|
1593
|
+
log
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function regenerateNamespaces(input) {
|
|
1597
|
+
const { projectRoot, config, namespaces, log } = resolveRegenerateInput(input);
|
|
1598
|
+
if (namespaces.length === 0) {
|
|
1599
|
+
throw new Error("[Codegen Error] regenerateNamespaces requires a non-empty namespaces list.");
|
|
1600
|
+
}
|
|
1601
|
+
const sourceEntries = resolveNamespaces(config);
|
|
1602
|
+
const known = new Set(sourceEntries.map((entry) => entry.namespace));
|
|
1603
|
+
const unknown = namespaces.filter((namespace) => !known.has(namespace));
|
|
1604
|
+
if (unknown.length > 0) {
|
|
1605
|
+
throw new Error(
|
|
1606
|
+
`[Codegen Error] Unknown namespace(s) for regenerateNamespaces: ${unknown.join(", ")}.`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
const requested = new Set(namespaces);
|
|
1610
|
+
const selectedEntries = sourceEntries.filter((entry) => requested.has(entry.namespace));
|
|
1611
|
+
const paths = resolveCodegenPaths(config);
|
|
1612
|
+
const schemaPath = path8.resolve(projectRoot, paths.dictionarySchemaOutput);
|
|
1613
|
+
const established = loadDictionarySpecFromSchemaFile(schemaPath);
|
|
1614
|
+
const artifactsPathRelative = resolveArtifactsPath(config);
|
|
1615
|
+
const delivery = config.delivery ?? "split-by-locale";
|
|
1616
|
+
const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
|
|
1617
|
+
if (!analysisResult.ok) {
|
|
1618
|
+
throw new Error(
|
|
1619
|
+
"[Codegen Error] Dictionary ICU analysis failed while regenerating namespaces."
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
const {
|
|
1623
|
+
argsSpecByNamespace,
|
|
1624
|
+
locales,
|
|
1625
|
+
dictionariesByNamespace: sourceDictionaries
|
|
1626
|
+
} = analysisResult.analysis;
|
|
1627
|
+
const current = buildDictionarySpecFromAnalysis(selectedEntries, argsSpecByNamespace);
|
|
1628
|
+
if (!namespaceContractsMatch(namespaces, current, established)) {
|
|
1629
|
+
throw new Error(
|
|
1630
|
+
"[Codegen Error] Namespace ICU contract changed (keys or params). Contract change requires runCodegen."
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
if (config.localeFallback) {
|
|
1634
|
+
const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
|
|
1635
|
+
if (localeFallbackIssues.length > 0) {
|
|
1636
|
+
reportCodegenIssues(localeFallbackIssues);
|
|
1637
|
+
throw new Error("[Codegen Error] Invalid localeFallback configuration.");
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const requestLocales = collectRequestLocales(locales, config.localeFallback);
|
|
1641
|
+
const requestLocalesList = [...requestLocales].sort();
|
|
1642
|
+
if (delivery === "custom" && config.deliveryArtifacts) {
|
|
1643
|
+
const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
|
|
1644
|
+
config.deliveryArtifacts,
|
|
1645
|
+
requestLocales
|
|
1646
|
+
);
|
|
1647
|
+
if (deliveryArtifactsIssues.length > 0) {
|
|
1648
|
+
reportCodegenIssues(deliveryArtifactsIssues);
|
|
1649
|
+
throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const dictionariesByNamespace = {};
|
|
1653
|
+
for (const entry of selectedEntries) {
|
|
1654
|
+
const dictionary = sourceDictionaries[entry.namespace];
|
|
1655
|
+
if (!dictionary) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
dictionariesByNamespace[entry.namespace] = dictionary;
|
|
1661
|
+
}
|
|
1662
|
+
const { splitPathsByNamespace, compiledFiles } = prepareDictionaryEntries(
|
|
1663
|
+
projectRoot,
|
|
1664
|
+
selectedEntries,
|
|
1665
|
+
artifactsPathRelative,
|
|
1666
|
+
{
|
|
1667
|
+
dictionariesByNamespace,
|
|
1668
|
+
delivery,
|
|
1669
|
+
localeFallback: config.localeFallback,
|
|
1670
|
+
requestLocales: delivery === "split-by-locale" ? requestLocalesList : void 0,
|
|
1671
|
+
deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : void 0
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
if (log) {
|
|
1675
|
+
if (compiledFiles.length > 0) {
|
|
1676
|
+
console.log(`\u2705 Regenerated delivery artifacts: ${compiledFiles.join(", ")}`);
|
|
1677
|
+
} else {
|
|
1678
|
+
console.log("\u2705 Regenerated delivery artifacts: unchanged");
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return { projectRoot, compiledFiles, splitPathsByNamespace };
|
|
1682
|
+
}
|
|
191
1683
|
|
|
192
|
-
export { DELIVERY_MODES, SUPPORTED_IMPORT_EXTENSIONS, buildCodegenConfig, codegenConfigKeys, inferProjectName,
|
|
1684
|
+
export { DEFAULT_FACTORY_NAME, DEFAULT_LOCALE_FALLBACK_CONST_NAME, DELIVERY_MODES, GENERATED_BASENAMES, LOADER_STRATEGIES, SUPPORTED_IMPORT_EXTENSIONS, buildCodegenConfig, codegenConfigKeys, formatCodegenConfigIssues, inferProjectName, loadConfig, regenerateNamespaces, resolveArtifactsPath, resolveCodegenPaths, resolveNamespaces, runCodegen, typeNamesForProject, writeCodegenConfig };
|
|
193
1685
|
//# sourceMappingURL=index.js.map
|
|
194
1686
|
//# sourceMappingURL=index.js.map
|