@xndrjs/i18n 0.8.1 → 0.8.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/audit.js +739 -0
  3. package/dist/cli/audit.js.map +1 -0
  4. package/dist/cli/codegen.js +1471 -0
  5. package/dist/cli/codegen.js.map +1 -0
  6. package/dist/cli/setup.js +148 -0
  7. package/dist/cli/setup.js.map +1 -0
  8. package/dist/codegen/index.js +43 -40
  9. package/dist/codegen/index.js.map +1 -1
  10. package/package.json +6 -9
  11. package/bin/audit.mjs +0 -14
  12. package/bin/codegen.mjs +0 -17
  13. package/bin/setup.mjs +0 -14
  14. package/src/IcuTranslationProviderMulti.test.ts +0 -215
  15. package/src/IcuTranslationProviderMulti.ts +0 -188
  16. package/src/audit/audit-dictionaries.test.ts +0 -134
  17. package/src/audit/audit-dictionaries.ts +0 -178
  18. package/src/audit/run-audit.test.ts +0 -169
  19. package/src/audit/run-audit.ts +0 -94
  20. package/src/builder-load-registry.test.ts +0 -44
  21. package/src/builder-load-registry.ts +0 -78
  22. package/src/builder-loaders.ts +0 -15
  23. package/src/builder-types.test.ts +0 -57
  24. package/src/builder-types.ts +0 -24
  25. package/src/builder.test.ts +0 -303
  26. package/src/builder.ts +0 -15
  27. package/src/codegen/codegen-config-schema.ts +0 -156
  28. package/src/codegen/config.test.ts +0 -231
  29. package/src/codegen/config.ts +0 -42
  30. package/src/codegen/constants.ts +0 -9
  31. package/src/codegen/delivery-artifacts.test.ts +0 -167
  32. package/src/codegen/delivery-artifacts.ts +0 -154
  33. package/src/codegen/dictionary-spec-contract.test.ts +0 -28
  34. package/src/codegen/dictionary-spec-contract.ts +0 -138
  35. package/src/codegen/emit/dictionary-schema-file.ts +0 -113
  36. package/src/codegen/emit/instance-file.test.ts +0 -71
  37. package/src/codegen/emit/instance-file.ts +0 -257
  38. package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
  39. package/src/codegen/emit/namespace-loaders-file.ts +0 -229
  40. package/src/codegen/emit/types-file.test.ts +0 -96
  41. package/src/codegen/emit/types-file.ts +0 -142
  42. package/src/codegen/fixtures/billing-namespace.json +0 -6
  43. package/src/codegen/generate-i18n-types.test.ts +0 -1137
  44. package/src/codegen/generate-i18n-types.ts +0 -20
  45. package/src/codegen/icu-analysis.ts +0 -110
  46. package/src/codegen/locale-fallback.ts +0 -73
  47. package/src/codegen/locale-policy.test.ts +0 -35
  48. package/src/codegen/locale-policy.ts +0 -30
  49. package/src/codegen/paths.ts +0 -44
  50. package/src/codegen/project-locales-set-namespace.test.ts +0 -134
  51. package/src/codegen/read-dictionary.test.ts +0 -594
  52. package/src/codegen/read-dictionary.ts +0 -260
  53. package/src/codegen/regenerate-namespaces.ts +0 -180
  54. package/src/codegen/run-codegen.test.ts +0 -230
  55. package/src/codegen/run-codegen.ts +0 -252
  56. package/src/codegen/types.ts +0 -10
  57. package/src/codegen/write-file-if-changed.test.ts +0 -42
  58. package/src/codegen/write-file-if-changed.ts +0 -20
  59. package/src/codegen-config/build-config.ts +0 -15
  60. package/src/codegen-config/codegen-config.test.ts +0 -36
  61. package/src/codegen-config/index.ts +0 -37
  62. package/src/codegen-config/type-names.ts +0 -20
  63. package/src/codegen-config/write-config.ts +0 -8
  64. package/src/engine.ts +0 -47
  65. package/src/fetch-artifact.ts +0 -16
  66. package/src/format-core.ts +0 -91
  67. package/src/i18n-handle.ts +0 -216
  68. package/src/icu/extract-variables.test.ts +0 -199
  69. package/src/icu/extract-variables.ts +0 -227
  70. package/src/icu/parse-template.ts +0 -31
  71. package/src/index.ts +0 -63
  72. package/src/load-state.ts +0 -15
  73. package/src/project-locales.test.ts +0 -280
  74. package/src/project-locales.ts +0 -211
  75. package/src/resolve-locale.test.ts +0 -91
  76. package/src/resolve-locale.ts +0 -88
  77. package/src/scope-multi.ts +0 -105
  78. package/src/scope-types.ts +0 -35
  79. package/src/scope.test.ts +0 -172
  80. package/src/serialized-state.test.ts +0 -114
  81. package/src/serialized-state.ts +0 -28
  82. package/src/setup/setup-i18n.test.ts +0 -85
  83. package/src/setup/setup-i18n.ts +0 -156
  84. package/src/types.ts +0 -65
  85. package/src/validation/create-args-schema.ts +0 -76
  86. package/src/validation/create-normalized-schema.ts +0 -52
  87. package/src/validation/errors.ts +0 -43
  88. package/src/validation/index.ts +0 -202
  89. package/src/validation/normalize.ts +0 -299
  90. package/src/validation/to-dictionary.ts +0 -67
  91. package/src/validation/types.ts +0 -82
  92. package/src/validation/validate-normalized.ts +0 -133
  93. package/src/validation/validation.test.ts +0 -562
