@xndrjs/i18n 0.8.1 → 0.8.2-alpha.1

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,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
- });