@xndrjs/i18n 0.2.0 → 0.3.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 (33) hide show
  1. package/README.md +58 -8
  2. package/bin/audit.mjs +14 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/{types-CFWVT2PP.d.ts → types-C1CpXVOJ.d.ts} +7 -1
  5. package/dist/validation/index.d.ts +3 -2
  6. package/dist/validation/index.js +125 -28
  7. package/dist/validation/index.js.map +1 -1
  8. package/package.json +5 -3
  9. package/src/IcuTranslationProviderSingle.test.ts +54 -0
  10. package/src/audit/audit-dictionaries.test.ts +134 -0
  11. package/src/audit/audit-dictionaries.ts +172 -0
  12. package/src/audit/run-audit.test.ts +136 -0
  13. package/src/audit/run-audit.ts +81 -0
  14. package/src/codegen/emit/dictionary-file.ts +7 -3
  15. package/src/codegen/emit/dictionary-schema-file.ts +6 -4
  16. package/src/codegen/emit/instance-file.ts +10 -5
  17. package/src/codegen/emit/namespace-loaders-file.ts +10 -6
  18. package/src/codegen/generate-i18n-types.test.ts +496 -0
  19. package/src/codegen/generate-i18n-types.ts +33 -5
  20. package/src/codegen/icu-analysis.ts +22 -7
  21. package/src/codegen/locale-policy.test.ts +35 -0
  22. package/src/codegen/locale-policy.ts +26 -0
  23. package/src/codegen/paths.ts +27 -1
  24. package/src/codegen/read-dictionary.test.ts +104 -0
  25. package/src/codegen/read-dictionary.ts +133 -0
  26. package/src/codegen/types.ts +5 -0
  27. package/src/ensure-namespace.test.ts +1 -1
  28. package/src/icu/extract-variables.test.ts +199 -0
  29. package/src/icu/extract-variables.ts +160 -20
  30. package/src/icu/parse-template.ts +9 -2
  31. package/src/setup/setup-i18n.ts +7 -4
  32. package/src/validation/normalize.ts +17 -10
  33. package/src/validation/validation.test.ts +104 -0
@@ -11,7 +11,9 @@ import { formatNamespaceLoadersFile } from "./emit/namespace-loaders-file.js";
11
11
  import { formatTypesFile } from "./emit/types-file.js";
12
12
  import { analyzeDictionaries } from "./icu-analysis.js";
13
13
  import { collectRequestLocales, validateCodegenLocaleFallback } from "./locale-fallback.js";
14
- import { fail, toModuleBasename } from "./paths.js";
14
+ import { enrichLocaleFallback } from "./locale-policy.js";
15
+ import { fail, resolveImportExtension, toModuleBasename } from "./paths.js";
16
+ import { prepareDictionaryEntries } from "./read-dictionary.js";
15
17
 