@@ -1,252 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import {
4
- loadConfig,
5
- resolveArtifactsPath,
6
- resolveCodegenPaths,
7
- resolveNamespaces,
8
- } from "./config.js";
9
- import type { CodegenConfig } from "./codegen-config-schema.js";
10
- import {
11
- formatDictionarySchemaFile,
12
- formatDictionarySpecBlock,
13
- } from "./emit/dictionary-schema-file.js";
14
- import { formatInstanceFile } from "./emit/instance-file.js";
15
- import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
16
- import { formatTypesFile } from "./emit/types-file.js";
17
- import { analyzeDictionaries } from "./icu-analysis.js";
18
- import { getDeliveryArtifactsIssues } from "./delivery-artifacts.js";
19
- import { collectRequestLocales, getCodegenLocaleFallbackIssues } from "./locale-fallback.js";
20
- import { enrichLocaleFallback } from "./locale-policy.js";
21
- import { DEFAULT_IMPORT_EXTENSION, reportCodegenIssues, toModuleBasename } from "./paths.js";
22
- import { prepareDictionaryEntries } from "./read-dictionary.js";
23
- import { writeFileIfChanged } from "./write-file-if-changed.js";
24
-
25
- export type RunCodegenInput =
26
- | string
27
- | {
28
- configPath?: string;
29
- config?: CodegenConfig;
30
- projectRoot?: string;
31
- /** When false, skip console.log summaries (default true). */
32
- log?: boolean;
33
- };
34
-
35
- export interface RunCodegenResult {
36
- projectRoot: string;
37
- generatedFiles: string[];
38
- compiledFiles: string[];
39
- }
40
-
41
- function resolveRunCodegenInput(input?: RunCodegenInput): {
42
- projectRoot: string;
43
- config: CodegenConfig;
44
- log: boolean;
45
- } {
46
- if (typeof input === "string" || input === undefined) {
47
- const configPath = path.resolve(
48
- process.cwd(),
49
- typeof input === "string" ? input : "i18n/i18n.codegen.json"
50
- );
51
- if (!fs.existsSync(configPath)) {
52
- throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
53
- }
54
- return {
55
- projectRoot: path.dirname(configPath),
56
- config: loadConfig(configPath),
57
- log: true,
58
- };
59
- }
60
-
61
- const log = input.log !== false;
62
-
63
- if (input.config !== undefined) {
64
- const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
65
- return { projectRoot, config: input.config, log };
66
- }
67
-
68
- const configPath = path.resolve(process.cwd(), input.configPath ?? "i18n/i18n.codegen.json");
69
- if (!fs.existsSync(configPath)) {
70
- throw new Error(`[Codegen Error] Config file not found: ${configPath}`);
71
- }
72
- return {
73
- projectRoot: path.dirname(configPath),
74
- config: loadConfig(configPath),
75
- log,
76
- };
77
- }
78
-
79
- /**
80
- * Full codegen pipeline: types + instance + loaders + delivery JSON (+ schema).
81
- * Run at build time or when the *contract* changes (keys, params, namespaces, locales).
82
- * Content-only refreshes (same ICU contract) should use {@link regenerateNamespaces} instead.
83
- */
84
- export function runCodegen(input?: RunCodegenInput): RunCodegenResult {
85
- const { projectRoot, config, log } = resolveRunCodegenInput(input);
86
- const sourceEntries = resolveNamespaces(config);
87
- const artifactsPathRelative = resolveArtifactsPath(config);
88
- const delivery = config.delivery ?? "split-by-locale";
89
- const loaderStrategy = config.loaderStrategy ?? "import";
90
-
91
- const analysisResult = analyzeDictionaries(projectRoot, sourceEntries);
92
- if (!analysisResult.ok) {
93
- throw new Error("[Codegen Error] Dictionary ICU analysis failed. See messages above.");
94
- }
95
-
96
- const { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace } =
97
- analysisResult.analysis;
98
-
99
- if (config.localeFallback) {
100
- const localeFallbackIssues = getCodegenLocaleFallbackIssues(config.localeFallback, locales);
101
- if (localeFallbackIssues.length > 0) {
102
- reportCodegenIssues(localeFallbackIssues);
103
- throw new Error("[Codegen Error] Invalid localeFallback configuration.");
104
- }
105
- }
106
-
107
- const paths = resolveCodegenPaths(config);
108
- const {
109
- paramsTypeName,
110
- schemaTypeName,
111
- localeTypeName,
112
- localeFallbackConstName,
113
- factoryName,
114
- deliveryAreaTypeName: resolvedDeliveryAreaTypeName,
115
- } = paths;
116
- const localeFallbackTypeName = `${localeTypeName}Fallback`;
117
- const importExtension = DEFAULT_IMPORT_EXTENSION;
118
-
119
- const requestLocales = collectRequestLocales(locales, config.localeFallback);
120
- const requestLocalesList = [...requestLocales].sort();
121
-
122
- if (delivery === "custom" && config.deliveryArtifacts) {
123
- const deliveryArtifactsIssues = getDeliveryArtifactsIssues(
124
- config.deliveryArtifacts,
125
- requestLocales
126
- );
127
- if (deliveryArtifactsIssues.length > 0) {
128
- reportCodegenIssues(deliveryArtifactsIssues);
129
- throw new Error("[Codegen Error] Invalid deliveryArtifacts configuration.");
130
- }
131
- }
132
-
133
- const {
134
- resolvedEntries: entries,
135
- splitPathsByNamespace,
136
- compiledFiles,
137
- } = prepareDictionaryEntries(projectRoot, sourceEntries, artifactsPathRelative, {
138
- dictionariesByNamespace,
139
- delivery,
140
- localeFallback: config.localeFallback,
141
- requestLocales: delivery === "split-by-locale" ? requestLocalesList : undefined,
142
- deliveryArtifacts: delivery === "custom" ? config.deliveryArtifacts : undefined,
143
- });
144
-
145
- const typesOutputPath = path.resolve(projectRoot, paths.typesOutput);
146
- const instanceOutputPath = path.resolve(projectRoot, paths.instanceOutput);
147
- const namespaceLoadersOutputPath = path.resolve(projectRoot, paths.namespaceLoadersOutput);
148
- const dictionarySchemaOutputPath = path.resolve(projectRoot, paths.dictionarySchemaOutput);
149
- const typesModule = toModuleBasename(typesOutputPath);
150
-
151
- const deliveryAreaNames =
152
- delivery === "custom" && config.deliveryArtifacts
153
- ? Object.keys(config.deliveryArtifacts).sort()
154
- : undefined;
155
- const deliveryAreaTypeName = delivery === "custom" ? resolvedDeliveryAreaTypeName : undefined;
156
-
157
- const localeFallbackForEmit = config.localeFallback
158
- ? enrichLocaleFallback(locales, config.localeFallback)
159
- : undefined;
160
-
161
- const typesContent = formatTypesFile({
162
- entries,
163
- projectRoot,
164
- typesOutputPath,
165
- paramsTypeName,
166
- schemaTypeName,
167
- localeTypeName,
168
- localeFallbackConstName,
169
- localeFallbackTypeName,
170
- localeFallback: localeFallbackForEmit,
171
- paramsByNamespace,
172
- requestLocales: requestLocalesList,
173
- ...(deliveryAreaTypeName && deliveryAreaNames
174
- ? {
175
- deliveryAreaTypeName,
176
- deliveryAreaNames,
177
- ...(delivery === "custom" && config.deliveryArtifacts
178
- ? { deliveryArtifacts: config.deliveryArtifacts }
179
- : {}),
180
- }
181
- : {}),
182
- lazyEntries: entries,
183
- });
184
-
185
- const instanceContent = formatInstanceFile({
186
- typesOutputPath,
187
- namespaceLoadersOutputPath,
188
- paramsTypeName,
189
- schemaTypeName,
190
- localeTypeName,
191
- localeFallbackConstName,
192
- factoryName,
193
- hasLocaleFallback: Boolean(config.localeFallback),
194
- hasLocaleType: requestLocalesList.length > 0,
195
- importExtension,
196
- delivery,
197
- loaderStrategy,
198
- ...(delivery === "custom" ? { localeDeliveryAreaConstName: "LOCALE_DELIVERY_AREA" } : {}),
199
- });
200
-
201
- writeFileIfChanged(typesOutputPath, typesContent);
202
-
203
- const generatedFiles = [
204
- path.relative(projectRoot, typesOutputPath),
205
- path.relative(projectRoot, instanceOutputPath),
206
- ];
207
-
208
- writeFileIfChanged(instanceOutputPath, instanceContent);
209
-
210
- const dictionarySpecBlock = formatDictionarySpecBlock(entries, argsSpecByNamespace);
211
- const dictionarySchemaContent = formatDictionarySchemaFile(
212
- schemaTypeName,
213
- typesModule,
214
- dictionarySpecBlock,
215
- importExtension
216
- );
217
- writeFileIfChanged(dictionarySchemaOutputPath, dictionarySchemaContent);
218
- generatedFiles.push(path.relative(projectRoot, dictionarySchemaOutputPath));
219
-
220
- const lazyEntriesWithPaths = entries.map((entry) => ({
221
- ...entry,
222
- absolutePath: path.resolve(projectRoot, entry.filePath),
223
- }));
224
- const namespaceLoadersContent = formatNamespaceLoadersFile({
225
- loadersOutputPath: namespaceLoadersOutputPath,
226
- lazyEntries: lazyEntriesWithPaths,
227
- schemaTypeName,
228
- localeTypeName,
229
- typesModule,
230
- importExtension,
231
- projectRoot,
232
- delivery,
233
- splitPathsByNamespace,
234
- loaderStrategy,
235
- ...(delivery === "split-by-locale" ? { requestLocales: requestLocalesList } : {}),
236
- ...(delivery === "custom" && deliveryAreaTypeName && deliveryAreaNames
237
- ? { deliveryAreaTypeName, deliveryAreaNames }
238
- : {}),
239
- });
240
-
241
- writeFileIfChanged(namespaceLoadersOutputPath, namespaceLoadersContent);
242
- generatedFiles.push(path.relative(projectRoot, namespaceLoadersOutputPath));
243
-
244
- if (log) {
245
- console.log(`✅ Generated: ${generatedFiles.join(", ")}`);
246
- if (compiledFiles.length > 0) {
247
- console.log(`✅ Compiled: ${compiledFiles.join(", ")}`);
248
- }
249
- }
250
-
251
- return { projectRoot, generatedFiles, compiledFiles };
252
- }
@@ -1,10 +0,0 @@
1
- import type { SUPPORTED_IMPORT_EXTENSIONS } from "./constants.js";
2
-
3
- export type ImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];
4
-
5
- export interface NamespaceEntry {
6
- namespace: string;
7
- filePath: string;
8
- }
9
-
10
- export type DictionaryJson = Record<string, Record<string, string>>;
@@ -1,42 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync, statSync, 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 { writeFileIfChanged } from "./write-file-if-changed.js";
6
-
7
- describe("writeFileIfChanged", () => {
8
- let tempDir: string;
9
-
10
- afterEach(() => {
11
- if (tempDir) {
12
- rmSync(tempDir, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- it("creates missing files together with their parent directories", () => {
17
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
18
- const filePath = join(tempDir, "nested/dir/output.ts");
19
-
20
- expect(writeFileIfChanged(filePath, "export {};\n")).toBe(true);
21
- expect(readFileSync(filePath, "utf8")).toBe("export {};\n");
22
- });
23
-
24
- it("skips the write when content is unchanged", () => {
25
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
26
- const filePath = join(tempDir, "output.ts");
27
- writeFileSync(filePath, "export {};\n");
28
- const mtimeBefore = statSync(filePath).mtimeMs;
29
-
30
- expect(writeFileIfChanged(filePath, "export {};\n")).toBe(false);
31
- expect(statSync(filePath).mtimeMs).toBe(mtimeBefore);
32
- });
33
-
34
- it("overwrites the file when content differs", () => {
35
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-write-"));
36
- const filePath = join(tempDir, "output.ts");
37
- writeFileSync(filePath, "export const a = 1;\n");
38
-
39
- expect(writeFileIfChanged(filePath, "export const a = 2;\n")).toBe(true);
40
- expect(readFileSync(filePath, "utf8")).toBe("export const a = 2;\n");
41
- });
42
- });
@@ -1,20 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- /**
5
- * Writes `content` to `absolutePath` only when it differs from the current
6
- * file content, keeping mtimes stable for downstream watchers/bundlers.
7
- * Returns true when the file was actually written.
8
- */
9
- export function writeFileIfChanged(absolutePath: string, content: string): boolean {
10
- if (fs.existsSync(absolutePath)) {
11
- const currentContent = fs.readFileSync(absolutePath, "utf8");
12
- if (currentContent === content) {
13
- return false;
14
- }
15
- }
16
-
17
- fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
18
- fs.writeFileSync(absolutePath, content);
19
- return true;
20
- }
@@ -1,15 +0,0 @@
1
- import type { CodegenConfigInput } from "../codegen/codegen-config-schema.js";
2
-
3
- const GENERATED_DIR = "generated";
4
- const TRANSLATIONS_DIR = "translations";
5
-
6
- export function buildCodegenConfig(project: string): CodegenConfigInput {
7
- return {
8
- projectName: project,
9
- namespaces: {
10
- default: `${TRANSLATIONS_DIR}/default.json`,
11
- },
12
- delivery: "split-by-locale",
13
- codegenPath: GENERATED_DIR,
14
- };
15
- }
@@ -1,36 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { describe, expect, it, afterEach } from "vitest";
5
- import { buildCodegenConfig, type CodegenConfigInput, writeCodegenConfig } from "./index.js";
6
-
7
- describe("codegen-config", () => {
8
- let tempDir: string;
9
-
10
- afterEach(() => {
11
- if (tempDir) {
12
- rmSync(tempDir, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- it("buildCodegenConfig returns a valid multi-namespace input shape", () => {
17
- const config = buildCodegenConfig("MyApp");
18
- expect(config.namespaces).toEqual({
19
- default: "translations/default.json",
20
- });
21
- expect(config.projectName).toBe("MyApp");
22
- expect(config.codegenPath).toBe("generated");
23
- expect(config).not.toHaveProperty("dictionary");
24
- expect(config).not.toHaveProperty("paramsTypeName");
25
- });
26
-
27
- it("writeCodegenConfig writes formatted JSON", () => {
28
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-codegen-config-"));
29
- const configPath = join(tempDir, "i18n/i18n.codegen.json");
30
- const config: CodegenConfigInput = buildCodegenConfig("Demo");
31
-
32
- writeCodegenConfig(configPath, config);
33
-
34
- expect(readFileSync(configPath, "utf8")).toBe(`${JSON.stringify(config, null, 2)}\n`);
35
- });
36
- });
@@ -1,37 +0,0 @@
1
- export type {
2
- CodegenConfig,
3
- CodegenConfigInput,
4
- DeliveryMode,
5
- LoaderStrategy,
6
- ResolvedCodegenPaths,
7
- } from "../codegen/codegen-config-schema.js";
8
- export {
9
- codegenConfigKeys,
10
- DELIVERY_MODES,
11
- LOADER_STRATEGIES,
12
- GENERATED_BASENAMES,
13
- DEFAULT_FACTORY_NAME,
14
- DEFAULT_LOCALE_FALLBACK_CONST_NAME,
15
- formatCodegenConfigIssues,
16
- resolveCodegenPaths,
17
- resolveArtifactsPath,
18
- } from "../codegen/codegen-config-schema.js";
19
- export { loadConfig, resolveNamespaces } from "../codegen/config.js";
20
-
21
- export type { DeliveryArtifactsMap } from "../codegen/delivery-artifacts.js";
22
-
23
- export {
24
- SUPPORTED_IMPORT_EXTENSIONS,
25
- type SupportedImportExtension,
26
- } from "../codegen/constants.js";
27
-
28
- export { buildCodegenConfig } from "./build-config.js";
29
- export { inferProjectName, typeNamesForProject } from "./type-names.js";
30
- export { writeCodegenConfig } from "./write-config.js";
31
-
32
- export { runCodegen, type RunCodegenInput, type RunCodegenResult } from "../codegen/run-codegen.js";
33
- export {
34
- regenerateNamespaces,
35
- type RegenerateNamespacesInput,
36
- type RegenerateNamespacesResult,
37
- } from "../codegen/regenerate-namespaces.js";
@@ -1,20 +0,0 @@
1
- export function inferProjectName(dirName: string): string {
2
- const parts = dirName.split(/[-_]+/).filter(Boolean);
3
- if (parts.length === 0) {
4
- return "App";
5
- }
6
-
7
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8
- }
9
-
10
- export function typeNamesForProject(project: string): {
11
- paramsTypeName: string;
12
- schemaTypeName: string;
13
- localeTypeName: string;
14
- } {
15
- return {
16
- paramsTypeName: `${project}Params`,
17
- schemaTypeName: `${project}Schema`,
18
- localeTypeName: `${project}Locale`,
19
- };
20
- }
@@ -1,8 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import type { CodegenConfigInput } from "../codegen/codegen-config-schema.js";
4
-
5
- export function writeCodegenConfig(configPath: string, config: CodegenConfigInput): void {
6
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
7
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
8
- }
package/src/engine.ts DELETED
@@ -1,47 +0,0 @@
1
- import type { IcuTranslationProviderMulti } from "./IcuTranslationProviderMulti.js";
2
- import type {
3
- LocaleFallbackMap,
4
- LocaleOfMulti,
5
- MultiDictionary,
6
- PartialMultiDictionary,
7
- } from "./types.js";
8
- import type { I18nScopeMulti, I18nScopeMultiForLocale } from "./scope-multi.js";
9
- import type { MultiParams, ParamsForNamespaces, SchemaForNamespaces } from "./scope-types.js";
10
-
11
- /** Mutable translation engine for a multi-namespace dictionary. */
12
- export interface I18nEngineMulti<
13
- Schema extends MultiDictionary,
14
- Params extends MultiParams<Schema>,
15
- RequestLocales extends string = LocaleOfMulti<Schema>,
16
- > {
17
- readonly __i18nEngineMode: "multi";
18
- /** Loaded namespaces only — may be empty at cold start. */
19
- getAll(): PartialMultiDictionary<Schema, RequestLocales>;
20
- toScope<NsList extends readonly (keyof Schema & string)[]>(options: {
21
- namespaces: NsList;
22
- }): I18nScopeMulti<
23
- SchemaForNamespaces<Schema, NsList>,
24
- ParamsForNamespaces<Schema, Params, NsList>,
25
- RequestLocales
26
- >;
27
- toScope<
28
- NsList extends readonly (keyof Schema & string)[],
29
- Locale extends RequestLocales,
30
- >(options: {
31
- namespaces: NsList;
32
- locale: Locale;
33
- }): I18nScopeMultiForLocale<
34
- SchemaForNamespaces<Schema, NsList>,
35
- ParamsForNamespaces<Schema, Params, NsList>,
36
- RequestLocales,
37
- Locale
38
- >;
39
- }
40
-
41
- /** Alias for the default multi-namespace engine implementation. */
42
- export type I18nEngineMultiImpl<
43
- Schema extends MultiDictionary,
44
- Params extends MultiParams<Schema>,
45
- RequestLocales extends string = LocaleOfMulti<Schema>,
46
- Fallback extends LocaleFallbackMap | undefined = undefined,
47
- > = IcuTranslationProviderMulti<Schema, Params, RequestLocales, Fallback>;
@@ -1,16 +0,0 @@
1
- /**
2
- * Identifies a delivery JSON artifact. Codegen passes this to {@link FetchArtifact};
3
- * URL / filesystem mapping is an application concern.
4
- */
5
- export type DeliveryResourceId = {
6
- locale: string;
7
- namespace: string;
8
- /** Present for `delivery: "custom"` (partition key is the area). */
9
- area?: string;
10
- };
11
-
12
- /**
13
- * Loads a translation JSON artifact by resource id.
14
- * Must return the parsed JSON value (not a raw Response).
15
- */
16
- export type FetchArtifact = (id: DeliveryResourceId) => Promise<unknown>;
@@ -1,91 +0,0 @@
1
- import { IntlMessageFormat } from "intl-messageformat";
2
- import { formatLocaleFallbackChain, resolveLocaleTemplate } from "./resolve-locale.js";
3
- import type { LocaleCache, LocaleFallbackMap, OnMissingTranslation } from "./types.js";
4
-
5
- export type FormatCoreContext = {
6
- key: string;
7
- namespace?: string | undefined;
8
- locale: string;
9
- localeFallback?: LocaleFallbackMap | undefined;
10
- onMissing: OnMissingTranslation;
11
- };
12
-
13
- export type ResolveAndFormatOptions = {
14
- localeByKey: Record<string, string | undefined> | undefined;
15
- locale: string;
16
- params?: Record<string, unknown> | undefined;
17
- getCache: (resolvedLocale: string) => LocaleCache;
18
- context: FormatCoreContext;
19
- };
20
-
21
- function formatErrorLabel(context: Pick<FormatCoreContext, "key" | "namespace">): string {
22
- if (context.namespace !== undefined) {
23
- return `namespace "${context.namespace}", key "${context.key}"`;
24
- }
25
- return `key "${context.key}"`;
26
- }
27
-
28
- function formatMissingError(context: FormatCoreContext): string {
29
- const chain = formatLocaleFallbackChain(context.locale, context.localeFallback);
30
- if (context.namespace !== undefined) {
31
- return `[i18n] Missing key or locale: namespace "${context.namespace}", key "${context.key}" [${context.locale}] (fallback chain: ${chain})`;
32
- }
33
- return `[i18n] Missing key or locale: "${context.key}" [${context.locale}] (fallback chain: ${chain})`;
34
- }
35
-
36
- function resolveMissing(context: FormatCoreContext): string {
37
- const chain = formatLocaleFallbackChain(context.locale, context.localeFallback);
38
- if (context.onMissing === "key") {
39
- if (context.namespace !== undefined) {
40
- return `${context.namespace}.${context.key}`;
41
- }
42
- return context.key;
43
- }
44
- if (typeof context.onMissing === "function") {
45
- return context.onMissing({
46
- ...(context.namespace !== undefined ? { namespace: context.namespace } : {}),
47
- key: context.key,
48
- locale: context.locale,
49
- fallbackChain: chain,
50
- });
51
- }
52
- throw new Error(formatMissingError(context));
53
- }
54
-
55
- /**
56
- * Shared runtime path for both ICU providers: resolve locale (with fallback) →
57
- * compile template (cached) → format with params; handles missing keys via `onMissing`.
58
- */
59
- export function resolveAndFormat(options: ResolveAndFormatOptions): string {
60
- const { localeByKey, locale, params, getCache, context } = options;
61
- const resolved = resolveLocaleTemplate(localeByKey, locale, context.localeFallback);
62
-
63
- if (!resolved) {
64
- return resolveMissing(context);
65
- }
66
-
67
- const { template, resolvedLocale } = resolved;
68
- const cache = getCache(resolvedLocale);
69
- const cacheKey = context.key;
70
- const label = formatErrorLabel(context);
71
-
72
- if (!cache[cacheKey]) {
73
- try {
74
- cache[cacheKey] = new IntlMessageFormat(template, resolvedLocale);
75
- } catch (err) {
76
- const message = err instanceof Error ? err.message : String(err);
77
- throw new Error(
78
- `[i18n ICU Syntax Error] Dictionary error for ${label} [${resolvedLocale}]: ${message}`
79
- );
80
- }
81
- }
82
-
83
- try {
84
- return cache[cacheKey].format(params) as string;
85
- } catch (err) {
86
- const message = err instanceof Error ? err.message : String(err);
87
- throw new Error(
88
- `[i18n Formatting Error] Invalid or missing parameters for ${label} [${resolvedLocale}]: ${message}`
89
- );
90
- }
91
- }