@xndrjs/i18n 0.7.0 → 0.8.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.
- package/README.md +67 -872
- package/dist/codegen/index.d.ts +117 -51
- package/dist/codegen/index.js +1585 -93
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +100 -234
- package/dist/index.js +74 -638
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js.map +1 -1
- package/package.json +2 -2
- package/src/IcuTranslationProviderMulti.test.ts +18 -40
- package/src/IcuTranslationProviderMulti.ts +27 -37
- package/src/audit/audit-dictionaries.ts +1 -1
- package/src/audit/run-audit.test.ts +6 -7
- package/src/builder-load-registry.ts +26 -5
- package/src/builder-loaders.ts +8 -11
- package/src/builder-types.test.ts +11 -42
- package/src/builder.test.ts +82 -243
- package/src/builder.ts +8 -110
- package/src/codegen/codegen-config-schema.ts +82 -77
- package/src/codegen/config.test.ts +55 -143
- package/src/codegen/config.ts +10 -67
- package/src/codegen/dictionary-spec-contract.test.ts +28 -0
- package/src/codegen/dictionary-spec-contract.ts +138 -0
- package/src/codegen/emit/dictionary-schema-file.ts +7 -43
- package/src/codegen/emit/instance-file.test.ts +35 -52
- package/src/codegen/emit/instance-file.ts +170 -268
- package/src/codegen/emit/namespace-loaders-file.test.ts +72 -74
- package/src/codegen/emit/namespace-loaders-file.ts +147 -90
- package/src/codegen/emit/types-file.test.ts +20 -41
- package/src/codegen/emit/types-file.ts +48 -74
- package/src/codegen/generate-i18n-types.test.ts +163 -492
- package/src/codegen/generate-i18n-types.ts +7 -269
- package/src/codegen/paths.ts +0 -14
- package/src/codegen/project-locales-set-namespace.test.ts +44 -86
- package/src/codegen/read-dictionary.test.ts +27 -9
- package/src/codegen/read-dictionary.ts +18 -40
- package/src/codegen/regenerate-namespaces.ts +180 -0
- package/src/codegen/run-codegen.test.ts +230 -0
- package/src/codegen/run-codegen.ts +252 -0
- package/src/codegen/types.ts +0 -6
- package/src/codegen-config/build-config.ts +4 -23
- package/src/codegen-config/codegen-config.test.ts +10 -6
- package/src/codegen-config/index.ts +18 -3
- package/src/engine.ts +3 -27
- package/src/fetch-artifact.ts +16 -0
- package/src/i18n-handle.ts +202 -0
- package/src/index.ts +15 -34
- package/src/project-locales.ts +2 -2
- package/src/scope-multi.ts +1 -20
- package/src/scope-types.ts +14 -6
- package/src/scope.test.ts +3 -312
- package/src/serialized-state.test.ts +114 -0
- package/src/serialized-state.ts +28 -0
- package/src/setup/setup-i18n.test.ts +22 -33
- package/src/setup/setup-i18n.ts +22 -27
- package/src/types.ts +3 -3
- package/src/validation/index.ts +4 -0
- package/src/IcuTranslationProviderSingle.test.ts +0 -177
- package/src/IcuTranslationProviderSingle.ts +0 -114
- package/src/builder-multi.ts +0 -481
- package/src/codegen/emit/dictionary-file.test.ts +0 -215
- package/src/codegen/emit/dictionary-file.ts +0 -244
- package/src/deep-freeze.test.ts +0 -40
- package/src/deep-freeze.ts +0 -22
- package/src/patch-key.test.ts +0 -186
- package/src/patch-key.ts +0 -140
- package/src/scope-single.ts +0 -85
- package/src/single-builder.ts +0 -153
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
IDENTIFIER_NAME_PATTERN,
|
|
5
|
-
IDENTIFIER_NAME_REQUIREMENT,
|
|
6
|
-
SUPPORTED_IMPORT_EXTENSIONS,
|
|
7
|
-
} from "./constants.js";
|
|
3
|
+
import { IDENTIFIER_NAME_PATTERN, IDENTIFIER_NAME_REQUIREMENT } from "./constants.js";
|
|
8
4
|
import { getDeliveryArtifactsStructureIssues } from "./delivery-artifacts.js";
|
|
5
|
+
import { typeNamesForProject } from "../codegen-config/type-names.js";
|
|
9
6
|
|
|
10
7
|
const localeFallbackSchema = z.record(z.string(), z.union([z.string(), z.null()]));
|
|
11
8
|
const deliveryArtifactsSchema = z.record(z.string(), z.array(z.string().min(1)));
|
|
12
9
|
|
|
13
|
-
export const DELIVERY_MODES = ["
|
|
10
|
+
export const DELIVERY_MODES = ["split-by-locale", "custom"] as const;
|
|
14
11
|
export type DeliveryMode = (typeof DELIVERY_MODES)[number];
|
|
15
12
|
|
|
13
|
+
export const LOADER_STRATEGIES = ["import", "fetch"] as const;
|
|
14
|
+
export type LoaderStrategy = (typeof LOADER_STRATEGIES)[number];
|
|
15
|
+
|
|
16
|
+
const PROJECT_NAME_PATTERN = /^[A-Z][a-zA-Z0-9]*$/;
|
|
17
|
+
|
|
16
18
|
const codegenConfigShape = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
defaultNamespace: z.string().min(1).optional(),
|
|
20
|
-
typesOutput: z.string().min(1),
|
|
21
|
-
dictionaryOutput: z
|
|
19
|
+
/** PascalCase project id — generates `{project}Params`, `{project}Schema`, `{project}Locale`. */
|
|
20
|
+
projectName: z
|
|
22
21
|
.string()
|
|
23
22
|
.min(1)
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
importExtension: z.enum(SUPPORTED_IMPORT_EXTENSIONS).optional(),
|
|
33
|
-
paramsTypeName: z.string().min(1),
|
|
34
|
-
schemaTypeName: z.string().min(1),
|
|
35
|
-
localeTypeName: z.string().min(1).optional(),
|
|
36
|
-
localeFallbackConstName: z.string().min(1).optional(),
|
|
23
|
+
.regex(PROJECT_NAME_PATTERN, 'projectName must be PascalCase (e.g. "MyApp")'),
|
|
24
|
+
namespaces: z.record(z.string(), z.string().min(1)),
|
|
25
|
+
/**
|
|
26
|
+
* Directory for generated TypeScript modules:
|
|
27
|
+
* `i18n-types.generated.ts`, `instance.generated.ts`,
|
|
28
|
+
* `namespace-loaders.generated.ts`, `dictionary-schema.generated.ts`.
|
|
29
|
+
*/
|
|
30
|
+
codegenPath: z.string().min(1),
|
|
37
31
|
localeFallback: localeFallbackSchema.optional(),
|
|
38
|
-
|
|
39
|
-
delivery: z.enum(DELIVERY_MODES).optional().default("canonical"),
|
|
32
|
+
delivery: z.enum(DELIVERY_MODES).optional().default("split-by-locale"),
|
|
40
33
|
deliveryArtifacts: deliveryArtifactsSchema.optional(),
|
|
41
|
-
|
|
34
|
+
/** Directory for delivery JSON (`translations/`). Defaults to {@link codegenPath}. */
|
|
35
|
+
artifactsPath: z.string().min(1).optional(),
|
|
36
|
+
/**
|
|
37
|
+
* How generated `namespaceLoaders` resolve artifacts.
|
|
38
|
+
* - `import` (default): dynamic `import()` — JSON is bundled; content updates need a rebuild.
|
|
39
|
+
* - `fetch`: runtime `fetchImpl({ locale, namespace, area? })` via required `createI18n({ fetchImpl })`.
|
|
40
|
+
* Codegen does not know URLs — mapping id → transport is an application concern.
|
|
41
|
+
*/
|
|
42
|
+
loaderStrategy: z.enum(LOADER_STRATEGIES).optional().default("import"),
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export const codegenConfigKeys = Object.keys(
|
|
@@ -49,39 +50,16 @@ export const codegenConfigSchema = z
|
|
|
49
50
|
.object(codegenConfigShape)
|
|
50
51
|
.strict()
|
|
51
52
|
.superRefine((config, ctx) => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (config.namespaces) {
|
|
63
|
-
for (const namespace of Object.keys(config.namespaces)) {
|
|
64
|
-
if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
|
|
65
|
-
ctx.addIssue({
|
|
66
|
-
code: "custom",
|
|
67
|
-
path: ["namespaces", namespace],
|
|
68
|
-
message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
53
|
+
for (const namespace of Object.keys(config.namespaces)) {
|
|
54
|
+
if (!IDENTIFIER_NAME_PATTERN.test(namespace)) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: "custom",
|
|
57
|
+
path: ["namespaces", namespace],
|
|
58
|
+
message: `Invalid namespace name "${namespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`,
|
|
59
|
+
});
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
62
|
|
|
74
|
-
if (
|
|
75
|
-
config.defaultNamespace !== undefined &&
|
|
76
|
-
!IDENTIFIER_NAME_PATTERN.test(config.defaultNamespace)
|
|
77
|
-
) {
|
|
78
|
-
ctx.addIssue({
|
|
79
|
-
code: "custom",
|
|
80
|
-
path: ["defaultNamespace"],
|
|
81
|
-
message: `Invalid namespace name "${config.defaultNamespace}" (${IDENTIFIER_NAME_REQUIREMENT}).`,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
63
|
if (config.delivery === "custom") {
|
|
86
64
|
if (!config.deliveryArtifacts) {
|
|
87
65
|
ctx.addIssue({
|
|
@@ -105,35 +83,62 @@ export const codegenConfigSchema = z
|
|
|
105
83
|
message: 'deliveryArtifacts is only allowed when delivery is "custom".',
|
|
106
84
|
});
|
|
107
85
|
}
|
|
108
|
-
|
|
109
|
-
if (config.loadOnInit !== undefined && config.delivery !== "canonical") {
|
|
110
|
-
ctx.addIssue({
|
|
111
|
-
code: "custom",
|
|
112
|
-
path: ["loadOnInit"],
|
|
113
|
-
message: 'loadOnInit is only allowed when delivery is "canonical".',
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
86
|
});
|
|
117
87
|
|
|
118
88
|
export type CodegenConfigInput = z.input<typeof codegenConfigSchema>;
|
|
119
89
|
export type CodegenConfig = z.infer<typeof codegenConfigSchema>;
|
|
120
90
|
|
|
121
|
-
export
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
91
|
+
export const GENERATED_BASENAMES = {
|
|
92
|
+
types: "i18n-types.generated.ts",
|
|
93
|
+
instance: "instance.generated.ts",
|
|
94
|
+
namespaceLoaders: "namespace-loaders.generated.ts",
|
|
95
|
+
dictionarySchema: "dictionary-schema.generated.ts",
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_FACTORY_NAME = "createI18n";
|
|
99
|
+
export const DEFAULT_LOCALE_FALLBACK_CONST_NAME = "LOCALE_FALLBACK";
|
|
100
|
+
|
|
101
|
+
/** Derived paths and symbol names from `projectName` + `codegenPath`. */
|
|
102
|
+
export interface ResolvedCodegenPaths {
|
|
103
|
+
codegenPath: string;
|
|
104
|
+
typesOutput: string;
|
|
105
|
+
instanceOutput: string;
|
|
106
|
+
namespaceLoadersOutput: string;
|
|
107
|
+
dictionarySchemaOutput: string;
|
|
108
|
+
artifactsPath: string;
|
|
109
|
+
paramsTypeName: string;
|
|
110
|
+
schemaTypeName: string;
|
|
111
|
+
localeTypeName: string;
|
|
112
|
+
localeFallbackConstName: string;
|
|
113
|
+
factoryName: string;
|
|
114
|
+
deliveryAreaTypeName: string;
|
|
125
115
|
}
|
|
126
116
|
|
|
127
|
-
|
|
117
|
+
export function resolveCodegenPaths(
|
|
118
|
+
config: Pick<CodegenConfig, "projectName" | "codegenPath" | "artifactsPath">
|
|
119
|
+
): ResolvedCodegenPaths {
|
|
120
|
+
const typeNames = typeNamesForProject(config.projectName);
|
|
121
|
+
const codegenPath = config.codegenPath;
|
|
122
|
+
return {
|
|
123
|
+
codegenPath,
|
|
124
|
+
typesOutput: path.join(codegenPath, GENERATED_BASENAMES.types),
|
|
125
|
+
instanceOutput: path.join(codegenPath, GENERATED_BASENAMES.instance),
|
|
126
|
+
namespaceLoadersOutput: path.join(codegenPath, GENERATED_BASENAMES.namespaceLoaders),
|
|
127
|
+
dictionarySchemaOutput: path.join(codegenPath, GENERATED_BASENAMES.dictionarySchema),
|
|
128
|
+
artifactsPath: config.artifactsPath ?? codegenPath,
|
|
129
|
+
paramsTypeName: typeNames.paramsTypeName,
|
|
130
|
+
schemaTypeName: typeNames.schemaTypeName,
|
|
131
|
+
localeTypeName: typeNames.localeTypeName,
|
|
132
|
+
localeFallbackConstName: DEFAULT_LOCALE_FALLBACK_CONST_NAME,
|
|
133
|
+
factoryName: DEFAULT_FACTORY_NAME,
|
|
134
|
+
deliveryAreaTypeName: `${config.projectName}DeliveryArea`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
config: Pick<CodegenConfig, "typesOutput" | "dictionaryOutput">
|
|
138
|
+
export function resolveArtifactsPath(
|
|
139
|
+
config: Pick<CodegenConfig, "codegenPath" | "artifactsPath">
|
|
132
140
|
): string {
|
|
133
|
-
return
|
|
134
|
-
config.dictionaryOutput ??
|
|
135
|
-
path.join(path.dirname(config.typesOutput), DEFAULT_DICTIONARY_BASENAME)
|
|
136
|
-
);
|
|
141
|
+
return config.artifactsPath ?? config.codegenPath;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
export function formatCodegenConfigIssues(error: z.ZodError): string {
|
|
@@ -2,9 +2,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { codegenConfigKeys,
|
|
6
|
-
import { loadConfig,
|
|
7
|
-
import { resolveImportExtension } from "./paths.js";
|
|
5
|
+
import { codegenConfigKeys, resolveCodegenPaths } from "./codegen-config-schema.js";
|
|
6
|
+
import { loadConfig, resolveArtifactsPath } from "./config.js";
|
|
8
7
|
|
|
9
8
|
function writeConfig(dir: string, config: Record<string, unknown>) {
|
|
10
9
|
const configPath = join(dir, "i18n.codegen.json");
|
|
@@ -13,14 +12,11 @@ function writeConfig(dir: string, config: Record<string, unknown>) {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
const validMultiConfig = {
|
|
15
|
+
projectName: "App",
|
|
16
16
|
namespaces: {
|
|
17
17
|
default: "translations/default.json",
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
dictionaryOutput: "generated/dictionary.generated.ts",
|
|
21
|
-
instanceOutput: "generated/instance.generated.ts",
|
|
22
|
-
paramsTypeName: "AppParams",
|
|
23
|
-
schemaTypeName: "AppSchema",
|
|
19
|
+
codegenPath: "generated",
|
|
24
20
|
};
|
|
25
21
|
|
|
26
22
|
describe("loadConfig", () => {
|
|
@@ -32,43 +28,52 @@ describe("loadConfig", () => {
|
|
|
32
28
|
}
|
|
33
29
|
});
|
|
34
30
|
|
|
35
|
-
it("accepts a valid multi-namespace config", () => {
|
|
31
|
+
it("accepts a valid multi-namespace config and defaults delivery to split-by-locale", () => {
|
|
36
32
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
37
33
|
const configPath = writeConfig(tempDir, validMultiConfig);
|
|
38
34
|
|
|
39
35
|
expect(loadConfig(configPath)).toEqual({
|
|
40
36
|
...validMultiConfig,
|
|
41
|
-
delivery: "
|
|
37
|
+
delivery: "split-by-locale",
|
|
38
|
+
loaderStrategy: "import",
|
|
42
39
|
});
|
|
43
40
|
});
|
|
44
41
|
|
|
45
|
-
it("accepts
|
|
42
|
+
it("accepts fetch loaderStrategy", () => {
|
|
46
43
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
47
|
-
const { dictionaryOutput: _dictionaryOutput, ...configWithoutDictionaryOutput } =
|
|
48
|
-
validMultiConfig;
|
|
49
44
|
const configPath = writeConfig(tempDir, {
|
|
50
|
-
...
|
|
51
|
-
|
|
45
|
+
...validMultiConfig,
|
|
46
|
+
loaderStrategy: "fetch",
|
|
52
47
|
});
|
|
53
48
|
|
|
54
|
-
expect(loadConfig(configPath)).
|
|
55
|
-
|
|
56
|
-
delivery: "split-by-locale",
|
|
49
|
+
expect(loadConfig(configPath)).toMatchObject({
|
|
50
|
+
loaderStrategy: "fetch",
|
|
57
51
|
});
|
|
58
52
|
});
|
|
59
53
|
|
|
60
|
-
it("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
).
|
|
54
|
+
it("rejects unknown config keys", () => {
|
|
55
|
+
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
56
|
+
const configPath = writeConfig(tempDir, {
|
|
57
|
+
...validMultiConfig,
|
|
58
|
+
notARealKey: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(() => loadConfig(configPath)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("resolves code paths and type names from projectName + codegenPath", () => {
|
|
65
|
+
expect(resolveCodegenPaths(validMultiConfig)).toMatchObject({
|
|
66
|
+
typesOutput: "generated/i18n-types.generated.ts",
|
|
67
|
+
instanceOutput: "generated/instance.generated.ts",
|
|
68
|
+
namespaceLoadersOutput: "generated/namespace-loaders.generated.ts",
|
|
69
|
+
dictionarySchemaOutput: "generated/dictionary-schema.generated.ts",
|
|
70
|
+
artifactsPath: "generated",
|
|
71
|
+
paramsTypeName: "AppParams",
|
|
72
|
+
schemaTypeName: "AppSchema",
|
|
73
|
+
localeTypeName: "AppLocale",
|
|
74
|
+
factoryName: "createI18n",
|
|
75
|
+
localeFallbackConstName: "LOCALE_FALLBACK",
|
|
76
|
+
});
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
it("throws on invalid config JSON", () => {
|
|
@@ -94,27 +99,26 @@ describe("loadConfig", () => {
|
|
|
94
99
|
expect(act).toThrow(`Allowed keys: ${codegenConfigKeys.join(", ")}`);
|
|
95
100
|
});
|
|
96
101
|
|
|
97
|
-
it("throws when
|
|
102
|
+
it("throws when required fields are missing", () => {
|
|
98
103
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
99
104
|
const configPath = writeConfig(tempDir, {
|
|
100
|
-
|
|
101
|
-
dictionary: "translations/default.json",
|
|
105
|
+
namespaces: validMultiConfig.namespaces,
|
|
102
106
|
});
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
108
|
+
const act = () => loadConfig(configPath);
|
|
109
|
+
expect(act).toThrow("projectName");
|
|
110
|
+
expect(act).toThrow("codegenPath");
|
|
111
|
+
expect(act).toThrow("Allowed keys:");
|
|
107
112
|
});
|
|
108
113
|
|
|
109
|
-
it("throws when
|
|
114
|
+
it("throws when projectName is not PascalCase", () => {
|
|
110
115
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
111
116
|
const configPath = writeConfig(tempDir, {
|
|
112
|
-
|
|
117
|
+
...validMultiConfig,
|
|
118
|
+
projectName: "my-app",
|
|
113
119
|
});
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
expect(act).toThrow("typesOutput");
|
|
117
|
-
expect(act).toThrow("Allowed keys:");
|
|
121
|
+
expect(() => loadConfig(configPath)).toThrow("projectName must be PascalCase");
|
|
118
122
|
});
|
|
119
123
|
|
|
120
124
|
it("throws when a namespace name is not a valid identifier", () => {
|
|
@@ -132,41 +136,29 @@ describe("loadConfig", () => {
|
|
|
132
136
|
);
|
|
133
137
|
});
|
|
134
138
|
|
|
135
|
-
it("
|
|
139
|
+
it("accepts artifactsPath for custom json artifact directory", () => {
|
|
136
140
|
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
137
141
|
const configPath = writeConfig(tempDir, {
|
|
138
142
|
...validMultiConfig,
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(() => loadConfig(configPath)).toThrow(
|
|
143
|
-
'defaultNamespace: Invalid namespace name "1default"'
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("accepts deliveryOutput for custom json artifact directory", () => {
|
|
148
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
149
|
-
const configPath = writeConfig(tempDir, {
|
|
150
|
-
...validMultiConfig,
|
|
151
|
-
deliveryOutput: "public/i18n",
|
|
143
|
+
artifactsPath: "public/i18n",
|
|
152
144
|
});
|
|
153
145
|
|
|
154
146
|
expect(loadConfig(configPath)).toMatchObject({
|
|
155
|
-
|
|
156
|
-
delivery: "
|
|
147
|
+
artifactsPath: "public/i18n",
|
|
148
|
+
delivery: "split-by-locale",
|
|
157
149
|
});
|
|
158
150
|
});
|
|
159
151
|
|
|
160
|
-
it("
|
|
152
|
+
it("resolveArtifactsPath defaults to codegenPath", () => {
|
|
161
153
|
expect(
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
resolveArtifactsPath({
|
|
155
|
+
codegenPath: "generated",
|
|
164
156
|
})
|
|
165
157
|
).toBe("generated");
|
|
166
158
|
expect(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
resolveArtifactsPath({
|
|
160
|
+
codegenPath: "generated",
|
|
161
|
+
artifactsPath: "public/i18n",
|
|
170
162
|
})
|
|
171
163
|
).toBe("public/i18n");
|
|
172
164
|
});
|
|
@@ -236,84 +228,4 @@ describe("loadConfig", () => {
|
|
|
236
228
|
|
|
237
229
|
expect(() => loadConfig(configPath)).toThrow('Locale "fr" appears in both "eu" and "us"');
|
|
238
230
|
});
|
|
239
|
-
|
|
240
|
-
it("throws when loadOnInit is set for split-by-locale delivery", () => {
|
|
241
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
242
|
-
const configPath = writeConfig(tempDir, {
|
|
243
|
-
...validMultiConfig,
|
|
244
|
-
delivery: "split-by-locale",
|
|
245
|
-
loadOnInit: ["default"],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
expect(() => loadConfig(configPath)).toThrow(
|
|
249
|
-
'loadOnInit is only allowed when delivery is "canonical".'
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("throws when loadOnInit is set for custom delivery", () => {
|
|
254
|
-
tempDir = mkdtempSync(join(tmpdir(), "xndrjs-i18n-config-"));
|
|
255
|
-
const configPath = writeConfig(tempDir, {
|
|
256
|
-
...validMultiConfig,
|
|
257
|
-
delivery: "custom",
|
|
258
|
-
deliveryArtifacts: {
|
|
259
|
-
eu: ["it"],
|
|
260
|
-
},
|
|
261
|
-
loadOnInit: ["default"],
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
expect(() => loadConfig(configPath)).toThrow(
|
|
265
|
-
'loadOnInit is only allowed when delivery is "canonical".'
|
|
266
|
-
);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe("resolveLoadOnInit", () => {
|
|
271
|
-
const entries = [
|
|
272
|
-
{ namespace: "default", filePath: "translations/default.json" },
|
|
273
|
-
{ namespace: "billing", filePath: "translations/billing.json" },
|
|
274
|
-
];
|
|
275
|
-
|
|
276
|
-
it("throws when loadOnInit is used in single mode", () => {
|
|
277
|
-
const config = { ...validMultiConfig, delivery: "canonical" as const, loadOnInit: ["default"] };
|
|
278
|
-
|
|
279
|
-
expect(() => resolveLoadOnInit(config, entries, true)).toThrow(
|
|
280
|
-
'[Codegen Error] "loadOnInit" is only supported in multi mode (namespaces config).'
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("throws when loadOnInit references an unknown namespace", () => {
|
|
285
|
-
const config = { ...validMultiConfig, delivery: "canonical" as const, loadOnInit: ["missing"] };
|
|
286
|
-
|
|
287
|
-
expect(() => resolveLoadOnInit(config, entries, false)).toThrow(
|
|
288
|
-
'[Codegen Error] loadOnInit: namespace "missing" is not defined in namespaces config.'
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("treats all namespaces as lazy when delivery is split-by-locale", () => {
|
|
293
|
-
const config = { ...validMultiConfig, delivery: "split-by-locale" as const };
|
|
294
|
-
|
|
295
|
-
expect(resolveLoadOnInit(config, entries, false)).toEqual({
|
|
296
|
-
loadOnInitSet: new Set(),
|
|
297
|
-
lazyEntries: entries,
|
|
298
|
-
hasLazy: true,
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("treats all namespaces as lazy when delivery is custom", () => {
|
|
303
|
-
const config = { ...validMultiConfig, delivery: "custom" as const };
|
|
304
|
-
|
|
305
|
-
expect(resolveLoadOnInit(config, entries, false)).toEqual({
|
|
306
|
-
loadOnInitSet: new Set(),
|
|
307
|
-
lazyEntries: entries,
|
|
308
|
-
hasLazy: true,
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe("resolveImportExtension", () => {
|
|
314
|
-
it("throws on unsupported import extensions", () => {
|
|
315
|
-
expect(() => resolveImportExtension({ importExtension: ".mjs" as never })).toThrow(
|
|
316
|
-
'[Codegen Error] importExtension must be "none", ".ts", or ".js", got ".mjs".'
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
231
|
});
|
package/src/codegen/config.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import type {
|
|
2
|
+
import type { NamespaceEntry } from "./types.js";
|
|
3
3
|
import {
|
|
4
4
|
CodegenConfig,
|
|
5
5
|
codegenConfigSchema,
|
|
6
6
|
formatCodegenConfigIssues,
|
|
7
7
|
} from "./codegen-config-schema.js";
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
resolveCodegenPaths,
|
|
11
|
+
resolveArtifactsPath,
|
|
12
|
+
GENERATED_BASENAMES,
|
|
13
|
+
DEFAULT_FACTORY_NAME,
|
|
14
|
+
DEFAULT_LOCALE_FALLBACK_CONST_NAME,
|
|
15
|
+
} from "./codegen-config-schema.js";
|
|
10
16
|
|
|
11
17
|
/** Parses and validates `i18n.codegen.json`; first step of the codegen pipeline. */
|
|
12
18
|
export function loadConfig(configPath: string): CodegenConfig {
|
|
@@ -27,73 +33,10 @@ export function loadConfig(configPath: string): CodegenConfig {
|
|
|
27
33
|
return result.data;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
/** Maps config to namespace entries
|
|
36
|
+
/** Maps config namespaces to namespace entries. */
|
|
31
37
|
export function resolveNamespaces(config: CodegenConfig): NamespaceEntry[] {
|
|
32
|
-
|
|
33
|
-
const hasNamespaces = Boolean(config.namespaces);
|
|
34
|
-
|
|
35
|
-
if (hasDictionary === hasNamespaces) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
'[Codegen Error] Config must specify exactly one of "dictionary" or "namespaces".'
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (hasDictionary) {
|
|
42
|
-
return [
|
|
43
|
-
{
|
|
44
|
-
namespace: config.defaultNamespace ?? "default",
|
|
45
|
-
filePath: config.dictionary!,
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return Object.entries(config.namespaces!).map(([namespace, filePath]) => ({
|
|
38
|
+
return Object.entries(config.namespaces).map(([namespace, filePath]) => ({
|
|
51
39
|
namespace,
|
|
52
40
|
filePath,
|
|
53
41
|
}));
|
|
54
42
|
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Splits namespaces into eager (`loadOnInit`) and lazy sets.
|
|
58
|
-
* Drives `InitialSchema`, `defaultDictionary`, and `namespace-loaders.generated.ts`.
|
|
59
|
-
*/
|
|
60
|
-
export function resolveLoadOnInit(
|
|
61
|
-
config: CodegenConfig,
|
|
62
|
-
entries: NamespaceEntry[],
|
|
63
|
-
isSingle: boolean
|
|
64
|
-
): LoadOnInitResolution {
|
|
65
|
-
if (isSingle) {
|
|
66
|
-
if (config.loadOnInit) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
'[Codegen Error] "loadOnInit" is only supported in multi mode (namespaces config).'
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
const all = new Set(entries.map((entry) => entry.namespace));
|
|
72
|
-
return { loadOnInitSet: all, lazyEntries: [], hasLazy: false };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const delivery = config.delivery ?? "canonical";
|
|
76
|
-
if (delivery !== "canonical") {
|
|
77
|
-
return {
|
|
78
|
-
loadOnInitSet: new Set(),
|
|
79
|
-
lazyEntries: entries,
|
|
80
|
-
hasLazy: entries.length > 0,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const allNamespaces = new Set(entries.map((entry) => entry.namespace));
|
|
85
|
-
const loadOnInitSet = config.loadOnInit
|
|
86
|
-
? new Set(config.loadOnInit)
|
|
87
|
-
: new Set(entries.map((entry) => entry.namespace));
|
|
88
|
-
|
|
89
|
-
for (const namespace of loadOnInitSet) {
|
|
90
|
-
if (!allNamespaces.has(namespace)) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`[Codegen Error] loadOnInit: namespace "${namespace}" is not defined in namespaces config.`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const lazyEntries = entries.filter((entry) => !loadOnInitSet.has(entry.namespace));
|
|
98
|
-
return { loadOnInitSet, lazyEntries, hasLazy: lazyEntries.length > 0 };
|
|
99
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildDictionarySpecFromAnalysis,
|
|
4
|
+
namespaceContractsMatch,
|
|
5
|
+
} from "./dictionary-spec-contract.js";
|
|
6
|
+
|
|
7
|
+
describe("dictionary-spec-contract", () => {
|
|
8
|
+
it("namespaceContractsMatch compares keys and VariableSpec", () => {
|
|
9
|
+
const entries = [{ namespace: "default", filePath: "translations/default.json" }];
|
|
10
|
+
const current = buildDictionarySpecFromAnalysis(entries, {
|
|
11
|
+
default: { welcome: { name: "string" }, login: {} },
|
|
12
|
+
});
|
|
13
|
+
const established = buildDictionarySpecFromAnalysis(entries, {
|
|
14
|
+
default: { welcome: { name: "string" }, login: {} },
|
|
15
|
+
});
|
|
16
|
+
expect(namespaceContractsMatch(["default"], current, established)).toBe(true);
|
|
17
|
+
|
|
18
|
+
const changedArgs = buildDictionarySpecFromAnalysis(entries, {
|
|
19
|
+
default: { welcome: { name: "string", extra: "string" }, login: {} },
|
|
20
|
+
});
|
|
21
|
+
expect(namespaceContractsMatch(["default"], changedArgs, established)).toBe(false);
|
|
22
|
+
|
|
23
|
+
const changedKeys = buildDictionarySpecFromAnalysis(entries, {
|
|
24
|
+
default: { welcome: { name: "string" } },
|
|
25
|
+
});
|
|
26
|
+
expect(namespaceContractsMatch(["default"], changedKeys, established)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|