@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.
- package/README.md +1 -2
- package/dist/cli/audit.js +739 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/codegen.js +1471 -0
- package/dist/cli/codegen.js.map +1 -0
- package/dist/cli/setup.js +148 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/codegen/index.js +43 -40
- package/dist/codegen/index.js.map +1 -1
- package/package.json +6 -9
- package/bin/audit.mjs +0 -14
- package/bin/codegen.mjs +0 -17
- package/bin/setup.mjs +0 -14
- package/src/IcuTranslationProviderMulti.test.ts +0 -215
- package/src/IcuTranslationProviderMulti.ts +0 -188
- package/src/audit/audit-dictionaries.test.ts +0 -134
- package/src/audit/audit-dictionaries.ts +0 -178
- package/src/audit/run-audit.test.ts +0 -169
- package/src/audit/run-audit.ts +0 -94
- package/src/builder-load-registry.test.ts +0 -44
- package/src/builder-load-registry.ts +0 -78
- package/src/builder-loaders.ts +0 -15
- package/src/builder-types.test.ts +0 -57
- package/src/builder-types.ts +0 -24
- package/src/builder.test.ts +0 -303
- package/src/builder.ts +0 -15
- package/src/codegen/codegen-config-schema.ts +0 -156
- package/src/codegen/config.test.ts +0 -231
- package/src/codegen/config.ts +0 -42
- package/src/codegen/constants.ts +0 -9
- package/src/codegen/delivery-artifacts.test.ts +0 -167
- package/src/codegen/delivery-artifacts.ts +0 -154
- package/src/codegen/dictionary-spec-contract.test.ts +0 -28
- package/src/codegen/dictionary-spec-contract.ts +0 -138
- package/src/codegen/emit/dictionary-schema-file.ts +0 -113
- package/src/codegen/emit/instance-file.test.ts +0 -71
- package/src/codegen/emit/instance-file.ts +0 -257
- package/src/codegen/emit/namespace-loaders-file.test.ts +0 -251
- package/src/codegen/emit/namespace-loaders-file.ts +0 -229
- package/src/codegen/emit/types-file.test.ts +0 -96
- package/src/codegen/emit/types-file.ts +0 -142
- package/src/codegen/fixtures/billing-namespace.json +0 -6
- package/src/codegen/generate-i18n-types.test.ts +0 -1137
- package/src/codegen/generate-i18n-types.ts +0 -20
- package/src/codegen/icu-analysis.ts +0 -110
- package/src/codegen/locale-fallback.ts +0 -73
- package/src/codegen/locale-policy.test.ts +0 -35
- package/src/codegen/locale-policy.ts +0 -30
- package/src/codegen/paths.ts +0 -44
- package/src/codegen/project-locales-set-namespace.test.ts +0 -134
- package/src/codegen/read-dictionary.test.ts +0 -594
- package/src/codegen/read-dictionary.ts +0 -260
- package/src/codegen/regenerate-namespaces.ts +0 -180
- package/src/codegen/run-codegen.test.ts +0 -230
- package/src/codegen/run-codegen.ts +0 -252
- package/src/codegen/types.ts +0 -10
- package/src/codegen/write-file-if-changed.test.ts +0 -42
- package/src/codegen/write-file-if-changed.ts +0 -20
- package/src/codegen-config/build-config.ts +0 -15
- package/src/codegen-config/codegen-config.test.ts +0 -36
- package/src/codegen-config/index.ts +0 -37
- package/src/codegen-config/type-names.ts +0 -20
- package/src/codegen-config/write-config.ts +0 -8
- package/src/engine.ts +0 -47
- package/src/fetch-artifact.ts +0 -16
- package/src/format-core.ts +0 -91
- package/src/i18n-handle.ts +0 -216
- package/src/icu/extract-variables.test.ts +0 -199
- package/src/icu/extract-variables.ts +0 -227
- package/src/icu/parse-template.ts +0 -31
- package/src/index.ts +0 -63
- package/src/load-state.ts +0 -15
- package/src/project-locales.test.ts +0 -280
- package/src/project-locales.ts +0 -211
- package/src/resolve-locale.test.ts +0 -91
- package/src/resolve-locale.ts +0 -88
- package/src/scope-multi.ts +0 -105
- package/src/scope-types.ts +0 -35
- package/src/scope.test.ts +0 -172
- package/src/serialized-state.test.ts +0 -114
- package/src/serialized-state.ts +0 -28
- package/src/setup/setup-i18n.test.ts +0 -85
- package/src/setup/setup-i18n.ts +0 -156
- package/src/types.ts +0 -65
- package/src/validation/create-args-schema.ts +0 -76
- package/src/validation/create-normalized-schema.ts +0 -52
- package/src/validation/errors.ts +0 -43
- package/src/validation/index.ts +0 -202
- package/src/validation/normalize.ts +0 -299
- package/src/validation/to-dictionary.ts +0 -67
- package/src/validation/types.ts +0 -82
- package/src/validation/validate-normalized.ts +0 -133
- package/src/validation/validation.test.ts +0 -562
package/src/setup/setup-i18n.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { buildCodegenConfig } from "../codegen-config/build-config.js";
|
|
5
|
-
import { inferProjectName } from "../codegen-config/type-names.js";
|
|
6
|
-
import { writeCodegenConfig } from "../codegen-config/write-config.js";
|
|
7
|
-
import { DEFAULT_IMPORT_EXTENSION, importExtensionSuffix } from "../codegen/paths.js";
|
|
8
|
-
|
|
9
|
-
export { buildCodegenConfig } from "../codegen-config/build-config.js";
|
|
10
|
-
|
|
11
|
-
export interface SetupOptions {
|
|
12
|
-
targetDir: string;
|
|
13
|
-
project?: string | undefined;
|
|
14
|
-
force?: boolean | undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SetupResult {
|
|
18
|
-
targetDir: string;
|
|
19
|
-
project: string;
|
|
20
|
-
created: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const I18N_ROOT = "i18n";
|
|
24
|
-
const CONFIG_FILE = `${I18N_ROOT}/i18n.codegen.json`;
|
|
25
|
-
/** Paths in i18n.codegen.json are relative to the i18n/ folder */
|
|
26
|
-
const GENERATED_DIR = "generated";
|
|
27
|
-
const TRANSLATIONS_DIR = "translations";
|
|
28
|
-
|
|
29
|
-
const DEFAULT_STARTER = {
|
|
30
|
-
welcome: { en: "Welcome {name}!" },
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const DEFAULT_IMPORT_SUFFIX = importExtensionSuffix(DEFAULT_IMPORT_EXTENSION);
|
|
34
|
-
|
|
35
|
-
const INDEX_TS =
|
|
36
|
-
`import { createI18n } from "./generated/instance.generated${DEFAULT_IMPORT_SUFFIX}";\n\n` +
|
|
37
|
-
`export * from "./generated/instance.generated${DEFAULT_IMPORT_SUFFIX}";\n` +
|
|
38
|
-
`export * from "./generated/i18n-types.generated${DEFAULT_IMPORT_SUFFIX}";\n` +
|
|
39
|
-
`export * from "./generated/namespace-loaders.generated${DEFAULT_IMPORT_SUFFIX}";\n\n` +
|
|
40
|
-
`export const i18n = createI18n();\n`;
|
|
41
|
-
|
|
42
|
-
function writeJson(filePath: string, value: unknown): void {
|
|
43
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
44
|
-
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function writeText(filePath: string, content: string): void {
|
|
48
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
49
|
-
fs.writeFileSync(filePath, content);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function relative(fromRoot: string, filePath: string): string {
|
|
53
|
-
return path.relative(fromRoot, filePath).replace(/\\/g, "/");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function runSetup(options: SetupOptions): SetupResult {
|
|
57
|
-
const targetDir = path.resolve(options.targetDir);
|
|
58
|
-
const project =
|
|
59
|
-
options.project ?? inferProjectName(path.basename(targetDir.replace(/[/\\]$/, "") || "app"));
|
|
60
|
-
|
|
61
|
-
if (!/^[A-Z][a-zA-Z0-9]*$/.test(project)) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`[Setup Error] Invalid project name "${project}". Use PascalCase (e.g. MyApp) via --project.`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const configPath = path.join(targetDir, CONFIG_FILE);
|
|
68
|
-
if (fs.existsSync(configPath) && !options.force) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`[Setup Error] ${relative(process.cwd(), configPath)} already exists. Use --force to overwrite.`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const created: string[] = [];
|
|
75
|
-
|
|
76
|
-
writeCodegenConfig(configPath, buildCodegenConfig(project));
|
|
77
|
-
created.push(relative(targetDir, configPath));
|
|
78
|
-
|
|
79
|
-
const defaultPath = path.join(targetDir, I18N_ROOT, TRANSLATIONS_DIR, "default.json");
|
|
80
|
-
writeJson(defaultPath, DEFAULT_STARTER);
|
|
81
|
-
created.push(relative(targetDir, defaultPath));
|
|
82
|
-
|
|
83
|
-
const indexPath = path.join(targetDir, I18N_ROOT, "index.ts");
|
|
84
|
-
writeText(indexPath, INDEX_TS);
|
|
85
|
-
created.push(relative(targetDir, indexPath));
|
|
86
|
-
|
|
87
|
-
fs.mkdirSync(path.join(targetDir, I18N_ROOT, GENERATED_DIR), { recursive: true });
|
|
88
|
-
|
|
89
|
-
return { targetDir, project, created };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function parseSetupArgs(argv: string[]): SetupOptions {
|
|
93
|
-
const first = argv[0];
|
|
94
|
-
|
|
95
|
-
let targetDir = ".";
|
|
96
|
-
let project: string | undefined;
|
|
97
|
-
let force = false;
|
|
98
|
-
let startIndex = 0;
|
|
99
|
-
|
|
100
|
-
if (first === "multi") {
|
|
101
|
-
startIndex = 1;
|
|
102
|
-
} else if (first !== undefined && !first.startsWith("-")) {
|
|
103
|
-
// positional targetDir without mode
|
|
104
|
-
} else if (first === undefined || first.startsWith("-")) {
|
|
105
|
-
// flags only / empty
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (let index = startIndex; index < argv.length; index++) {
|
|
109
|
-
const arg = argv[index]!;
|
|
110
|
-
if (arg === "--project") {
|
|
111
|
-
project = argv[++index];
|
|
112
|
-
if (!project) {
|
|
113
|
-
throw new Error("[Setup Error] --project requires a value.");
|
|
114
|
-
}
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (arg === "--force") {
|
|
118
|
-
force = true;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (arg.startsWith("-")) {
|
|
122
|
-
throw new Error(`[Setup Error] Unknown flag: ${arg}`);
|
|
123
|
-
}
|
|
124
|
-
targetDir = arg;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { targetDir, project, force };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function main() {
|
|
131
|
-
try {
|
|
132
|
-
const options = parseSetupArgs(process.argv.slice(2));
|
|
133
|
-
const result = runSetup(options);
|
|
134
|
-
|
|
135
|
-
console.log(`✅ Setup i18n in ${result.targetDir}`);
|
|
136
|
-
console.log(
|
|
137
|
-
` Project types: ${result.project}Params, ${result.project}Schema, ${result.project}Locale`
|
|
138
|
-
);
|
|
139
|
-
console.log(` Created: ${result.created.join(", ")}`);
|
|
140
|
-
const targetLabel = path.relative(process.cwd(), result.targetDir) || ".";
|
|
141
|
-
|
|
142
|
-
console.log("");
|
|
143
|
-
console.log(`Add to package.json scripts (from ${targetLabel}):`);
|
|
144
|
-
console.log(` "i18n:codegen": "xndrjs-i18n-codegen --config ${CONFIG_FILE}"`);
|
|
145
|
-
console.log("");
|
|
146
|
-
console.log("Then run your package manager's run script (pnpm/npm/yarn).");
|
|
147
|
-
} catch (error) {
|
|
148
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
-
console.error(message);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
155
|
-
main();
|
|
156
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { IntlMessageFormat } from "intl-messageformat";
|
|
2
|
-
|
|
3
|
-
export type LocaleDictionary = Record<string, string>;
|
|
4
|
-
export type KeyDictionary = Record<string, LocaleDictionary>;
|
|
5
|
-
export type MultiDictionary = Record<string, KeyDictionary>;
|
|
6
|
-
|
|
7
|
-
/** Locales present on any key of a flat (per-namespace) key dictionary. */
|
|
8
|
-
export type LocaleOfKeys<Schema extends KeyDictionary> = {
|
|
9
|
-
[K in keyof Schema]: keyof Schema[K];
|
|
10
|
-
}[keyof Schema] &
|
|
11
|
-
string;
|
|
12
|
-
|
|
13
|
-
export type LocaleOfMulti<Schema extends MultiDictionary> = {
|
|
14
|
-
[NS in keyof Schema]: {
|
|
15
|
-
[K in keyof Schema[NS]]: keyof Schema[NS][K];
|
|
16
|
-
}[keyof Schema[NS]];
|
|
17
|
-
}[keyof Schema] &
|
|
18
|
-
string;
|
|
19
|
-
|
|
20
|
-
/** Subset of schema keys with any subset of project locales per key — valid input for merge operations. */
|
|
21
|
-
export type PartialKeyDictionary<
|
|
22
|
-
T extends KeyDictionary,
|
|
23
|
-
Locales extends string = LocaleOfKeys<T>,
|
|
24
|
-
> = {
|
|
25
|
-
[K in keyof T]?: Partial<Record<Locales, string>>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** Subset of schema namespaces with partial keys/locales — valid input for multi merge operations. */
|
|
29
|
-
export type PartialMultiDictionary<
|
|
30
|
-
T extends MultiDictionary,
|
|
31
|
-
Locales extends string = LocaleOfMulti<T>,
|
|
32
|
-
> = {
|
|
33
|
-
[NS in keyof T]?: PartialKeyDictionary<T[NS], Locales>;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type LocaleFallbackMap = Record<string, string | null>;
|
|
37
|
-
|
|
38
|
-
export type MissingTranslationContext = {
|
|
39
|
-
/** Only set by the multi-namespace provider. */
|
|
40
|
-
namespace?: string;
|
|
41
|
-
key: string;
|
|
42
|
-
locale: string;
|
|
43
|
-
fallbackChain: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type OnMissingTranslation =
|
|
47
|
-
| "throw"
|
|
48
|
-
| "key"
|
|
49
|
-
| ((context: MissingTranslationContext) => string);
|
|
50
|
-
|
|
51
|
-
export type IcuTranslationProviderOptions<
|
|
52
|
-
Fallback extends LocaleFallbackMap | undefined = undefined,
|
|
53
|
-
> = {
|
|
54
|
-
localeFallback?: Fallback;
|
|
55
|
-
/**
|
|
56
|
-
* Strategy when no template resolves for a key/locale (after walking the
|
|
57
|
-
* fallback chain). "throw" (default) raises an error, "key" returns the
|
|
58
|
-
* translation key itself, a function receives the context and returns the
|
|
59
|
-
* string to display. ICU syntax and formatting errors always throw.
|
|
60
|
-
*/
|
|
61
|
-
onMissing?: OnMissingTranslation;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export type LocaleCache = Record<string, IntlMessageFormat>;
|
|
65
|
-
export type MultiCompiledCache = Record<string, Record<string, LocaleCache>>;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { VariableSpec, VariableType } from "../icu/extract-variables.js";
|
|
3
|
-
|
|
4
|
-
export const variableTypeSchema = z.enum(["string", "number", "date"]);
|
|
5
|
-
|
|
6
|
-
function formatVariableMismatch(expected: VariableSpec, found: VariableSpec): string {
|
|
7
|
-
const expectedKeys = Object.keys(expected).sort();
|
|
8
|
-
const foundKeys = Object.keys(found).sort();
|
|
9
|
-
return `Expected variables [${expectedKeys.join(", ")}], found [${foundKeys.join(", ")}]`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createArgsSchema(expected: VariableSpec): z.ZodType<VariableSpec> {
|
|
13
|
-
return z.record(z.string(), variableTypeSchema).superRefine((found, ctx) => {
|
|
14
|
-
const expectedKeys = Object.keys(expected).sort();
|
|
15
|
-
const foundKeys = Object.keys(found).sort();
|
|
16
|
-
|
|
17
|
-
if (expectedKeys.join() !== foundKeys.join()) {
|
|
18
|
-
ctx.addIssue({
|
|
19
|
-
code: "custom",
|
|
20
|
-
message: formatVariableMismatch(expected, found),
|
|
21
|
-
});
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
for (const [name, type] of Object.entries(expected)) {
|
|
26
|
-
if (found[name] !== type) {
|
|
27
|
-
ctx.addIssue({
|
|
28
|
-
code: "custom",
|
|
29
|
-
message: `Variable "${name}": expected ${type}, found ${found[name]}`,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function mapArgsSchemaError(
|
|
37
|
-
expected: VariableSpec,
|
|
38
|
-
found: VariableSpec,
|
|
39
|
-
message: string,
|
|
40
|
-
path: readonly string[]
|
|
41
|
-
) {
|
|
42
|
-
const expectedKeys = Object.keys(expected).sort();
|
|
43
|
-
const foundKeys = Object.keys(found).sort();
|
|
44
|
-
|
|
45
|
-
if (expectedKeys.join() !== foundKeys.join()) {
|
|
46
|
-
return {
|
|
47
|
-
kind: "variable_mismatch" as const,
|
|
48
|
-
path,
|
|
49
|
-
expected,
|
|
50
|
-
found,
|
|
51
|
-
message,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const [name, type] of Object.entries(expected)) {
|
|
56
|
-
const foundType = found[name];
|
|
57
|
-
if (foundType !== undefined && foundType !== type) {
|
|
58
|
-
return {
|
|
59
|
-
kind: "variable_type_mismatch" as const,
|
|
60
|
-
path,
|
|
61
|
-
variable: name,
|
|
62
|
-
expected: type,
|
|
63
|
-
found: foundType as VariableType,
|
|
64
|
-
message,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
kind: "variable_mismatch" as const,
|
|
71
|
-
path,
|
|
72
|
-
expected,
|
|
73
|
-
found,
|
|
74
|
-
message,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { VariableSpec } from "../icu/extract-variables.js";
|
|
3
|
-
import { createArgsSchema, variableTypeSchema } from "./create-args-schema.js";
|
|
4
|
-
import type { DictionarySpec, NormalizedDictionary, ParsedKeyEntry } from "./types.js";
|
|
5
|
-
|
|
6
|
-
const parsedLocaleEntrySchema = z.object({
|
|
7
|
-
template: z.string(),
|
|
8
|
-
args: z.record(z.string(), variableTypeSchema),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const parsedKeyEntrySchema = z.object({
|
|
12
|
-
locales: z.record(z.string(), parsedLocaleEntrySchema),
|
|
13
|
-
mergedArgs: z.record(z.string(), variableTypeSchema),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export function createKeyDictionarySchema(
|
|
17
|
-
argsByKey: Readonly<Record<string, VariableSpec>>
|
|
18
|
-
): z.ZodType<Record<string, ParsedKeyEntry>> {
|
|
19
|
-
const keySchemas = Object.fromEntries(
|
|
20
|
-
Object.entries(argsByKey).map(([key, expectedArgs]) => [
|
|
21
|
-
key,
|
|
22
|
-
parsedKeyEntrySchema.extend({
|
|
23
|
-
mergedArgs: createArgsSchema(expectedArgs),
|
|
24
|
-
}),
|
|
25
|
-
])
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return z.object(keySchemas);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function createNormalizedDictionarySchema(
|
|
32
|
-
spec: DictionarySpec
|
|
33
|
-
): z.ZodType<NormalizedDictionary> {
|
|
34
|
-
if (spec.mode === "single") {
|
|
35
|
-
return z.object({
|
|
36
|
-
mode: z.literal("single"),
|
|
37
|
-
keys: createKeyDictionarySchema(spec.argsByKey),
|
|
38
|
-
}) as z.ZodType<NormalizedDictionary>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const namespaceSchemas = Object.fromEntries(
|
|
42
|
-
Object.entries(spec.argsByKey).map(([namespace, argsByKey]) => [
|
|
43
|
-
namespace,
|
|
44
|
-
createKeyDictionarySchema(argsByKey),
|
|
45
|
-
])
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return z.object({
|
|
49
|
-
mode: z.literal("multi"),
|
|
50
|
-
namespaces: z.object(namespaceSchemas),
|
|
51
|
-
}) as z.ZodType<NormalizedDictionary>;
|
|
52
|
-
}
|
package/src/validation/errors.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ValidationIssue, ValidationResult } from "./types.js";
|
|
2
|
-
|
|
3
|
-
function issueMessage(issue: ValidationIssue): string {
|
|
4
|
-
switch (issue.kind) {
|
|
5
|
-
case "missing_key":
|
|
6
|
-
return `Missing required key at ${issue.path.join(".")}`;
|
|
7
|
-
case "unknown_key":
|
|
8
|
-
return `Unknown key at ${issue.path.join(".")}`;
|
|
9
|
-
case "invalid_input":
|
|
10
|
-
case "invalid_locale_value":
|
|
11
|
-
case "icu_syntax_error":
|
|
12
|
-
case "locale_args_mismatch":
|
|
13
|
-
case "variable_mismatch":
|
|
14
|
-
case "variable_type_mismatch":
|
|
15
|
-
return issue.message;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function formatIssues(issues: readonly ValidationIssue[]): string {
|
|
20
|
-
return issues
|
|
21
|
-
.map((issue) => {
|
|
22
|
-
if ("path" in issue && issue.path.length > 0) {
|
|
23
|
-
return `[${issue.path.join(".")}] ${issue.kind}: ${issueMessage(issue)}`;
|
|
24
|
-
}
|
|
25
|
-
return `${issue.kind}: ${issueMessage(issue)}`;
|
|
26
|
-
})
|
|
27
|
-
.join("\n");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class DictionaryValidationError extends Error {
|
|
31
|
-
constructor(public readonly issues: readonly ValidationIssue[]) {
|
|
32
|
-
super(formatIssues(issues));
|
|
33
|
-
this.name = "DictionaryValidationError";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function assertValidDictionary<T>(
|
|
38
|
-
result: ValidationResult<T>
|
|
39
|
-
): asserts result is { ok: true; data: T } {
|
|
40
|
-
if (!result.ok) {
|
|
41
|
-
throw new DictionaryValidationError(result.issues);
|
|
42
|
-
}
|
|
43
|
-
}
|
package/src/validation/index.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External payload validation for server-side codegen/ingest (CMS → authoring files).
|
|
3
|
-
* Not a runtime patch API — validate before writing authoring, then `regenerateNamespaces` / `load()`.
|
|
4
|
-
*/
|
|
5
|
-
import { normalizeDictionaryPartial, normalizeKeyDictionaryPartial } from "./normalize.js";
|
|
6
|
-
import { toDictionary, toNamespaceDictionary } from "./to-dictionary.js";
|
|
7
|
-
import type {
|
|
8
|
-
DictionarySpec,
|
|
9
|
-
NormalizedKeyDictionary,
|
|
10
|
-
ValidationIssue,
|
|
11
|
-
ValidationResult,
|
|
12
|
-
} from "./types.js";
|
|
13
|
-
import { validateNormalizedKeyDictionaryPartial } from "./validate-normalized.js";
|
|
14
|
-
|
|
15
|
-
export function validateExternalDictionaryPartial<TSchema>(
|
|
16
|
-
input: unknown,
|
|
17
|
-
spec: DictionarySpec
|
|
18
|
-
): ValidationResult<Partial<TSchema>> {
|
|
19
|
-
if (spec.mode === "single") {
|
|
20
|
-
const step1 = normalizeKeyDictionaryPartial(input, spec.requiredKeys, spec.argsByKey, []);
|
|
21
|
-
if (!step1.ok) {
|
|
22
|
-
return step1;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const step2 = validateNormalizedKeyDictionaryPartial(step1.data, spec.argsByKey, spec, []);
|
|
26
|
-
if (!step2.ok) {
|
|
27
|
-
return step2;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
ok: true,
|
|
32
|
-
data: toDictionary({ mode: "single", keys: step2.data }) as Partial<TSchema>,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const step1 = normalizeDictionaryPartial(input, spec);
|
|
37
|
-
if (!step1.ok) {
|
|
38
|
-
return step1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (step1.data.mode !== "multi") {
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
issues: [
|
|
45
|
-
{
|
|
46
|
-
kind: "invalid_input",
|
|
47
|
-
message: "validateExternalDictionaryPartial requires multi-mode dictionary spec",
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const issues: ValidationIssue[] = [];
|
|
54
|
-
const namespaces: Record<string, NormalizedKeyDictionary> = {};
|
|
55
|
-
|
|
56
|
-
for (const [namespace, keys] of Object.entries(step1.data.namespaces)) {
|
|
57
|
-
const step2 = validateNormalizedKeyDictionaryPartial(
|
|
58
|
-
keys,
|
|
59
|
-
spec.argsByKey[namespace] ?? {},
|
|
60
|
-
spec,
|
|
61
|
-
[namespace]
|
|
62
|
-
);
|
|
63
|
-
if (!step2.ok) {
|
|
64
|
-
issues.push(...step2.issues);
|
|
65
|
-
} else {
|
|
66
|
-
namespaces[namespace] = step2.data;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (issues.length > 0) {
|
|
71
|
-
return { ok: false, issues };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
data: toDictionary({ mode: "multi", namespaces }) as Partial<TSchema>,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function validateExternalNamespacePartial<TNamespaceSchema>(
|
|
81
|
-
namespace: string,
|
|
82
|
-
input: unknown,
|
|
83
|
-
spec: DictionarySpec
|
|
84
|
-
): ValidationResult<Partial<TNamespaceSchema>> {
|
|
85
|
-
if (spec.mode !== "multi") {
|
|
86
|
-
return {
|
|
87
|
-
ok: false,
|
|
88
|
-
issues: [
|
|
89
|
-
{
|
|
90
|
-
kind: "invalid_input",
|
|
91
|
-
message: "validateExternalNamespacePartial requires multi-mode dictionary spec",
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const knownKeys = spec.requiredKeys[namespace];
|
|
98
|
-
if (!knownKeys) {
|
|
99
|
-
return {
|
|
100
|
-
ok: false,
|
|
101
|
-
issues: [{ kind: "unknown_key", path: [namespace] }],
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const step1 = normalizeKeyDictionaryPartial(input, knownKeys, spec.argsByKey[namespace] ?? {}, [
|
|
106
|
-
namespace,
|
|
107
|
-
]);
|
|
108
|
-
if (!step1.ok) {
|
|
109
|
-
return step1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const step2 = validateNormalizedKeyDictionaryPartial(
|
|
113
|
-
step1.data,
|
|
114
|
-
spec.argsByKey[namespace] ?? {},
|
|
115
|
-
spec,
|
|
116
|
-
[namespace]
|
|
117
|
-
);
|
|
118
|
-
if (!step2.ok) {
|
|
119
|
-
return step2;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
ok: true,
|
|
124
|
-
data: toNamespaceDictionary(
|
|
125
|
-
{ mode: "multi", namespaces: { [namespace]: step2.data } },
|
|
126
|
-
namespace
|
|
127
|
-
) as Partial<TNamespaceSchema>,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function validateExternalKey<TKeySlice>(
|
|
132
|
-
key: string,
|
|
133
|
-
input: unknown,
|
|
134
|
-
spec: Extract<DictionarySpec, { mode: "single" }>
|
|
135
|
-
): ValidationResult<TKeySlice>;
|
|
136
|
-
|
|
137
|
-
export function validateExternalKey<TKeySlice>(
|
|
138
|
-
namespace: string,
|
|
139
|
-
key: string,
|
|
140
|
-
input: unknown,
|
|
141
|
-
spec: Extract<DictionarySpec, { mode: "multi" }>
|
|
142
|
-
): ValidationResult<TKeySlice>;
|
|
143
|
-
|
|
144
|
-
export function validateExternalKey<TKeySlice>(
|
|
145
|
-
namespaceOrKey: string,
|
|
146
|
-
keyOrInput: string | unknown,
|
|
147
|
-
inputOrSpec?: unknown,
|
|
148
|
-
specMaybe?: DictionarySpec
|
|
149
|
-
): ValidationResult<TKeySlice> {
|
|
150
|
-
if (specMaybe?.mode === "multi") {
|
|
151
|
-
const namespace = namespaceOrKey;
|
|
152
|
-
const key = keyOrInput as string;
|
|
153
|
-
const input = inputOrSpec;
|
|
154
|
-
const spec = specMaybe;
|
|
155
|
-
|
|
156
|
-
return validateExternalNamespacePartial<TKeySlice>(
|
|
157
|
-
namespace,
|
|
158
|
-
{ [key]: input },
|
|
159
|
-
spec
|
|
160
|
-
) as ValidationResult<TKeySlice>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const key = namespaceOrKey;
|
|
164
|
-
const input = keyOrInput;
|
|
165
|
-
const spec = inputOrSpec as Extract<DictionarySpec, { mode: "single" }>;
|
|
166
|
-
|
|
167
|
-
return validateExternalDictionaryPartial<TKeySlice>(
|
|
168
|
-
{ [key]: input },
|
|
169
|
-
spec
|
|
170
|
-
) as ValidationResult<TKeySlice>;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export {
|
|
174
|
-
normalizeDictionaryPartial,
|
|
175
|
-
normalizeKeyDictionaryPartial,
|
|
176
|
-
toDictionary,
|
|
177
|
-
toNamespaceDictionary,
|
|
178
|
-
};
|
|
179
|
-
export { validateNormalizedKeyDictionaryPartial } from "./validate-normalized.js";
|
|
180
|
-
export { createArgsSchema } from "./create-args-schema.js";
|
|
181
|
-
export { createNormalizedDictionarySchema } from "./create-normalized-schema.js";
|
|
182
|
-
export { DictionaryValidationError, assertValidDictionary, formatIssues } from "./errors.js";
|
|
183
|
-
export type {
|
|
184
|
-
DictionarySpec,
|
|
185
|
-
NormalizedDictionary,
|
|
186
|
-
NormalizedKeyDictionary,
|
|
187
|
-
NormalizedMultiDictionary,
|
|
188
|
-
ParsedKeyEntry,
|
|
189
|
-
ParsedLocaleEntry,
|
|
190
|
-
ValidationIssue,
|
|
191
|
-
ValidationResult,
|
|
192
|
-
VariableSpec,
|
|
193
|
-
VariableType,
|
|
194
|
-
} from "./types.js";
|
|
195
|
-
|
|
196
|
-
export type { ParseTemplateResult } from "../icu/parse-template.js";
|
|
197
|
-
export { parseTemplate } from "../icu/parse-template.js";
|
|
198
|
-
export {
|
|
199
|
-
extractVariables,
|
|
200
|
-
mergeVariableSpecs,
|
|
201
|
-
variableSpecsEqual,
|
|
202
|
-
} from "../icu/extract-variables.js";
|