@xndrjs/i18n 0.8.0 → 0.8.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/audit.js +739 -0
  3. package/dist/cli/audit.js.map +1 -0
  4. package/dist/cli/codegen.js +1471 -0
  5. package/dist/cli/codegen.js.map +1 -0
  6. package/dist/cli/setup.js +148 -0
  7. package/dist/cli/setup.js.map +1 -0
  8. package/dist/codegen/index.js +43 -40
  9. package/dist/codegen/index.js.map +1 -1
  10. package/dist/index.d.ts +20 -1
  11. package/dist/index.js +56 -10
  12. package/dist/index.js.map +1 -1
  13. package/package.json +6 -9
  14. package/bin/audit.mjs +0 -14
  15. package/bin/codegen.mjs +0 -17
  16. package/bin/setup.mjs +0 -14
  17. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  18. package/src/IcuTranslationProviderMulti.ts +0 -176
  19. package/src/audit/audit-dictionaries.test.ts +0 -134
  20. package/src/audit/audit-dictionaries.ts +0 -178
  21. package/src/audit/run-audit.test.ts +0 -169
  22. package/src/audit/run-audit.ts +0 -94
  23. package/src/builder-load-registry.ts +0 -39
  24. package/src/builder-loaders.ts +0 -15
  25. package/src/builder-types.test.ts +0 -49
  26. package/src/builder-types.ts +0 -24
  27. package/src/builder.test.ts +0 -260
  28. package/src/builder.ts +0 -10
  29. package/src/codegen/codegen-config-schema.ts +0 -156
  30. package/src/codegen/config.test.ts +0 -231
  31. package/src/codegen/config.ts +0 -42
  32. package/src/codegen/constants.ts +0 -9
  33. package/src/codegen/delivery-artifacts.test.ts +0 -167
  34. package/src/codegen/delivery-artifacts.ts +0 -154
  35. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  36. package/src/codegen/dictionary-spec-contract.ts +0 -138
  37. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  38. package/src/codegen/emit/instance-file.test.ts +0 -71
  39. package/src/codegen/emit/instance-file.ts +0 -257
  40. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  41. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  42. package/src/codegen/emit/types-file.test.ts +0 -96
  43. package/src/codegen/emit/types-file.ts +0 -142
  44. package/src/codegen/fixtures/billing-namespace.json +0 -6
  45. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  46. package/src/codegen/generate-i18n-types.ts +0 -20
  47. package/src/codegen/icu-analysis.ts +0 -110
  48. package/src/codegen/locale-fallback.ts +0 -73
  49. package/src/codegen/locale-policy.test.ts +0 -35
  50. package/src/codegen/locale-policy.ts +0 -30
  51. package/src/codegen/paths.ts +0 -44
  52. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  53. package/src/codegen/read-dictionary.test.ts +0 -594
  54. package/src/codegen/read-dictionary.ts +0 -260
  55. package/src/codegen/regenerate-namespaces.ts +0 -180
  56. package/src/codegen/run-codegen.test.ts +0 -230
  57. package/src/codegen/run-codegen.ts +0 -252
  58. package/src/codegen/types.ts +0 -10
  59. package/src/codegen/write-file-if-changed.test.ts +0 -42
  60. package/src/codegen/write-file-if-changed.ts +0 -20
  61. package/src/codegen-config/build-config.ts +0 -15
  62. package/src/codegen-config/codegen-config.test.ts +0 -36
  63. package/src/codegen-config/index.ts +0 -37
  64. package/src/codegen-config/type-names.ts +0 -20
  65. package/src/codegen-config/write-config.ts +0 -8
  66. package/src/engine.ts +0 -47
  67. package/src/fetch-artifact.ts +0 -16
  68. package/src/format-core.ts +0 -91
  69. package/src/i18n-handle.ts +0 -202
  70. package/src/icu/extract-variables.test.ts +0 -199
  71. package/src/icu/extract-variables.ts +0 -227
  72. package/src/icu/parse-template.ts +0 -31
  73. package/src/index.ts +0 -58
  74. package/src/project-locales.test.ts +0 -280
  75. package/src/project-locales.ts +0 -211
  76. package/src/resolve-locale.test.ts +0 -91
  77. package/src/resolve-locale.ts +0 -88
  78. package/src/scope-multi.ts +0 -105
  79. package/src/scope-types.ts +0 -35
  80. package/src/scope.test.ts +0 -172
  81. package/src/serialized-state.test.ts +0 -114
  82. package/src/serialized-state.ts +0 -28
  83. package/src/setup/setup-i18n.test.ts +0 -85
  84. package/src/setup/setup-i18n.ts +0 -156
  85. package/src/types.ts +0 -65
  86. package/src/validation/create-args-schema.ts +0 -76
  87. package/src/validation/create-normalized-schema.ts +0 -52
  88. package/src/validation/errors.ts +0 -43
  89. package/src/validation/index.ts +0 -202
  90. package/src/validation/normalize.ts +0 -299
  91. package/src/validation/to-dictionary.ts +0 -67
  92. package/src/validation/types.ts +0 -82
  93. package/src/validation/validate-normalized.ts +0 -133
  94. package/src/validation/validation.test.ts +0 -562