16
18
  function main() {
17
19
  const configArgIndex = process.argv.indexOf("--config");
@@ -26,7 +28,21 @@ function main() {
26
28
  }
27
29
 
28
30
  const config = loadConfig(configPath);
29
- const entries = resolveNamespaces(config);
31
+ const sourceEntries = resolveNamespaces(config);
32
+ const generatedDirRelative = path.dirname(config.typesOutput);
33
+ let resolvedEntries;
34
+ let compiledFiles: string[];
35
+
36
+ try {
37
+ const prepared = prepareDictionaryEntries(projectRoot, sourceEntries, generatedDirRelative);
38
+ resolvedEntries = prepared.resolvedEntries;
39
+ compiledFiles = prepared.compiledFiles;
40
+ } catch (error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ fail(message);
43
+ }
44
+
45
+ const entries = resolvedEntries;
30
46
  const isSingle = Boolean(config.dictionary);
31
47
  const { loadOnInitSet, lazyEntries, hasLazy } = resolveLoadOnInit(config, entries, isSingle);
32
48
 
@@ -57,6 +73,7 @@ function main() {
57
73
  const localeFallbackConstName = config.localeFallbackConstName ?? "LOCALE_FALLBACK";
58
74
  const localeFallbackTypeName = `${localeTypeName}Fallback`;
59
75
  const factoryName = config.factoryName ?? "createI18n";
76
+ const importExtension = resolveImportExtension(config);
60
77
  const typesModule = toModuleBasename(typesOutputPath);
61
78
 
62
79
  const requestLocales = collectRequestLocales(locales, config.localeFallback);
@@ -69,6 +86,10 @@ function main() {
69
86
  ? entries.filter((entry) => loadOnInitSet.has(entry.namespace))
70
87
  : entries;
71
88
 
89
+ const localeFallbackForEmit = config.localeFallback
90
+ ? enrichLocaleFallback(locales, config.localeFallback)
91
+ : undefined;
92
+
72
93
  const typesContent = formatTypesFile({
73
94
  isSingle,
74
95
  entries,
@@ -79,7 +100,7 @@ function main() {
79
100
  localeTypeName,
80
101
  localeFallbackConstName,
81
102
  localeFallbackTypeName,
82
- localeFallback: config.localeFallback,
103
+ localeFallback: localeFallbackForEmit,
83
104
  paramsByNamespace,
84
105
  requestLocaleUnion,
85
106
  hasLazy,
@@ -96,6 +117,7 @@ function main() {
96
117
  dictionaryOutputPath,
97
118
  typesOutputPath,
98
119
  schemaTypeName,
120
+ importExtension,
99
121
  });
100
122
 
101
123
  const instanceContent = formatInstanceFile({
@@ -109,6 +131,7 @@ function main() {
109
131
  localeFallbackConstName,
110
132
  factoryName,
111
133
  hasLocaleFallback: Boolean(config.localeFallback),
134
+ importExtension,
112
135
  });
113
136
 
114
137
  fs.mkdirSync(path.dirname(typesOutputPath), { recursive: true });
@@ -133,7 +156,8 @@ function main() {
133
156
  schemaTypeName,
134
157
  typesModule,
135
158
  isSingle,
136
- dictionarySpecBlock
159
+ dictionarySpecBlock,
160
+ importExtension
137
161
  );
138
162
 
139
163
  fs.mkdirSync(path.dirname(dictionarySchemaOutputPath), { recursive: true });
@@ -159,7 +183,8 @@ function main() {
159
183
  typesModule,
160
184
  toModuleBasename(dictionarySchemaOutputPath),
161
185
  toModuleBasename(instanceOutputPath),
162
- factoryName
186
+ factoryName,
187
+ importExtension
163
188
  );
164
189
 
165
190
  fs.mkdirSync(path.dirname(namespaceLoadersOutputPath), { recursive: true });
@@ -168,6 +193,9 @@ function main() {
168
193
  }
169
194
 
170
195
  console.log(`✅ Generated: ${generatedFiles.join(", ")}`);
196
+ if (compiledFiles.length > 0) {
197
+ console.log(`✅ Compiled: ${compiledFiles.join(", ")}`);
198
+ }
171
199
  }
172
200
 
173
201
  main();
@@ -2,11 +2,12 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse } from "@formatjs/icu-messageformat-parser";
4
4
  import {
5
- extractVariables,
6
- mergeVariableSpecs,
5
+ extractVariableMeta,
6
+ mergeVariableMetaAcrossLocales,
7
7
  type VariableSpec,
8
8
  } from "../icu/extract-variables.js";
9
- import type { DictionaryJson, NamespaceEntry } from "./types.js";
9
+ import type { NamespaceEntry } from "./types.js";
10
+ import { readDictionaryFile } from "./read-dictionary.js";
10
11
 
11
12
  export function paramsTypeForVariables(variables: VariableSpec): string {
12
13
  const keys = Object.keys(variables);
@@ -47,12 +48,12 @@ export function analyzeDictionaries(
47
48
  continue;
48
49
  }
49
50
 
50
- const dictionary = JSON.parse(fs.readFileSync(absolutePath, "utf8")) as DictionaryJson;
51
+ const dictionary = readDictionaryFile(absolutePath);
51
52
  paramsByNamespace[entry.namespace] = {};
52
53
  argsSpecByNamespace[entry.namespace] = {};
53
54
 
54
55
  for (const [key, localesByKey] of Object.entries(dictionary)) {
55
- const variables: VariableSpec = {};
56
+ const localeMetas: ReturnType<typeof extractVariableMeta>[] = [];
56
57
 
57
58
  for (const locale of Object.keys(localesByKey)) {
58
59
  locales.add(locale);
@@ -61,8 +62,7 @@ export function analyzeDictionaries(
61
62
  for (const [locale, template] of Object.entries(localesByKey)) {
62
63
  try {
63
64
  const ast = parse(template);
64
- const extracted = extractVariables(ast);
65
- Object.assign(variables, mergeVariableSpecs(variables, extracted));
65
+ localeMetas.push(extractVariableMeta(ast));
66
66
  } catch (error) {
67
67
  hasErrors = true;
68
68
  const message = error instanceof Error ? error.message : String(error);
@@ -72,6 +72,21 @@ export function analyzeDictionaries(
72
72
  }
73
73
  }
74
74
 
75
+ if (localeMetas.length === 0) {
76
+ continue;
77
+ }
78
+
79
+ const mergedVariables = mergeVariableMetaAcrossLocales(localeMetas);
80
+ if (!mergedVariables.ok) {
81
+ hasErrors = true;
82
+ console.error(
83
+ `[Codegen Error] ${mergedVariables.message} — namespace "${entry.namespace}", key "${key}"`
84
+ );
85
+ continue;
86
+ }
87
+
88
+ const variables = mergedVariables.merged;
89
+
75
90
  paramsByNamespace[entry.namespace]![key] = paramsTypeForVariables(variables);
76
91
  argsSpecByNamespace[entry.namespace]![key] = variables;
77
92
  }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildRequiredLocales, enrichLocaleFallback } from "./locale-policy.js";
3
+
4
+ describe("locale-policy", () => {
5
+ it("buildRequiredLocales unions dictionary locales with fallback keys and targets", () => {
6
+ const locales = new Set(["en", "it"]);
7
+ const fallback = { en: null, "de-CH": "de-DE", "de-DE": "en", it: "en" };
8
+
9
+ expect(buildRequiredLocales(locales, fallback)).toEqual(["de-CH", "de-DE", "en", "it"]);
10
+ expect(buildRequiredLocales(locales)).toEqual(["en", "it"]);
11
+ });
12
+
13
+ it("enrichLocaleFallback adds null for required locales missing from config", () => {
14
+ const locales = new Set(["en", "fr"]);
15
+ const fallback = { en: null, it: "en" };
16
+
17
+ expect(enrichLocaleFallback(locales, fallback)).toEqual({
18
+ en: null,
19
+ fr: null,
20
+ it: "en",
21
+ });
22
+ });
23
+
24
+ it("enrichLocaleFallback is a no-op when all required locales are already configured", () => {
25
+ const locales = new Set(["en", "it"]);
26
+ const fallback = {
27
+ en: null,
28
+ "de-DE": "en",
29
+ "de-CH": "de-DE",
30
+ it: "en",
31
+ };
32
+
33
+ expect(enrichLocaleFallback(locales, fallback)).toEqual(fallback);
34
+ });
35
+ });
@@ -0,0 +1,26 @@
1
+ import { collectRequestLocales } from "./locale-fallback.js";
2
+
3
+ export function buildRequiredLocales(
4
+ dictionaryLocales: Set<string>,
5
+ configFallback?: Record<string, string | null>
6
+ ): string[] {
7
+ return [...collectRequestLocales(dictionaryLocales, configFallback)].sort();
8
+ }
9
+
10
+ export function enrichLocaleFallback(
11
+ dictionaryLocales: Set<string>,
12
+ configFallback: Record<string, string | null>
13
+ ): Record<string, string | null> {
14
+ const enriched: Record<string, string | null> = { ...configFallback };
15
+
16
+ for (const locale of buildRequiredLocales(dictionaryLocales, configFallback)) {
17
+ if (!(locale in enriched)) {
18
+ enriched[locale] = null;
19
+ }
20
+ }
21
+
22
+ const sortedEntries = Object.entries(enriched).sort(([left], [right]) =>
23
+ left.localeCompare(right)
24
+ );
25
+ return Object.fromEntries(sortedEntries);
26
+ }
@@ -1,6 +1,9 @@
1
1
  import path from "node:path";
2
+ import type { CodegenConfig, ImportExtension } from "./types.js";
3
+ import { SUPPORTED_IMPORT_EXTENSIONS } from "./types.js";
2
4
 
3
5
  export const GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
6
+ export const DEFAULT_IMPORT_EXTENSION: ImportExtension = "none";
4
7
 
5
8
  export function fail(message: string): never {
6
9
  console.error(message);
@@ -10,7 +13,7 @@ export function fail(message: string): never {
10
13
 
11
14
  export function toImportPath(fromFile: string, toFile: string): string {
12
15
  const relative = path.relative(path.dirname(fromFile), toFile).replace(/\\/g, "/");
13
- const withoutExt = relative.replace(/\.json$/, "");
16
+ const withoutExt = relative.replace(/\.(json|ya?ml)$/i, "");
14
17
  return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
15
18
  }
16
19
 
@@ -18,6 +21,29 @@ export function toModuleBasename(filePath: string): string {
18
21
  return path.basename(filePath).replace(/\.ts$/, "");
19
22
  }
20
23
 
24
+ export function resolveImportExtension(
25
+ config: Pick<CodegenConfig, "importExtension">
26
+ ): ImportExtension {
27
+ const extension = config.importExtension ?? DEFAULT_IMPORT_EXTENSION;
28
+ if (!SUPPORTED_IMPORT_EXTENSIONS.includes(extension)) {
29
+ fail(
30
+ `[Codegen Error] importExtension must be "none", ".ts", or ".js", got ${JSON.stringify(extension)}.`
31
+ );
32
+ }
33
+ return extension;
34
+ }
35
+
36
+ export function importExtensionSuffix(importExtension: ImportExtension): string {
37
+ return importExtension === "none" ? "" : importExtension;
38
+ }
39
+
40
+ export function toRelativeModuleImport(
41
+ moduleBasename: string,
42
+ importExtension: ImportExtension
43
+ ): string {
44
+ return `./${moduleBasename}${importExtensionSuffix(importExtension)}`;
45
+ }
46
+
21
47
  export function toImportIdentifier(namespace: string): string {
22
48
  const safe = namespace.replace(/[^a-zA-Z0-9_$]/g, "_");
23
49
  if (/^[0-9]/.test(safe)) {
@@ -0,0 +1,104 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } 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 {
6
+ getDictionaryFormat,
7
+ prepareDictionaryEntries,
8
+ readDictionaryFile,
9
+ resolveCompiledJsonPath,
10
+ } from "./read-dictionary.js";
11
+
12
+ describe("read-dictionary", () => {
13
+ let tempDir: string;
14
+
15
+ afterEach(() => {
16
+ if (tempDir) {
17
+ rmSync(tempDir, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ it("detects dictionary formats from file extension", () => {
22
+ expect(getDictionaryFormat("translations/default.json")).toBe("json");
23
+ expect(getDictionaryFormat("translations/default.yaml")).toBe("yaml");
24
+ expect(getDictionaryFormat("translations/default.yml")).toBe("yaml");
25
+ expect(getDictionaryFormat("translations/default.toml")).toBeNull();
26
+ });
27
+
28
+ it("resolves compiled json path under the generated output directory", () => {
29
+ expect(
30
+ resolveCompiledJsonPath("src/i18n/translations/billing.yaml", "src/i18n/generated")
31
+ ).toBe("src/i18n/generated/translations/billing.json");
32
+ expect(resolveCompiledJsonPath("translations/billing.yml", "generated")).toBe(
33
+ "generated/translations/billing.json"
34
+ );
35
+ });
36
+
37
+ it("reads multiline yaml dictionaries", () => {
38
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
39
+ const yamlPath = join(tempDir, "welcome.yaml");
40
+ writeFileSync(
41
+ yamlPath,
42
+ `welcome:
43
+ en: |
44
+ Line one
45
+ Line two
46
+ it: Ciao {name}
47
+ `
48
+ );
49
+
50
+ const dictionary = readDictionaryFile(yamlPath);
51
+ expect(dictionary.welcome?.en).toBe("Line one\nLine two\n");
52
+ expect(dictionary.welcome?.it).toBe("Ciao {name}");
53
+ });
54
+
55
+ it("compiles yaml sources to json under the generated output directory", () => {
56
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
57
+ mkdirSync(join(tempDir, "src/i18n/translations"), { recursive: true });
58
+ mkdirSync(join(tempDir, "src/i18n/generated"), { recursive: true });
59
+ writeFileSync(
60
+ join(tempDir, "src/i18n/translations/billing.yaml"),
61
+ `invoice_summary:
62
+ en: You have {count} invoices
63
+ `
64
+ );
65
+
66
+ const result = prepareDictionaryEntries(
67
+ tempDir,
68
+ [{ namespace: "billing", filePath: "src/i18n/translations/billing.yaml" }],
69
+ "src/i18n/generated"
70
+ );
71
+
72
+ expect(result.resolvedEntries).toEqual([
73
+ { namespace: "billing", filePath: "src/i18n/generated/translations/billing.json" },
74
+ ]);
75
+ expect(result.compiledFiles).toEqual([
76
+ "src/i18n/translations/billing.yaml → src/i18n/generated/translations/billing.json",
77
+ ]);
78
+
79
+ const compiled = JSON.parse(
80
+ readFileSync(join(tempDir, "src/i18n/generated/translations/billing.json"), "utf8")
81
+ ) as Record<string, Record<string, string>>;
82
+ expect(compiled.invoice_summary?.en).toBe("You have {count} invoices");
83
+ });
84
+
85
+ it("keeps json sources unchanged", () => {
86
+ tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-read-dict-"));
87
+ mkdirSync(join(tempDir, "translations"), { recursive: true });
88
+ writeFileSync(
89
+ join(tempDir, "translations/default.json"),
90
+ JSON.stringify({ welcome: { en: "Welcome {name}!" } })
91
+ );
92
+
93
+ const result = prepareDictionaryEntries(
94
+ tempDir,
95
+ [{ namespace: "default", filePath: "translations/default.json" }],
96
+ "generated"
97
+ );
98
+
99
+ expect(result.resolvedEntries).toEqual([
100
+ { namespace: "default", filePath: "translations/default.json" },
101
+ ]);
102
+ expect(result.compiledFiles).toEqual([]);
103
+ });
104
+ });
@@ -0,0 +1,133 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import type { DictionaryJson, NamespaceEntry } from "./types.js";
5
+
6
+ export const SUPPORTED_DICTIONARY_EXTENSIONS = [".json", ".yaml", ".yml"] as const;
7
+ export type DictionaryFormat = "json" | "yaml";
8
+
9
+ export function getDictionaryFormat(filePath: string): DictionaryFormat | null {
10
+ const extension = path.extname(filePath).toLowerCase();
11
+ if (extension === ".json") {
12
+ return "json";
13
+ }
14
+ if (extension === ".yaml" || extension === ".yml") {
15
+ return "yaml";
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export function resolveCompiledJsonPath(sourcePath: string, generatedDirRelative: string): string {
21
+ const baseName = `${path.basename(sourcePath, path.extname(sourcePath))}.json`;
22
+ return path.join(generatedDirRelative, "translations", baseName).replace(/\\/g, "/");
23
+ }
24
+
25
+ function assertDictionaryShape(value: unknown, context: string): asserts value is DictionaryJson {
26
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
27
+ throw new Error(`[Codegen Error] ${context} must be a plain object.`);
28
+ }
29
+
30
+ for (const [key, localesByKey] of Object.entries(value)) {
31
+ if (localesByKey === null || typeof localesByKey !== "object" || Array.isArray(localesByKey)) {
32
+ throw new Error(`[Codegen Error] ${context}: key "${key}" must map locales to strings.`);
33
+ }
34
+
35
+ for (const [locale, template] of Object.entries(localesByKey)) {
36
+ if (typeof template !== "string") {
37
+ throw new Error(
38
+ `[Codegen Error] ${context}: key "${key}", locale "${locale}" must be a string.`
39
+ );
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ export function readDictionaryFile(absolutePath: string): DictionaryJson {
46
+ const format = getDictionaryFormat(absolutePath);
47
+ if (!format) {
48
+ throw new Error(
49
+ `[Codegen Error] Unsupported dictionary format "${path.extname(absolutePath)}" for ${absolutePath}. Use .json, .yaml, or .yml.`
50
+ );
51
+ }
52
+
53
+ const source = fs.readFileSync(absolutePath, "utf8");
54
+ let parsed: unknown;
55
+
56
+ try {
57
+ parsed = format === "json" ? JSON.parse(source) : parseYaml(source);
58
+ } catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`[Codegen Error] Failed to parse dictionary ${absolutePath}: ${message}`);
61
+ }
62
+
63
+ assertDictionaryShape(parsed, `Dictionary file ${absolutePath}`);
64
+ return parsed;
65
+ }
66
+
67
+ function writeCompiledJson(absoluteJsonPath: string, dictionary: DictionaryJson): boolean {
68
+ const nextContent = `${JSON.stringify(dictionary, null, 2)}\n`;
69
+ if (fs.existsSync(absoluteJsonPath)) {
70
+ const currentContent = fs.readFileSync(absoluteJsonPath, "utf8");
71
+ if (currentContent === nextContent) {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ fs.mkdirSync(path.dirname(absoluteJsonPath), { recursive: true });
77
+ fs.writeFileSync(absoluteJsonPath, nextContent);
78
+ return true;
79
+ }
80
+
81
+ export interface PrepareDictionariesResult {
82
+ resolvedEntries: NamespaceEntry[];
83
+ compiledFiles: string[];
84
+ }
85
+
86
+ export function prepareDictionaryEntries(
87
+ projectRoot: string,
88
+ entries: NamespaceEntry[],
89
+ generatedDirRelative: string
90
+ ): PrepareDictionariesResult {
91
+ const resolvedEntries: NamespaceEntry[] = [];
92
+ const compiledFiles: string[] = [];
93
+
94
+ for (const entry of entries) {
95
+ const sourceAbsolutePath = path.resolve(projectRoot, entry.filePath);
96
+ const format = getDictionaryFormat(entry.filePath);
97
+
98
+ if (!format) {
99
+ throw new Error(
100
+ `[Codegen Error] Namespace "${entry.namespace}" uses unsupported dictionary extension in "${entry.filePath}". Use .json, .yaml, or .yml.`
101
+ );
102
+ }
103
+
104
+ if (!fs.existsSync(sourceAbsolutePath)) {
105
+ throw new Error(
106
+ `[Codegen Error] Dictionary file not found for namespace "${entry.namespace}": ${sourceAbsolutePath}`
107
+ );
108
+ }
109
+
110
+ const dictionary = readDictionaryFile(sourceAbsolutePath);
111
+
112
+ if (format === "yaml") {
113
+ const compiledRelativePath = resolveCompiledJsonPath(entry.filePath, generatedDirRelative);
114
+ const compiledAbsolutePath = path.resolve(projectRoot, compiledRelativePath);
115
+ const wroteFile = writeCompiledJson(compiledAbsolutePath, dictionary);
116
+
117
+ if (wroteFile) {
118
+ compiledFiles.push(
119
+ `${path.relative(projectRoot, sourceAbsolutePath)} → ${compiledRelativePath}`
120
+ );
121
+ }
122
+ resolvedEntries.push({
123
+ namespace: entry.namespace,
124
+ filePath: compiledRelativePath,
125
+ });
126
+ continue;
127
+ }
128
+
129
+ resolvedEntries.push(entry);
130
+ }
131
+
132
+ return { resolvedEntries, compiledFiles };
133
+ }
@@ -1,3 +1,6 @@
1
+ export const SUPPORTED_IMPORT_EXTENSIONS = ["none", ".ts", ".js"] as const;
2
+ export type ImportExtension = (typeof SUPPORTED_IMPORT_EXTENSIONS)[number];
3
+
1
4
  export interface CodegenConfig {
2
5
  dictionary?: string;
3
6
  namespaces?: Record<string, string>;
@@ -8,6 +11,8 @@ export interface CodegenConfig {
8
11
  dictionarySchemaOutput?: string;
9
12
  loadOnInit?: string[];
10
13
  namespaceLoadersOutput?: string;
14
+ /** Extension used in relative imports between generated modules. Default: "none" */
15
+ importExtension?: ImportExtension;
11
16
  paramsTypeName: string;
12
17
  schemaTypeName: string;
13
18
  localeTypeName?: string;
@@ -89,7 +89,7 @@ describe("ensureNamespacesLoadedImpl", () => {
89
89
  const options = {
90
90
  provider,
91
91
  resolveLoader: () => load,
92
- validate: () => ({ ok: true, data: billingNs }),
92
+ validate: () => ({ ok: true, data: billingNs }) as const,
93
93
  };
94
94
 
95
95
  const first = ensureNamespacesLoadedImpl(options, ["billing"]);