@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,20 +0,0 @@
1
- /**
2
- * Codegen CLI entry point. Thin wrapper around {@link runCodegen}.
3
- */
4
- import path from "node:path";
5
- import { runCodegen } from "./run-codegen.js";
6
-
7
- function main() {
8
- const configArgIndex = process.argv.indexOf("--config");
9
- const configPath =
10
- configArgIndex >= 0 ? process.argv[configArgIndex + 1]! : "i18n/i18n.codegen.json";
11
-
12
- runCodegen(path.resolve(process.cwd(), configPath));
13
- }
14
-
15
- try {
16
- main();
17
- } catch (error) {
18
- console.error(error instanceof Error ? error.message : String(error));
19
- process.exit(1);
20
- }
@@ -1,110 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parse } from "@formatjs/icu-messageformat-parser";
4
- import {
5
- extractVariableMeta,
6
- mergeVariableMetaAcrossLocales,
7
- type VariableSpec,
8
- } from "../icu/extract-variables.js";
9
- import type { DictionaryJson, NamespaceEntry } from "./types.js";
10
- import { readDictionaryFile } from "./read-dictionary.js";
11
-
12
- export function paramsTypeForVariables(variables: VariableSpec): string {
13
- const keys = Object.keys(variables);
14
- if (keys.length === 0) {
15
- return "never";
16
- }
17
-
18
- const props = keys.map((key) => {
19
- const type = variables[key] === "date" ? "Date | number" : variables[key];
20
- return `${key}: ${type}`;
21
- });
22
- return `{ ${props.join("; ")} }`;
23
- }
24
-
25
- export interface DictionaryAnalysis {
26
- paramsByNamespace: Record<string, Record<string, string>>;
27
- argsSpecByNamespace: Record<string, Record<string, VariableSpec>>;
28
- locales: Set<string>;
29
- dictionariesByNamespace: Record<string, DictionaryJson>;
30
- }
31
-
32
- /**
33
- * Phase 1 of codegen: read source dictionaries, validate ICU syntax, infer param types.
34
- * Output feeds type emission, optional `DICTIONARY_SPEC`, and `prepareDictionaryEntries`.
35
- */
36
- export function analyzeDictionaries(
37
- projectRoot: string,
38
- entries: NamespaceEntry[]
39
- ): { ok: true; analysis: DictionaryAnalysis } | { ok: false } {
40
- const paramsByNamespace: Record<string, Record<string, string>> = {};
41
- const argsSpecByNamespace: Record<string, Record<string, VariableSpec>> = {};
42
- const locales = new Set<string>();
43
- const dictionariesByNamespace: Record<string, DictionaryJson> = {};
44
- let hasErrors = false;
45
-
46
- for (const entry of entries) {
47
- const absolutePath = path.resolve(projectRoot, entry.filePath);
48
-
49
- if (!fs.existsSync(absolutePath)) {
50
- console.error(
51
- `[Codegen Error] Dictionary file not found for namespace "${entry.namespace}": ${absolutePath}`
52
- );
53
- hasErrors = true;
54
- continue;
55
- }
56
-
57
- const dictionary = readDictionaryFile(absolutePath);
58
- dictionariesByNamespace[entry.namespace] = dictionary;
59
- paramsByNamespace[entry.namespace] = {};
60
- argsSpecByNamespace[entry.namespace] = {};
61
-
62
- for (const [key, localesByKey] of Object.entries(dictionary)) {
63
- const localeMetas: ReturnType<typeof extractVariableMeta>[] = [];
64
-
65
- for (const locale of Object.keys(localesByKey)) {
66
- locales.add(locale);
67
- }
68
-
69
- for (const [locale, template] of Object.entries(localesByKey)) {
70
- try {
71
- const ast = parse(template);
72
- localeMetas.push(extractVariableMeta(ast));
73
- } catch (error) {
74
- hasErrors = true;
75
- const message = error instanceof Error ? error.message : String(error);
76
- console.error(
77
- `[Codegen Error] ICU syntax error — namespace "${entry.namespace}", key "${key}", locale "${locale}": ${message}`
78
- );
79
- }
80
- }
81
-
82
- if (localeMetas.length === 0) {
83
- continue;
84
- }
85
-
86
- const mergedVariables = mergeVariableMetaAcrossLocales(localeMetas);
87
- if (!mergedVariables.ok) {
88
- hasErrors = true;
89
- console.error(
90
- `[Codegen Error] ${mergedVariables.message} — namespace "${entry.namespace}", key "${key}"`
91
- );
92
- continue;
93
- }
94
-
95
- const variables = mergedVariables.merged;
96
-
97
- paramsByNamespace[entry.namespace]![key] = paramsTypeForVariables(variables);
98
- argsSpecByNamespace[entry.namespace]![key] = variables;
99
- }
100
- }
101
-
102
- if (hasErrors) {
103
- return { ok: false };
104
- }
105
-
106
- return {
107
- ok: true,
108
- analysis: { paramsByNamespace, argsSpecByNamespace, locales, dictionariesByNamespace },
109
- };
110
- }
@@ -1,73 +0,0 @@
1
- import { validateLocaleFallback } from "../resolve-locale.js";
2
-
3
- /**
4
- * Union of locales found in dictionaries plus every locale referenced by `localeFallback`.
5
- * Drives split-by-locale partitioning and the generated locale union type.
6
- */
7
- export function collectRequestLocales(
8
- dictionaryLocales: Set<string>,
9
- fallback?: Record<string, string | null>
10
- ): Set<string> {
11
- const all = new Set(dictionaryLocales);
12
- if (!fallback) {
13
- return all;
14
- }
15
-
16
- for (const [locale, target] of Object.entries(fallback)) {
17
- all.add(locale);
18
- if (target !== null) {
19
- all.add(target);
20
- }
21
- }
22
-
23
- return all;
24
- }
25
-
26
- export type LocaleFallbackIssue = {
27
- path: (string | number)[];
28
- message: string;
29
- };
30
-
31
- /** Pre-emit validation gate for `localeFallback` config against discovered dictionary locales. */
32
- export function getCodegenLocaleFallbackIssues(
33
- fallback: Record<string, string | null>,
34
- dictionaryLocales: Set<string>
35
- ): LocaleFallbackIssue[] {
36
- const issues: LocaleFallbackIssue[] = [];
37
-
38
- try {
39
- validateLocaleFallback(fallback);
40
- } catch (error) {
41
- const message = error instanceof Error ? error.message : String(error);
42
- issues.push({ path: ["localeFallback"], message });
43
- }
44
-
45
- for (const [locale, target] of Object.entries(fallback)) {
46
- if (target !== null && !(target in fallback) && !dictionaryLocales.has(target)) {
47
- issues.push({
48
- path: ["localeFallback", locale],
49
- message: `localeFallback: "${locale}" points to "${target}" which is not defined in the fallback map or dictionary locales`,
50
- });
51
- }
52
- }
53
-
54
- return issues;
55
- }
56
-
57
- export function formatLocaleFallbackBlock(
58
- fallback: Record<string, string | null>,
59
- constName: string,
60
- typeName: string
61
- ): string {
62
- const lines = Object.entries(fallback)
63
- .map(
64
- ([locale, target]) =>
65
- ` ${JSON.stringify(locale)}: ${target === null ? "null" : JSON.stringify(target)},`
66
- )
67
- .join("\n");
68
-
69
- return (
70
- `export const ${constName} = {\n${lines}\n} as const satisfies Record<string, string | null>;\n\n` +
71
- `export type ${typeName} = typeof ${constName};\n\n`
72
- );
73
- }
@@ -1,35 +0,0 @@
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
- });
@@ -1,30 +0,0 @@
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
- /**
11
- * Completes `localeFallback` with explicit `null` entries for every request locale,
12
- * producing a stable map emitted into `i18n-types.generated.ts`.
13
- */
14
- export function enrichLocaleFallback(
15
- dictionaryLocales: Set<string>,
16
- configFallback: Record<string, string | null>
17
- ): Record<string, string | null> {
18
- const enriched: Record<string, string | null> = { ...configFallback };
19
-
20
- for (const locale of buildRequiredLocales(dictionaryLocales, configFallback)) {
21
- if (!(locale in enriched)) {
22
- enriched[locale] = null;
23
- }
24
- }
25
-
26
- const sortedEntries = Object.entries(enriched).sort(([left], [right]) =>
27
- left.localeCompare(right)
28
- );
29
- return Object.fromEntries(sortedEntries);
30
- }
@@ -1,44 +0,0 @@
1
- import path from "node:path";
2
- import type { ImportExtension } from "./types.js";
3
-
4
- export const GENERATED_FILE_BANNER = "// Automatically generated code. Do not edit manually.\n";
5
- export const DEFAULT_IMPORT_EXTENSION: ImportExtension = "none";
6
-
7
- export function reportCodegenIssues(issues: readonly { message: string }[]): void {
8
- for (const issue of issues) {
9
- console.error(`[Codegen Error] ${issue.message}`);
10
- }
11
- }
12
-
13
- export function toImportPath(fromFile: string, toFile: string): string {
14
- const relative = path.relative(path.dirname(fromFile), toFile).replace(/\\/g, "/");
15
- const withoutExt = relative.replace(/\.(json|ya?ml)$/i, "");
16
- return withoutExt.startsWith(".") ? withoutExt : `./${withoutExt}`;
17
- }
18
-
19
- export function toModuleBasename(filePath: string): string {
20
- return path.basename(filePath).replace(/\.ts$/, "");
21
- }
22
-
23
- export function importExtensionSuffix(importExtension: ImportExtension): string {
24
- return importExtension === "none" ? "" : importExtension;
25
- }
26
-
27
- export function toRelativeModuleImport(
28
- moduleBasename: string,
29
- importExtension: ImportExtension
30
- ): string {
31
- return `./${moduleBasename}${importExtensionSuffix(importExtension)}`;
32
- }
33
-
34
- export function toImportIdentifier(namespace: string): string {
35
- const safe = namespace.replace(/[^a-zA-Z0-9_$]/g, "_");
36
- if (/^[0-9]/.test(safe)) {
37
- return `ns_${safe}`;
38
- }
39
- return `${safe}Ns`;
40
- }
41
-
42
- export function toLocaleObjectKey(locale: string): string {
43
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(locale) ? locale : JSON.stringify(locale);
44
- }
@@ -1,134 +0,0 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { spawnSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { afterEach, describe, expect, it } from "vitest";
7
-
8
- const codegenScript = fileURLToPath(new URL("./generate-i18n-types.ts", import.meta.url));
9
- const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
10
-
11
- function runCodegen(cwd: string) {
12
- return spawnSync("tsx", [codegenScript, "--config", "i18n.codegen.json"], {
13
- cwd,
14
- encoding: "utf8",
15
- env: process.env,
16
- });
17
- }
18
-
19
- function runTsc(projectPath: string) {
20
- return spawnSync("pnpm", ["exec", "tsc", "--noEmit", "-p", projectPath], {
21
- cwd: packageRoot,
22
- encoding: "utf8",
23
- env: process.env,
24
- });
25
- }
26
-
27
- function setupMultiCodegenFixture(tempDir: string) {
28
- const i18nDir = join(tempDir, "src/i18n");
29
- const translationsDir = join(i18nDir, "translations");
30
- mkdirSync(translationsDir, { recursive: true });
31
-
32
- writeFileSync(
33
- join(translationsDir, "default.json"),
34
- JSON.stringify({
35
- login_button: { en: "Login" },
36
- })
37
- );
38
- writeFileSync(
39
- join(translationsDir, "billing.json"),
40
- readFileSync(
41
- fileURLToPath(new URL("./fixtures/billing-namespace.json", import.meta.url)),
42
- "utf8"
43
- )
44
- );
45
-
46
- writeFileSync(
47
- join(tempDir, "i18n.codegen.json"),
48
- JSON.stringify({
49
- projectName: "App",
50
- namespaces: {
51
- default: "src/i18n/translations/default.json",
52
- billing: "src/i18n/translations/billing.json",
53
- },
54
- codegenPath: "src/i18n",
55
- localeFallback: {
56
- en: null,
57
- it: "en",
58
- },
59
- })
60
- );
61
-
62
- const codegen = runCodegen(tempDir);
63
- expect(codegen.status, codegen.stderr || codegen.stdout).toBe(0);
64
- }
65
-
66
- function writeTscProject(tempDir: string) {
67
- writeFileSync(
68
- join(tempDir, "tsconfig.json"),
69
- JSON.stringify(
70
- {
71
- compilerOptions: {
72
- target: "ES2022",
73
- module: "ESNext",
74
- moduleResolution: "bundler",
75
- strict: true,
76
- noEmit: true,
77
- resolveJsonModule: true,
78
- esModuleInterop: true,
79
- skipLibCheck: true,
80
- paths: {
81
- "@xndrjs/i18n": [join(packageRoot, "src/index.ts")],
82
- "@xndrjs/i18n/validation": [join(packageRoot, "src/validation/index.ts")],
83
- },
84
- },
85
- include: ["src/**/*.ts"],
86
- },
87
- null,
88
- 2
89
- )
90
- );
91
- }
92
-
93
- describe("codegen instance + load", () => {
94
- let tempDir: string;
95
-
96
- afterEach(() => {
97
- if (tempDir) {
98
- rmSync(tempDir, { recursive: true, force: true });
99
- }
100
- });
101
-
102
- it("emits createI18n wired to namespaceLoaders", () => {
103
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
104
- setupMultiCodegenFixture(tempDir);
105
-
106
- const factory = readFileSync(join(tempDir, "src/i18n/instance.generated.ts"), "utf8");
107
- expect(factory).toContain("export function createI18n(");
108
- expect(factory).toContain("namespaceLoaders");
109
- });
110
-
111
- it("passes tsc when loading via generated namespaceLoaders", () => {
112
- tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-project-locales-"));
113
- setupMultiCodegenFixture(tempDir);
114
-
115
- writeFileSync(
116
- join(tempDir, "src/hydrate-billing.ts"),
117
- [
118
- `import { createI18n } from "./i18n/instance.generated.js";`,
119
- ``,
120
- `async function hydrateBilling() {`,
121
- ` const { t } = await createI18n().load({ namespaces: ["billing"], locale: "en" });`,
122
- ` return t("billing", "invoice_summary", { count: 1 });`,
123
- `}`,
124
- ``,
125
- `export { hydrateBilling };`,
126
- ].join("\n")
127
- );
128
-
129
- writeTscProject(tempDir);
130
-
131
- const tsc = runTsc(join(tempDir, "tsconfig.json"));
132
- expect(tsc.status, `${tsc.stdout}\n${tsc.stderr}`).toBe(0);
133
- });
134
- });