@@ -1,260 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parse as parseYaml } from "yaml";
4
- import { IDENTIFIER_NAME_PATTERN, IDENTIFIER_NAME_REQUIREMENT } from "./constants.js";
5
- import type { DeliveryMode } from "./codegen-config-schema.js";
6
- import type { DeliveryArtifactsMap } from "./delivery-artifacts.js";
7
- import {
8
- projectNamespaceForDeliveryAreaCore,
9
- projectNamespaceLocalesCore,
10
- } from "../project-locales.js";
11
- import type { LocaleFallbackMap } from "../types.js";
12
- import type { DictionaryJson, NamespaceEntry } from "./types.js";
13
- import { writeFileIfChanged } from "./write-file-if-changed.js";
14
-
15
- /**
16
- * Dictionary I/O for codegen and audit: read/validate source files (`readDictionaryFile`)
17
- * and write compiled or split JSON artifacts (`prepareDictionaryEntries`).
18
- */
19
- export const SUPPORTED_DICTIONARY_EXTENSIONS = [".json", ".yaml", ".yml"] as const;
20
- export type DictionaryFormat = "json" | "yaml";
21
-
22
- export function getDictionaryFormat(filePath: string): DictionaryFormat | null {
23
- const extension = path.extname(filePath).toLowerCase();
24
- if (extension === ".json") {
25
- return "json";
26
- }
27
- if (extension === ".yaml" || extension === ".yml") {
28
- return "yaml";
29
- }
30
- return null;
31
- }
32
-
33
- export function resolveCompiledJsonPath(sourcePath: string, generatedDirRelative: string): string {
34
- const baseName = `${path.basename(sourcePath, path.extname(sourcePath))}.json`;
35
- return path.join(generatedDirRelative, "translations", baseName).replace(/\\/g, "/");
36
- }
37
-
38
- export function resolveSplitJsonPath(
39
- sourcePath: string,
40
- locale: string,
41
- generatedDirRelative: string
42
- ): string {
43
- const baseName = path.basename(sourcePath, path.extname(sourcePath));
44
- return path
45
- .join(generatedDirRelative, "translations", `${baseName}.${locale}.json`)
46
- .replace(/\\/g, "/");
47
- }
48
-
49
- export function resolveAreaJsonPath(
50
- sourcePath: string,
51
- area: string,
52
- generatedDirRelative: string
53
- ): string {
54
- const baseName = path.basename(sourcePath, path.extname(sourcePath));
55
- return path
56
- .join(generatedDirRelative, "translations", `${baseName}.${area}.json`)
57
- .replace(/\\/g, "/");
58
- }
59
-
60
- /** Projects a canonical dictionary into per-locale slices for split-by-locale delivery. */
61
- export function splitDictionaryByLocale(
62
- dictionary: DictionaryJson,
63
- locales: readonly string[],
64
- localeFallback?: LocaleFallbackMap
65
- ): Record<string, DictionaryJson> {
66
- const byLocale: Record<string, DictionaryJson> = {};
67
-
68
- for (const locale of locales) {
69
- byLocale[locale] = projectNamespaceLocalesCore(dictionary, [locale], localeFallback);
70
- }
71
-
72
- return byLocale;
73
- }
74
-
75
- /** Projects a canonical dictionary into per-area slices for custom delivery. */
76
- export function splitDictionaryByDeliveryArea(
77
- dictionary: DictionaryJson,
78
- deliveryArtifacts: DeliveryArtifactsMap,
79
- localeFallback?: LocaleFallbackMap
80
- ): Record<string, DictionaryJson> {
81
- const byArea: Record<string, DictionaryJson> = {};
82
-
83
- for (const [area, areaLocales] of Object.entries(deliveryArtifacts)) {
84
- byArea[area] = projectNamespaceForDeliveryAreaCore(dictionary, areaLocales, localeFallback);
85
- }
86
-
87
- return byArea;
88
- }
89
-
90
- function assertDictionaryShape(value: unknown, context: string): asserts value is DictionaryJson {
91
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
92
- throw new Error(`[Codegen Error] ${context} must be a plain object.`);
93
- }
94
-
95
- for (const [key, localesByKey] of Object.entries(value)) {
96
- if (!IDENTIFIER_NAME_PATTERN.test(key)) {
97
- throw new Error(
98
- `[Codegen Error] ${context}: invalid key "${key}" (${IDENTIFIER_NAME_REQUIREMENT}).`
99
- );
100
- }
101
-
102
- if (localesByKey === null || typeof localesByKey !== "object" || Array.isArray(localesByKey)) {
103
- throw new Error(`[Codegen Error] ${context}: key "${key}" must map locales to strings.`);
104
- }
105
-
106
- for (const [locale, template] of Object.entries(localesByKey)) {
107
- if (typeof template !== "string") {
108
- throw new Error(
109
- `[Codegen Error] ${context}: key "${key}", locale "${locale}" must be a string.`
110
- );
111
- }
112
- }
113
- }
114
- }
115
-
116
- /** Reads and shape-validates a dictionary file from disk (JSON or YAML). */
117
- export function readDictionaryFile(absolutePath: string): DictionaryJson {
118
- const format = getDictionaryFormat(absolutePath);
119
- if (!format) {
120
- throw new Error(
121
- `[Codegen Error] Unsupported dictionary format "${path.extname(absolutePath)}" for ${absolutePath}. Use .json, .yaml, or .yml.`
122
- );
123
- }
124
-
125
- const source = fs.readFileSync(absolutePath, "utf8");
126
- let parsed: unknown;
127
-
128
- try {
129
- parsed = format === "json" ? JSON.parse(source) : parseYaml(source);
130
- } catch (error) {
131
- const message = error instanceof Error ? error.message : String(error);
132
- throw new Error(`[Codegen Error] Failed to parse dictionary ${absolutePath}: ${message}`);
133
- }
134
-
135
- assertDictionaryShape(parsed, `Dictionary file ${absolutePath}`);
136
- return parsed;
137
- }
138
-
139
- function writeCompiledJson(absoluteJsonPath: string, dictionary: DictionaryJson): boolean {
140
- return writeFileIfChanged(absoluteJsonPath, `${JSON.stringify(dictionary, null, 2)}\n`);
141
- }
142
-
143
- export interface PrepareDictionaryEntriesOptions {
144
- dictionariesByNamespace: Record<string, DictionaryJson>;
145
- delivery: DeliveryMode;
146
- localeFallback?: LocaleFallbackMap | undefined;
147
- requestLocales?: readonly string[] | undefined;
148
- deliveryArtifacts?: DeliveryArtifactsMap | undefined;
149
- }
150
-
151
- export interface PrepareDictionariesResult {
152
- resolvedEntries: NamespaceEntry[];
153
- splitPathsByNamespace: Record<string, Record<string, string>>;
154
- compiledFiles: string[];
155
- }
156
-
157
- /**
158
- * Phase 2 of codegen: materialize JSON artifacts on disk (locale/area split).
159
- * Returns `splitPathsByNamespace` consumed by namespace-loader emitters.
160
- */
161
- export function prepareDictionaryEntries(
162
- projectRoot: string,
163
- entries: NamespaceEntry[],
164
- generatedDirRelative: string,
165
- options: PrepareDictionaryEntriesOptions
166
- ): PrepareDictionariesResult {
167
- const delivery = options.delivery;
168
- const { dictionariesByNamespace, localeFallback, requestLocales, deliveryArtifacts } = options;
169
- const resolvedEntries: NamespaceEntry[] = [];
170
- const splitPathsByNamespace: Record<string, Record<string, string>> = {};
171
- const compiledFiles: string[] = [];
172
-
173
- if (delivery === "split-by-locale" && (!requestLocales || requestLocales.length === 0)) {
174
- throw new Error(
175
- "[Codegen Error] split-by-locale delivery requires at least one request locale."
176
- );
177
- }
178
-
179
- if (
180
- delivery === "custom" &&
181
- (!deliveryArtifacts || Object.keys(deliveryArtifacts).length === 0)
182
- ) {
183
- throw new Error("[Codegen Error] custom delivery requires deliveryArtifacts.");
184
- }
185
-
186
- for (const entry of entries) {
187
- const sourceAbsolutePath = path.resolve(projectRoot, entry.filePath);
188
- const format = getDictionaryFormat(entry.filePath);
189
-
190
- if (!format) {
191
- throw new Error(
192
- `[Codegen Error] Namespace "${entry.namespace}" uses unsupported dictionary extension in "${entry.filePath}". Use .json, .yaml, or .yml.`
193
- );
194
- }
195
-
196
- const dictionary = dictionariesByNamespace[entry.namespace];
197
- if (!dictionary) {
198
- throw new Error(
199
- `[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
200
- );
201
- }
202
-
203
- if (delivery === "split-by-locale") {
204
- const splitPaths: Record<string, string> = {};
205
- const dictionariesByLocale = splitDictionaryByLocale(
206
- dictionary,
207
- requestLocales!,
208
- localeFallback
209
- );
210
-
211
- for (const locale of requestLocales!) {
212
- const splitRelativePath = resolveSplitJsonPath(
213
- entry.filePath,
214
- locale,
215
- generatedDirRelative
216
- );
217
- const splitAbsolutePath = path.resolve(projectRoot, splitRelativePath);
218
- const wroteFile = writeCompiledJson(splitAbsolutePath, dictionariesByLocale[locale]!);
219
-
220
- if (wroteFile) {
221
- compiledFiles.push(
222
- `${path.relative(projectRoot, sourceAbsolutePath)} → ${splitRelativePath}`
223
- );
224
- }
225
-
226
- splitPaths[locale] = splitRelativePath;
227
- }
228
-
229
- splitPathsByNamespace[entry.namespace] = splitPaths;
230
- resolvedEntries.push(entry);
231
- continue;
232
- }
233
-
234
- const splitPaths: Record<string, string> = {};
235
- const dictionariesByArea = splitDictionaryByDeliveryArea(
236
- dictionary,
237
- deliveryArtifacts!,
238
- localeFallback
239
- );
240
-
241
- for (const area of Object.keys(deliveryArtifacts!).sort()) {
242
- const areaRelativePath = resolveAreaJsonPath(entry.filePath, area, generatedDirRelative);
243
- const areaAbsolutePath = path.resolve(projectRoot, areaRelativePath);
244
- const wroteFile = writeCompiledJson(areaAbsolutePath, dictionariesByArea[area]!);
245
-
246
- if (wroteFile) {
247
- compiledFiles.push(
248
- `${path.relative(projectRoot, sourceAbsolutePath)} → ${areaRelativePath}`
249
- );
250
- }
251
-
252
- splitPaths[area] = areaRelativePath;
253
- }
254
-
255
- splitPathsByNamespace[entry.namespace] = splitPaths;
256
- resolvedEntries.push(entry);
257
- }
258
-
259
- return { resolvedEntries, splitPathsByNamespace, compiledFiles };
260
- }
@@ -1,180 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { CodegenConfig } from "./codegen-config-schema.js";
4
- import {
5
- loadConfig,
6
- resolveArtifactsPath,
7
- resolveCodegenPaths,
8
- resolveNamespaces,
9
- } from "./config.js";
10
- import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
11
- import {
12
- buildDictionarySpecFromAnalysis,
13
- loadDictionarySpecFromSchemaFile,
14
- namespaceContractsMatch,
15
- } from "./dictionary-spec-contract.js";
16
- import { analyzeDictionaries } from "./icu-analysis.js";
17
- import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
18
- import { reportCodegenIssues } from "./paths.js";
19
- import { prepareDictionaryEntries } from "./read-dictionary.js";
20
- import type { DictionaryJson } from "./types.js";
21
-
22
- export interface RegenerateNamespacesInput {
23
- /** Namespaces whose delivery JSON should be refreshed from authoring sources. */
24
- namespaces: readonly string[];
25
- configPath?: string;
26
- config?: CodegenConfig;
27
- projectRoot?: string;
28
- /** When false, skip console.log summaries (default true). */
29
- log?: boolean;
30
- }
31
-
32
- export interface RegenerateNamespacesResult {
33
- projectRoot: string;
34
- compiledFiles: string[];
35
- splitPathsByNamespace: Record<string, Record<string, string>>;
36
- }
37
-
38
- function resolveRegenerateInput(input: RegenerateNamespacesInput): {
39
- projectRoot: string;
40
- config: CodegenConfig;
41
- namespaces: readonly string[];
42
- log: boolean;
43
- } {
44
- const log = input.log !== false;
45
- const namespaces = input.namespaces;
46
-
47
- if (input.config !== undefined) {
48
- return {
49
- projectRoot: path.resolve(input.projectRoot ?? process.cwd()),
50
- config: input.config,
51
- namespaces,
52
- log,
53
- };
54
- }
55
-
56
- const configPath = path.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
57
- if (!fs.existsSync(configPath)) {
58
- throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
59
- }
60
- return {
61
- projectRoot: path.dirname(configPath),
62
- config: loadConfig(configPath),
63
- namespaces,
64
- log,
65
- };
66
- }
67
-
68
- /**
69
- * Content-only refresh: re-materialize delivery JSON for selected namespaces from
70
- * current authoring files, without rewriting generated TypeScript.
71
- *
72
- * Must not change the ICU key/param contract established by a prior {@link runCodegen}.
73
- * If authoring changed keys or ICU args, this throws — run {@link runCodegen} and ship a release.
74
- *
75
- * Authoring updates are out of scope for this library (CMS or editors write those files).
76
- * End-to-end without app rebuild requires `loaderStrategy: "fetch"`.
77
- */
78
- export function regenerateNamespaces(input: RegenerateNamespacesInput): RegenerateNamespacesResult {
79
- const { projectRoot, config, namespaces, log } = resolveRegenerateInput(input);
80
-
81
- if (namespaces.length === 0) {
82
- throw new Error("[Codegen Error] regenerateNamespaces requires a non-empty namespaces list.");
83
- }
84
-
85
- const sourceEntries = resolveNamespaces(config);
86
- const known = new Set(sourceEntries.map((entry) => entry.namespace));
87
- const unknown = namespaces.filter((namespace) => !known.has(namespace));
88
- if (unknown.length > 0) {
89
- throw new Error(
90
- `[Codegen Error] Unknown namespace(s) for regenerateNamespaces: ${unknown.join(", ")}.`
91
- );
92
- }
93
-
94
- const requested = new Set(namespaces);
95
- const selectedEntries = sourceEntries.filter((entry) => requested.has(entry.namespace));
96
-
97
- const paths = resolveCodegenPaths(config);
98
- const schemaPath = path.resolve(projectRoot, paths.dictionarySchemaOutput);
99
- const established = loadDictionarySpecFromSchemaFile(schemaPath);
100
-
101
- const artifactsPathRelative = resolveArtifactsPath(config);
102
- const delivery = config.delivery ?? "split-by-locale";
103
-
104
- // Analyze all namespaces so locale unions / deliveryArtifacts stay consistent with full codegen.
105
- const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
106
- if (!analysisResult.ok) {
107
- throw new Error(
108
- "[Codegen Error] Dictionary ICU analysis failed while regenerating namespaces."
109
- );
110
- }
111
-
112
- const {
113
- argsSpecByNamespace,
114
- locales,
115
- dictionariesByNamespace: sourceDictionaries,
116
- } = analysisResult.analysis;
117
-
118
- const current = buildDictionarySpecFromAnalysis(selectedEntries, argsSpecByNamespace);
119
- if (!namespaceContractsMatch(namespaces, current, established)) {
120
- throw new Error(
121
- "[Codegen Error] Namespace ICU contract changed (keys or params). Contract change requires runCodegen."
122
- );
123
- }
124
-
125
- if (config.localeFallback) {
126
- const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
127
- if (localeFallbackIssues.length > 0) {
128
- reportCodegenIssues(localeFallbackIssues);
129
- throw new Error("[Codegen Error] Invalid localeFallback configuration.");
130
- }
131
- }
132
-
133
- const requestLocales = collectRequestLocales(locales, config.localeFallback);
134
- const requestLocalesList = [...requestLocales].sort();
135
-
136
- if (delivery === "custom" && config.deliveryArtifacts) {
137
- const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
138
- config.deliveryArtifacts,
139
- requestLocales
140
- );
141
- if (deliveryArtifactsIssues.length > 0) {
142
- reportCodegenIssues(deliveryArtifactsIssues);
143
- throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
144
- }
145
- }
146
-
147
- const dictionariesByNamespace: Record<string, DictionaryJson> = {};
148
- for (const entry of selectedEntries) {
149
- const dictionary = sourceDictionaries[entry.namespace];
150
- if (!dictionary) {
151
- throw new Error(
152
- `[Codegen Error] Missing parsed dictionary for namespace "${entry.namespace}".`
153
- );
154
- }
155
- dictionariesByNamespace[entry.namespace] = dictionary;
156
- }
157
-
158
- const { splitPathsByNamespace, compiledFiles } = prepareDictionaryEntries(
159
- projectRoot,
160
- selectedEntries,
161
- artifactsPathRelative,
162
- {
163
- dictionariesByNamespace,
164
- delivery,
165
- localeFallback: config.localeFallback,
166
- requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
167
- deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
168
- }
169
- );
170
-
171
- if (log) {
172
- if (compiledFiles.length > 0) {
173
- console.log(`✅ Regenerated delivery artifacts: ${compiledFiles.join(", ")}`);
174
- } else {
175
- console.log("✅ Regenerated delivery artifacts: unchanged");
176
- }
177
- }
178
-
179
- return { projectRoot, compiledFiles, splitPathsByNamespace };
180
- }
@@ -1,230 +0,0 @@
1
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
- import { regenerateNamespaces } from "./regenerate-namespaces.js";
6
- import { runCodegen } from "./run-codegen.js";
7
-
8
- function setupProject() {
9
- const projectRoot = mkdtempSync(join(tmpdir(), "xndrjs-i18n-a5-"));
10
- mkdirSync(join(projectRoot, "translations"), { recursive: true });
11
- mkdirSync(join(projectRoot, "generated"), { recursive: true });
12
-
13
- writeFileSync(
14
- join(projectRoot, "translations/default.json"),
15
- JSON.stringify(
16
- {
17
- welcome: { en: "Hello {name}!", it: "Ciao {name}!" },
18
- login: { en: "Login", it: "Accedi" },
19
- },
20
- null,
21
- 2
22
- )
23
- );
24
-
25
- writeFileSync(
26
- join(projectRoot, "translations/billing.json"),
27
- JSON.stringify(
28
- {
29
- invoice_summary: {
30
- en: "You have {count} invoices",
31
- it: "Hai {count} fatture",
32
- },
33
- },
34
- null,
35
- 2
36
- )
37
- );
38
-
39
- writeFileSync(
40
- join(projectRoot, "i18n.codegen.json"),
41
- JSON.stringify(
42
- {
43
- projectName: "App",
44
- namespaces: {
45
- default: "translations/default.json",
46
- billing: "translations/billing.json",
47
- },
48
- codegenPath: "generated",
49
- delivery: "split-by-locale",
50
- artifactsPath: "generated",
51
- },
52
- null,
53
- 2
54
- )
55
- );
56
-
57
- return projectRoot;
58
- }
59
-
60
- describe("runCodegen + regenerateNamespaces", () => {
61
- let projectRoot: string;
62
-
63
- afterEach(() => {
64
- if (projectRoot) {
65
- rmSync(projectRoot, { recursive: true, force: true });
66
- }
67
- });
68
-
69
- it("runCodegen emits types, loaders, and delivery JSON", () => {
70
- projectRoot = setupProject();
71
- const result = runCodegen({
72
- configPath: join(projectRoot, "i18n.codegen.json"),
73
- log: false,
74
- });
75
-
76
- expect(result.generatedFiles).toEqual(
77
- expect.arrayContaining([
78
- "generated/i18n-types.generated.ts",
79
- "generated/instance.generated.ts",
80
- "generated/namespace-loaders.generated.ts",
81
- "generated/dictionary-schema.generated.ts",
82
- ])
83
- );
84
- expect(
85
- readFileSync(join(projectRoot, "generated/translations/default.en.json"), "utf8")
86
- ).toContain("Hello {name}!");
87
- expect(
88
- readFileSync(join(projectRoot, "generated/namespace-loaders.generated.ts"), "utf8")
89
- ).toContain("import('./translations/default.en.json')");
90
- });
91
-
92
- it("runCodegen emits fetch loaders when configured", () => {
93
- projectRoot = setupProject();
94
- writeFileSync(
95
- join(projectRoot, "i18n.codegen.json"),
96
- JSON.stringify(
97
- {
98
- projectName: "App",
99
- namespaces: {
100
- default: "translations/default.json",
101
- billing: "translations/billing.json",
102
- },
103
- codegenPath: "generated",
104
- delivery: "split-by-locale",
105
- artifactsPath: "generated",
106
- loaderStrategy: "fetch",
107
- },
108
- null,
109
- 2
110
- )
111
- );
112
-
113
- runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
114
- const loaders = readFileSync(
115
- join(projectRoot, "generated/namespace-loaders.generated.ts"),
116
- "utf8"
117
- );
118
- expect(loaders).toContain('fetchImpl({ locale, namespace: "default" })');
119
- expect(loaders).toContain("createNamespaceLoaders");
120
- });
121
-
122
- it("regenerateNamespaces refreshes selected delivery JSON without rewriting types", () => {
123
- projectRoot = setupProject();
124
- runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
125
-
126
- const typesBefore = readFileSync(
127
- join(projectRoot, "generated/i18n-types.generated.ts"),
128
- "utf8"
129
- );
130
- const instanceBefore = readFileSync(
131
- join(projectRoot, "generated/instance.generated.ts"),
132
- "utf8"
133
- );
134
- const billingBefore = readFileSync(
135
- join(projectRoot, "generated/translations/billing.en.json"),
136
- "utf8"
137
- );
138
-
139
- writeFileSync(
140
- join(projectRoot, "translations/default.json"),
141
- JSON.stringify(
142
- {
143
- welcome: { en: "Hi {name}!", it: "Ciao {name}!" },
144
- login: { en: "Login", it: "Accedi" },
145
- },
146
- null,
147
- 2
148
- )
149
- );
150
-
151
- regenerateNamespaces({
152
- configPath: join(projectRoot, "i18n.codegen.json"),
153
- namespaces: ["default"],
154
- log: false,
155
- });
156
-
157
- const en = JSON.parse(
158
- readFileSync(join(projectRoot, "generated/translations/default.en.json"), "utf8")
159
- );
160
- expect(en.welcome.en).toBe("Hi {name}!");
161
- expect(en.login.en).toBe("Login");
162
- expect(readFileSync(join(projectRoot, "generated/translations/billing.en.json"), "utf8")).toBe(
163
- billingBefore
164
- );
165
- expect(readFileSync(join(projectRoot, "generated/i18n-types.generated.ts"), "utf8")).toBe(
166
- typesBefore
167
- );
168
- expect(readFileSync(join(projectRoot, "generated/instance.generated.ts"), "utf8")).toBe(
169
- instanceBefore
170
- );
171
- });
172
-
173
- it("regenerateNamespaces rejects authoring that changes ICU args", () => {
174
- projectRoot = setupProject();
175
- runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
176
-
177
- writeFileSync(
178
- join(projectRoot, "translations/default.json"),
179
- JSON.stringify(
180
- {
181
- welcome: { en: "Hello {name} {extra}!", it: "Ciao {name} {extra}!" },
182
- login: { en: "Login", it: "Accedi" },
183
- },
184
- null,
185
- 2
186
- )
187
- );
188
-
189
- expect(() =>
190
- regenerateNamespaces({
191
- configPath: join(projectRoot, "i18n.codegen.json"),
192
- namespaces: ["default"],
193
- log: false,
194
- })
195
- ).toThrow(/Contract change requires runCodegen/);
196
- });
197
-
198
- it("regenerateNamespaces rejects empty or unknown namespaces", () => {
199
- projectRoot = setupProject();
200
- runCodegen({ configPath: join(projectRoot, "i18n.codegen.json"), log: false });
201
-
202
- expect(() =>
203
- regenerateNamespaces({
204
- configPath: join(projectRoot, "i18n.codegen.json"),
205
- namespaces: [],
206
- log: false,
207
- })
208
- ).toThrow(/non-empty namespaces/);
209
-
210
- expect(() =>
211
- regenerateNamespaces({
212
- configPath: join(projectRoot, "i18n.codegen.json"),
213
- namespaces: ["missing"],
214
- log: false,
215
- })
216
- ).toThrow(/Unknown namespace/);
217
- });
218
-
219
- it("regenerateNamespaces requires a prior runCodegen schema", () => {
220
- projectRoot = setupProject();
221
-
222
- expect(() =>
223
- regenerateNamespaces({
224
- configPath: join(projectRoot, "i18n.codegen.json"),
225
- namespaces: ["default"],
226
- log: false,
227
- })
228
- ).toThrow(/Run runCodegen first/);
229
- });
230
- });