@xndrjs/i18n 0.2.0-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 +572 -0
- package/bin/codegen.mjs +17 -0
- package/bin/setup.mjs +14 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +320 -0
- package/dist/index.js.map +1 -0
- package/dist/types-CFWVT2PP.d.ts +77 -0
- package/dist/validation/index.d.ts +44 -0
- package/dist/validation/index.js +490 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +65 -0
- package/src/IcuTranslationProviderMulti.test.ts +290 -0
- package/src/IcuTranslationProviderMulti.ts +195 -0
- package/src/IcuTranslationProviderSingle.test.ts +314 -0
- package/src/IcuTranslationProviderSingle.ts +155 -0
- package/src/codegen/config.ts +63 -0
- package/src/codegen/emit/dictionary-file.ts +73 -0
- package/src/codegen/emit/dictionary-schema-file.ts +129 -0
- package/src/codegen/emit/instance-file.ts +60 -0
- package/src/codegen/emit/namespace-loaders-file.ts +44 -0
- package/src/codegen/emit/types-file.ts +118 -0
- package/src/codegen/generate-i18n-types.test.ts +482 -0
- package/src/codegen/generate-i18n-types.ts +173 -0
- package/src/codegen/icu-analysis.ts +88 -0
- package/src/codegen/locale-fallback.ts +64 -0
- package/src/codegen/paths.ts +27 -0
- package/src/codegen/types.ts +30 -0
- package/src/deep-freeze.test.ts +27 -0
- package/src/deep-freeze.ts +17 -0
- package/src/ensure-namespace.integration.test.ts +66 -0
- package/src/ensure-namespace.test.ts +125 -0
- package/src/ensure-namespace.ts +70 -0
- package/src/icu/extract-variables.ts +87 -0
- package/src/icu/parse-template.ts +24 -0
- package/src/index.ts +32 -0
- package/src/resolve-locale.test.ts +91 -0
- package/src/resolve-locale.ts +88 -0
- package/src/setup/setup-i18n.test.ts +86 -0
- package/src/setup/setup-i18n.ts +179 -0
- package/src/setup/type-names.ts +20 -0
- package/src/types.ts +34 -0
- package/src/validation/create-args-schema.ts +76 -0
- package/src/validation/create-normalized-schema.ts +52 -0
- package/src/validation/errors.ts +41 -0
- package/src/validation/index.ts +90 -0
- package/src/validation/normalize.ts +158 -0
- package/src/validation/to-dictionary.ts +67 -0
- package/src/validation/types.ts +78 -0
- package/src/validation/validate-normalized.ts +91 -0
- package/src/validation/validation.test.ts +265 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalizeDictionary } from "./normalize.js";
|
|
2
|
+
import { toDictionary, toNamespaceDictionary } from "./to-dictionary.js";
|
|
3
|
+
import type { DictionarySpec, NormalizedDictionary, ValidationResult } from "./types.js";
|
|
4
|
+
import { validateNormalizedDictionary } from "./validate-normalized.js";
|
|
5
|
+
|
|
6
|
+
export function validateExternalDictionary<TSchema>(
|
|
7
|
+
input: unknown,
|
|
8
|
+
spec: DictionarySpec
|
|
9
|
+
): ValidationResult<TSchema> {
|
|
10
|
+
const step1 = normalizeDictionary(input, spec);
|
|
11
|
+
if (!step1.ok) {
|
|
12
|
+
return step1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const step2 = validateNormalizedDictionary(step1.data, spec);
|
|
16
|
+
if (!step2.ok) {
|
|
17
|
+
return step2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { ok: true, data: toDictionary(step2.data) as TSchema };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateExternalNamespace<TNamespaceSchema>(
|
|
24
|
+
namespace: string,
|
|
25
|
+
input: unknown,
|
|
26
|
+
spec: DictionarySpec
|
|
27
|
+
): ValidationResult<TNamespaceSchema> {
|
|
28
|
+
if (spec.mode !== "multi") {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
issues: [
|
|
32
|
+
{
|
|
33
|
+
kind: "invalid_input",
|
|
34
|
+
message: "validateExternalNamespace requires multi-mode dictionary spec",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const wrapped = { [namespace]: input };
|
|
41
|
+
const namespaceSpec: DictionarySpec = {
|
|
42
|
+
mode: "multi",
|
|
43
|
+
requiredKeys: {
|
|
44
|
+
[namespace]: spec.requiredKeys[namespace] ?? [],
|
|
45
|
+
},
|
|
46
|
+
argsByKey: {
|
|
47
|
+
[namespace]: spec.argsByKey[namespace] ?? {},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const step1 = normalizeDictionary(wrapped, namespaceSpec);
|
|
52
|
+
if (!step1.ok) {
|
|
53
|
+
return step1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const step2 = validateNormalizedDictionary(step1.data, namespaceSpec);
|
|
57
|
+
if (!step2.ok) {
|
|
58
|
+
return step2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
data: toNamespaceDictionary(step2.data, namespace) as TNamespaceSchema,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { normalizeDictionary, validateNormalizedDictionary, toDictionary, toNamespaceDictionary };
|
|
68
|
+
export { createArgsSchema } from "./create-args-schema.js";
|
|
69
|
+
export { createNormalizedDictionarySchema } from "./create-normalized-schema.js";
|
|
70
|
+
export { DictionaryValidationError, assertValidDictionary, formatIssues } from "./errors.js";
|
|
71
|
+
export type {
|
|
72
|
+
DictionarySpec,
|
|
73
|
+
NormalizedDictionary,
|
|
74
|
+
NormalizedKeyDictionary,
|
|
75
|
+
NormalizedMultiDictionary,
|
|
76
|
+
ParsedKeyEntry,
|
|
77
|
+
ParsedLocaleEntry,
|
|
78
|
+
ValidationIssue,
|
|
79
|
+
ValidationResult,
|
|
80
|
+
VariableSpec,
|
|
81
|
+
VariableType,
|
|
82
|
+
} from "./types.js";
|
|
83
|
+
|
|
84
|
+
export type { ParseTemplateResult } from "../icu/parse-template.js";
|
|
85
|
+
export { parseTemplate } from "../icu/parse-template.js";
|
|
86
|
+
export {
|
|
87
|
+
extractVariables,
|
|
88
|
+
mergeVariableSpecs,
|
|
89
|
+
variableSpecsEqual,
|
|
90
|
+
} from "../icu/extract-variables.js";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { variableSpecsEqual } from "../icu/extract-variables.js";
|
|
2
|
+
import { parseTemplate } from "../icu/parse-template.js";
|
|
3
|
+
import type {
|
|
4
|
+
DictionarySpec,
|
|
5
|
+
NormalizedDictionary,
|
|
6
|
+
NormalizedKeyDictionary,
|
|
7
|
+
ParsedKeyEntry,
|
|
8
|
+
ValidationIssue,
|
|
9
|
+
ValidationResult,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
function failure(issues: ValidationIssue[]): ValidationResult<NormalizedDictionary> {
|
|
13
|
+
return { ok: false, issues };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function success(data: NormalizedDictionary): ValidationResult<NormalizedDictionary> {
|
|
17
|
+
return { ok: true, data };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeKeyDictionary(
|
|
25
|
+
input: Record<string, unknown>,
|
|
26
|
+
requiredKeys: readonly string[],
|
|
27
|
+
keyPathPrefix: readonly string[]
|
|
28
|
+
): ValidationResult<NormalizedKeyDictionary> {
|
|
29
|
+
const issues: ValidationIssue[] = [];
|
|
30
|
+
const keys: Record<string, ParsedKeyEntry> = {};
|
|
31
|
+
|
|
32
|
+
for (const key of requiredKeys) {
|
|
33
|
+
if (!(key in input)) {
|
|
34
|
+
issues.push({ kind: "missing_key", path: [...keyPathPrefix, key] });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const keyValue = input[key];
|
|
39
|
+
if (!isPlainObject(keyValue)) {
|
|
40
|
+
issues.push({
|
|
41
|
+
kind: "invalid_input",
|
|
42
|
+
message: `Expected object for key "${key}", got ${typeof keyValue}`,
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const locales: Record<string, ParsedKeyEntry["locales"][string]> = {};
|
|
48
|
+
let mergedArgs: ParsedKeyEntry["mergedArgs"] = {};
|
|
49
|
+
let hasLocaleArgs = false;
|
|
50
|
+
|
|
51
|
+
for (const [locale, template] of Object.entries(keyValue)) {
|
|
52
|
+
const localePath = [...keyPathPrefix, key, locale];
|
|
53
|
+
|
|
54
|
+
if (typeof template !== "string") {
|
|
55
|
+
issues.push({
|
|
56
|
+
kind: "invalid_locale_value",
|
|
57
|
+
path: localePath,
|
|
58
|
+
message: `expected string, got ${typeof template}`,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parsed = parseTemplate(template);
|
|
64
|
+
if (!parsed.ok) {
|
|
65
|
+
issues.push({
|
|
66
|
+
kind: "icu_syntax_error",
|
|
67
|
+
path: localePath,
|
|
68
|
+
message: parsed.message,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
locales[locale] = { template, args: parsed.args };
|
|
74
|
+
|
|
75
|
+
if (!hasLocaleArgs) {
|
|
76
|
+
mergedArgs = { ...parsed.args };
|
|
77
|
+
hasLocaleArgs = true;
|
|
78
|
+
} else if (!variableSpecsEqual(mergedArgs, parsed.args)) {
|
|
79
|
+
const localeArgs: Record<string, ParsedKeyEntry["mergedArgs"]> = {};
|
|
80
|
+
for (const [loc, entry] of Object.entries(locales)) {
|
|
81
|
+
localeArgs[loc] = entry.args;
|
|
82
|
+
}
|
|
83
|
+
issues.push({
|
|
84
|
+
kind: "locale_args_mismatch",
|
|
85
|
+
path: [...keyPathPrefix, key],
|
|
86
|
+
locales: localeArgs,
|
|
87
|
+
message: `Inconsistent ICU variables across locales for key "${key}"`,
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
keys[key] = { locales, mergedArgs };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (issues.length > 0) {
|
|
97
|
+
return { ok: false, issues };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { ok: true, data: keys };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeDictionary(
|
|
104
|
+
input: unknown,
|
|
105
|
+
spec: DictionarySpec
|
|
106
|
+
): ValidationResult<NormalizedDictionary> {
|
|
107
|
+
if (!isPlainObject(input)) {
|
|
108
|
+
return failure([
|
|
109
|
+
{
|
|
110
|
+
kind: "invalid_input",
|
|
111
|
+
message: `Expected object, got ${input === null ? "null" : typeof input}`,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (spec.mode === "single") {
|
|
117
|
+
const result = normalizeKeyDictionary(input, spec.requiredKeys, []);
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return success({ mode: "single", keys: result.data });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const issues: ValidationIssue[] = [];
|
|
126
|
+
const namespaces: Record<string, NormalizedKeyDictionary> = {};
|
|
127
|
+
|
|
128
|
+
for (const [namespace, requiredKeys] of Object.entries(spec.requiredKeys)) {
|
|
129
|
+
if (!(namespace in input)) {
|
|
130
|
+
for (const key of requiredKeys) {
|
|
131
|
+
issues.push({ kind: "missing_key", path: [namespace, key] });
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const namespaceValue = input[namespace];
|
|
137
|
+
if (!isPlainObject(namespaceValue)) {
|
|
138
|
+
issues.push({
|
|
139
|
+
kind: "invalid_input",
|
|
140
|
+
message: `Expected object for namespace "${namespace}", got ${typeof namespaceValue}`,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = normalizeKeyDictionary(namespaceValue, requiredKeys, [namespace]);
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
issues.push(...result.issues);
|
|
148
|
+
} else {
|
|
149
|
+
namespaces[namespace] = result.data;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (issues.length > 0) {
|
|
154
|
+
return failure(issues);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return success({ mode: "multi", namespaces });
|
|
158
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { NormalizedDictionary } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type LocaleDictionary = Record<string, string>;
|
|
4
|
+
type KeyDictionary = Record<string, LocaleDictionary>;
|
|
5
|
+
type MultiDictionary = Record<string, KeyDictionary>;
|
|
6
|
+
|
|
7
|
+
export function toDictionary(normalized: NormalizedDictionary): KeyDictionary | MultiDictionary {
|
|
8
|
+
if (normalized.mode === "single") {
|
|
9
|
+
const dictionary: KeyDictionary = {};
|
|
10
|
+
|
|
11
|
+
for (const [key, entry] of Object.entries(normalized.keys)) {
|
|
12
|
+
const locales: LocaleDictionary = {};
|
|
13
|
+
for (const [locale, localeEntry] of Object.entries(entry.locales)) {
|
|
14
|
+
locales[locale] = localeEntry.template;
|
|
15
|
+
}
|
|
16
|
+
dictionary[key] = locales;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return dictionary;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const dictionary: MultiDictionary = {};
|
|
23
|
+
|
|
24
|
+
for (const [namespace, keys] of Object.entries(normalized.namespaces)) {
|
|
25
|
+
const namespaceDictionary: KeyDictionary = {};
|
|
26
|
+
|
|
27
|
+
for (const [key, entry] of Object.entries(keys)) {
|
|
28
|
+
const locales: LocaleDictionary = {};
|
|
29
|
+
for (const [locale, localeEntry] of Object.entries(entry.locales)) {
|
|
30
|
+
locales[locale] = localeEntry.template;
|
|
31
|
+
}
|
|
32
|
+
namespaceDictionary[key] = locales;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dictionary[namespace] = namespaceDictionary;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return dictionary;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toNamespaceDictionary(
|
|
42
|
+
normalized: NormalizedDictionary,
|
|
43
|
+
namespace: string
|
|
44
|
+
): KeyDictionary {
|
|
45
|
+
if (normalized.mode !== "multi") {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`[i18n] toNamespaceDictionary requires multi-mode normalized dictionary, got "${normalized.mode}"`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const keys = normalized.namespaces[namespace];
|
|
52
|
+
if (!keys) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const dictionary: KeyDictionary = {};
|
|
57
|
+
|
|
58
|
+
for (const [key, entry] of Object.entries(keys)) {
|
|
59
|
+
const locales: LocaleDictionary = {};
|
|
60
|
+
for (const [locale, localeEntry] of Object.entries(entry.locales)) {
|
|
61
|
+
locales[locale] = localeEntry.template;
|
|
62
|
+
}
|
|
63
|
+
dictionary[key] = locales;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return dictionary;
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { VariableSpec, VariableType } from "../icu/extract-variables.js";
|
|
2
|
+
|
|
3
|
+
export type { VariableSpec, VariableType };
|
|
4
|
+
|
|
5
|
+
export type ParsedLocaleEntry = {
|
|
6
|
+
readonly template: string;
|
|
7
|
+
readonly args: VariableSpec;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ParsedKeyEntry = {
|
|
11
|
+
readonly locales: Readonly<Record<string, ParsedLocaleEntry>>;
|
|
12
|
+
readonly mergedArgs: VariableSpec;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NormalizedKeyDictionary = Readonly<Record<string, ParsedKeyEntry>>;
|
|
16
|
+
|
|
17
|
+
export type NormalizedMultiDictionary = Readonly<Record<string, NormalizedKeyDictionary>>;
|
|
18
|
+
|
|
19
|
+
export type NormalizedDictionary =
|
|
20
|
+
| { readonly mode: "single"; readonly keys: NormalizedKeyDictionary }
|
|
21
|
+
| { readonly mode: "multi"; readonly namespaces: NormalizedMultiDictionary };
|
|
22
|
+
|
|
23
|
+
export type DictionarySpec =
|
|
24
|
+
| {
|
|
25
|
+
readonly mode: "single";
|
|
26
|
+
readonly requiredKeys: readonly string[];
|
|
27
|
+
readonly argsByKey: Readonly<Record<string, VariableSpec>>;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
readonly mode: "multi";
|
|
31
|
+
readonly requiredKeys: Readonly<Record<string, readonly string[]>>;
|
|
32
|
+
readonly argsByKey: Readonly<Record<string, Readonly<Record<string, VariableSpec>>>>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ValidationIssue =
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: "invalid_input";
|
|
38
|
+
readonly message: string;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: "missing_key";
|
|
42
|
+
readonly path: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
readonly kind: "invalid_locale_value";
|
|
46
|
+
readonly path: readonly string[];
|
|
47
|
+
readonly message: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: "icu_syntax_error";
|
|
51
|
+
readonly path: readonly string[];
|
|
52
|
+
readonly message: string;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: "locale_args_mismatch";
|
|
56
|
+
readonly path: readonly string[];
|
|
57
|
+
readonly locales: Readonly<Record<string, VariableSpec>>;
|
|
58
|
+
readonly message: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: "variable_mismatch";
|
|
62
|
+
readonly path: readonly string[];
|
|
63
|
+
readonly expected: VariableSpec;
|
|
64
|
+
readonly found: VariableSpec;
|
|
65
|
+
readonly message: string;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: "variable_type_mismatch";
|
|
69
|
+
readonly path: readonly string[];
|
|
70
|
+
readonly variable: string;
|
|
71
|
+
readonly expected: VariableType;
|
|
72
|
+
readonly found: VariableType;
|
|
73
|
+
readonly message: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ValidationResult<T> =
|
|
77
|
+
| { readonly ok: true; readonly data: T }
|
|
78
|
+
| { readonly ok: false; readonly issues: readonly ValidationIssue[] };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ZodError } from "zod";
|
|
2
|
+
import { mapArgsSchemaError } from "./create-args-schema.js";
|
|
3
|
+
import { createNormalizedDictionarySchema } from "./create-normalized-schema.js";
|
|
4
|
+
import type {
|
|
5
|
+
DictionarySpec,
|
|
6
|
+
NormalizedDictionary,
|
|
7
|
+
ValidationIssue,
|
|
8
|
+
ValidationResult,
|
|
9
|
+
VariableSpec,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
function getExpectedArgs(spec: DictionarySpec, path: readonly string[]): VariableSpec | undefined {
|
|
13
|
+
if (spec.mode === "single") {
|
|
14
|
+
const key = path[path.length - 1];
|
|
15
|
+
if (typeof key !== "string") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return spec.argsByKey[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const namespace = path[0];
|
|
22
|
+
const key = path[path.length - 1];
|
|
23
|
+
if (typeof namespace !== "string" || typeof key !== "string") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return spec.argsByKey[namespace]?.[key];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getFoundArgs(
|
|
31
|
+
normalized: NormalizedDictionary,
|
|
32
|
+
path: readonly string[]
|
|
33
|
+
): VariableSpec | undefined {
|
|
34
|
+
if (normalized.mode === "single") {
|
|
35
|
+
const key = path[path.length - 1];
|
|
36
|
+
if (typeof key !== "string") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return normalized.keys[key]?.mergedArgs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const namespace = path[0];
|
|
43
|
+
const key = path[path.length - 1];
|
|
44
|
+
if (typeof namespace !== "string" || typeof key !== "string") {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return normalized.namespaces[namespace]?.[key]?.mergedArgs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mapZodError(
|
|
52
|
+
error: ZodError,
|
|
53
|
+
spec: DictionarySpec,
|
|
54
|
+
normalized: NormalizedDictionary
|
|
55
|
+
): ValidationIssue[] {
|
|
56
|
+
return error.issues.map((issue) => {
|
|
57
|
+
const path = issue.path.map(String);
|
|
58
|
+
|
|
59
|
+
if (path.includes("mergedArgs")) {
|
|
60
|
+
const keyPath = path.slice(0, path.indexOf("mergedArgs"));
|
|
61
|
+
const expected = getExpectedArgs(spec, keyPath);
|
|
62
|
+
const found = getFoundArgs(normalized, keyPath);
|
|
63
|
+
|
|
64
|
+
if (expected && found) {
|
|
65
|
+
return mapArgsSchemaError(expected, found, issue.message, keyPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
kind: "invalid_input" as const,
|
|
71
|
+
message: path.length > 0 ? `${path.join(".")}: ${issue.message}` : issue.message,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateNormalizedDictionary(
|
|
77
|
+
normalized: NormalizedDictionary,
|
|
78
|
+
spec: DictionarySpec
|
|
79
|
+
): ValidationResult<NormalizedDictionary> {
|
|
80
|
+
const schema = createNormalizedDictionarySchema(spec);
|
|
81
|
+
const result = schema.safeParse(normalized);
|
|
82
|
+
|
|
83
|
+
if (result.success) {
|
|
84
|
+
return { ok: true, data: result.data };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
issues: mapZodError(result.error, spec, normalized),
|
|
90
|
+
};
|
|
91
|
+
}
|