@xndrjs/i18n 0.6.0 → 0.7.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 +154 -134
- package/dist/codegen/index.js.map +1 -1
- package/dist/index.d.ts +276 -61
- package/dist/index.js +687 -126
- package/dist/index.js.map +1 -1
- package/dist/validation/index.d.ts +23 -7
- package/dist/validation/index.js +196 -89
- package/dist/validation/index.js.map +1 -1
- package/package.json +1 -1
- package/src/IcuTranslationProviderMulti.test.ts +115 -215
- package/src/IcuTranslationProviderMulti.ts +107 -96
- package/src/IcuTranslationProviderSingle.test.ts +36 -294
- package/src/IcuTranslationProviderSingle.ts +55 -72
- package/src/builder-load-registry.ts +18 -0
- package/src/builder-loaders.ts +18 -0
- package/src/builder-multi.ts +481 -0
- package/src/builder-types.test.ts +80 -0
- package/src/builder-types.ts +24 -0
- package/src/builder.test.ts +421 -0
- package/src/builder.ts +112 -0
- package/src/codegen/delivery-artifacts.test.ts +2 -1
- package/src/codegen/delivery-artifacts.ts +2 -1
- package/src/codegen/emit/dictionary-file.test.ts +0 -7
- package/src/codegen/emit/dictionary-schema-file.ts +55 -42
- package/src/codegen/emit/instance-file.test.ts +88 -0
- package/src/codegen/emit/instance-file.ts +164 -17
- package/src/codegen/emit/namespace-loaders-file.test.ts +35 -10
- package/src/codegen/emit/namespace-loaders-file.ts +11 -61
- package/src/codegen/emit/types-file.test.ts +0 -2
- package/src/codegen/generate-i18n-types.test.ts +8 -303
- package/src/codegen/generate-i18n-types.ts +16 -1
- package/src/codegen/project-locales-set-namespace.test.ts +25 -32
- package/src/engine.ts +71 -0
- package/src/index.ts +47 -7
- package/src/patch-key.test.ts +186 -0
- package/src/patch-key.ts +140 -0
- package/src/project-locales.test.ts +76 -0
- package/src/project-locales.ts +58 -1
- package/src/scope-multi.ts +124 -0
- package/src/scope-single.ts +85 -0
- package/src/scope-types.ts +27 -0
- package/src/scope.test.ts +481 -0
- package/src/single-builder.ts +153 -0
- package/src/types.ts +16 -0
- package/src/validation/create-normalized-schema.ts +1 -1
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +135 -27
- package/src/validation/normalize.ts +176 -46
- package/src/validation/types.ts +4 -0
- package/src/validation/validate-normalized.ts +43 -1
- package/src/validation/validation.test.ts +200 -7
package/src/validation/index.ts
CHANGED
|
@@ -1,70 +1,178 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeDictionaryPartial, normalizeKeyDictionaryPartial } from "./normalize.js";
|
|
2
2
|
import { toDictionary, toNamespaceDictionary } from "./to-dictionary.js";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
DictionarySpec,
|
|
5
|
+
NormalizedKeyDictionary,
|
|
6
|
+
ValidationIssue,
|
|
7
|
+
ValidationResult,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { validateNormalizedKeyDictionaryPartial } from "./validate-normalized.js";
|
|
5
10
|
|
|
6
|
-
export function
|
|
11
|
+
export function validateExternalDictionaryPartial<TSchema>(
|
|
7
12
|
input: unknown,
|
|
8
13
|
spec: DictionarySpec
|
|
9
|
-
): ValidationResult<TSchema
|
|
10
|
-
|
|
14
|
+
): ValidationResult<Partial<TSchema>> {
|
|
15
|
+
if (spec.mode === "single") {
|
|
16
|
+
const step1 = normalizeKeyDictionaryPartial(input, spec.requiredKeys, spec.argsByKey, []);
|
|
17
|
+
if (!step1.ok) {
|
|
18
|
+
return step1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const step2 = validateNormalizedKeyDictionaryPartial(step1.data, spec.argsByKey, spec, []);
|
|
22
|
+
if (!step2.ok) {
|
|
23
|
+
return step2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
data: toDictionary({ mode: "single", keys: step2.data }) as Partial<TSchema>,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const step1 = normalizeDictionaryPartial(input, spec);
|
|
11
33
|
if (!step1.ok) {
|
|
12
34
|
return step1;
|
|
13
35
|
}
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
if (step1.data.mode !== "multi") {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
issues: [
|
|
41
|
+
{
|
|
42
|
+
kind: "invalid_input",
|
|
43
|
+
message: "validateExternalDictionaryPartial requires multi-mode dictionary spec",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const issues: ValidationIssue[] = [];
|
|
50
|
+
const namespaces: Record<string, NormalizedKeyDictionary> = {};
|
|
51
|
+
|
|
52
|
+
for (const [namespace, keys] of Object.entries(step1.data.namespaces)) {
|
|
53
|
+
const step2 = validateNormalizedKeyDictionaryPartial(
|
|
54
|
+
keys,
|
|
55
|
+
spec.argsByKey[namespace] ?? {},
|
|
56
|
+
spec,
|
|
57
|
+
[namespace]
|
|
58
|
+
);
|
|
59
|
+
if (!step2.ok) {
|
|
60
|
+
issues.push(...step2.issues);
|
|
61
|
+
} else {
|
|
62
|
+
namespaces[namespace] = step2.data;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (issues.length > 0) {
|
|
67
|
+
return { ok: false, issues };
|
|
18
68
|
}
|
|
19
69
|
|
|
20
|
-
return {
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
data: toDictionary({ mode: "multi", namespaces }) as Partial<TSchema>,
|
|
73
|
+
};
|
|
21
74
|
}
|
|
22
75
|
|
|
23
|
-
export function
|
|
76
|
+
export function validateExternalNamespacePartial<TNamespaceSchema>(
|
|
24
77
|
namespace: string,
|
|
25
78
|
input: unknown,
|
|
26
79
|
spec: DictionarySpec
|
|
27
|
-
): ValidationResult<TNamespaceSchema
|
|
80
|
+
): ValidationResult<Partial<TNamespaceSchema>> {
|
|
28
81
|
if (spec.mode !== "multi") {
|
|
29
82
|
return {
|
|
30
83
|
ok: false,
|
|
31
84
|
issues: [
|
|
32
85
|
{
|
|
33
86
|
kind: "invalid_input",
|
|
34
|
-
message: "
|
|
87
|
+
message: "validateExternalNamespacePartial requires multi-mode dictionary spec",
|
|
35
88
|
},
|
|
36
89
|
],
|
|
37
90
|
};
|
|
38
91
|
}
|
|
39
92
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
[
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
[namespace]: spec.argsByKey[namespace] ?? {},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
93
|
+
const knownKeys = spec.requiredKeys[namespace];
|
|
94
|
+
if (!knownKeys) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
issues: [{ kind: "unknown_key", path: [namespace] }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
50
100
|
|
|
51
|
-
const step1 =
|
|
101
|
+
const step1 = normalizeKeyDictionaryPartial(input, knownKeys, spec.argsByKey[namespace] ?? {}, [
|
|
102
|
+
namespace,
|
|
103
|
+
]);
|
|
52
104
|
if (!step1.ok) {
|
|
53
105
|
return step1;
|
|
54
106
|
}
|
|
55
107
|
|
|
56
|
-
const step2 =
|
|
108
|
+
const step2 = validateNormalizedKeyDictionaryPartial(
|
|
109
|
+
step1.data,
|
|
110
|
+
spec.argsByKey[namespace] ?? {},
|
|
111
|
+
spec,
|
|
112
|
+
[namespace]
|
|
113
|
+
);
|
|
57
114
|
if (!step2.ok) {
|
|
58
115
|
return step2;
|
|
59
116
|
}
|
|
60
117
|
|
|
61
118
|
return {
|
|
62
119
|
ok: true,
|
|
63
|
-
data: toNamespaceDictionary(
|
|
120
|
+
data: toNamespaceDictionary(
|
|
121
|
+
{ mode: "multi", namespaces: { [namespace]: step2.data } },
|
|
122
|
+
namespace
|
|
123
|
+
) as Partial<TNamespaceSchema>,
|
|
64
124
|
};
|
|
65
125
|
}
|
|
66
126
|
|
|
67
|
-
export
|
|
127
|
+
export function validateExternalKey<TKeySlice>(
|
|
128
|
+
key: string,
|
|
129
|
+
input: unknown,
|
|
130
|
+
spec: Extract<DictionarySpec, { mode: "single" }>
|
|
131
|
+
): ValidationResult<TKeySlice>;
|
|
132
|
+
|
|
133
|
+
export function validateExternalKey<TKeySlice>(
|
|
134
|
+
namespace: string,
|
|
135
|
+
key: string,
|
|
136
|
+
input: unknown,
|
|
137
|
+
spec: Extract<DictionarySpec, { mode: "multi" }>
|
|
138
|
+
): ValidationResult<TKeySlice>;
|
|
139
|
+
|
|
140
|
+
export function validateExternalKey<TKeySlice>(
|
|
141
|
+
namespaceOrKey: string,
|
|
142
|
+
keyOrInput: string | unknown,
|
|
143
|
+
inputOrSpec?: unknown,
|
|
144
|
+
specMaybe?: DictionarySpec
|
|
145
|
+
): ValidationResult<TKeySlice> {
|
|
146
|
+
if (specMaybe?.mode === "multi") {
|
|
147
|
+
const namespace = namespaceOrKey;
|
|
148
|
+
const key = keyOrInput as string;
|
|
149
|
+
const input = inputOrSpec;
|
|
150
|
+
const spec = specMaybe;
|
|
151
|
+
|
|
152
|
+
return validateExternalNamespacePartial<TKeySlice>(
|
|
153
|
+
namespace,
|
|
154
|
+
{ [key]: input },
|
|
155
|
+
spec
|
|
156
|
+
) as ValidationResult<TKeySlice>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const key = namespaceOrKey;
|
|
160
|
+
const input = keyOrInput;
|
|
161
|
+
const spec = inputOrSpec as Extract<DictionarySpec, { mode: "single" }>;
|
|
162
|
+
|
|
163
|
+
return validateExternalDictionaryPartial<TKeySlice>(
|
|
164
|
+
{ [key]: input },
|
|
165
|
+
spec
|
|
166
|
+
) as ValidationResult<TKeySlice>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
normalizeDictionaryPartial,
|
|
171
|
+
normalizeKeyDictionaryPartial,
|
|
172
|
+
toDictionary,
|
|
173
|
+
toNamespaceDictionary,
|
|
174
|
+
};
|
|
175
|
+
export { validateNormalizedKeyDictionaryPartial } from "./validate-normalized.js";
|
|
68
176
|
export { createArgsSchema } from "./create-args-schema.js";
|
|
69
177
|
export { createNormalizedDictionarySchema } from "./create-normalized-schema.js";
|
|
70
178
|
export { DictionaryValidationError, assertValidDictionary, formatIssues } from "./errors.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mergeVariableMetaAcrossLocales, type VariableMetaSpec } from "../icu/extract-variables.js";
|
|
2
2
|
import { parseTemplate } from "../icu/parse-template.js";
|
|
3
|
+
import type { VariableSpec } from "../icu/extract-variables.js";
|
|
3
4
|
import type {
|
|
4
5
|
DictionarySpec,
|
|
5
6
|
NormalizedDictionary,
|
|
@@ -21,6 +22,68 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
21
22
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function normalizeSingleKey(
|
|
26
|
+
key: string,
|
|
27
|
+
keyValue: Record<string, unknown>,
|
|
28
|
+
keyPathPrefix: readonly string[]
|
|
29
|
+
): { entry: ParsedKeyEntry; issues: ValidationIssue[] } {
|
|
30
|
+
const issues: ValidationIssue[] = [];
|
|
31
|
+
const locales: Record<string, ParsedKeyEntry["locales"][string]> = {};
|
|
32
|
+
const localeMetas: VariableMetaSpec[] = [];
|
|
33
|
+
let mergedArgs: ParsedKeyEntry["mergedArgs"] = {};
|
|
34
|
+
let localeArgsMismatch: ValidationIssue | undefined;
|
|
35
|
+
|
|
36
|
+
for (const [locale, template] of Object.entries(keyValue)) {
|
|
37
|
+
const localePath = [...keyPathPrefix, key, locale];
|
|
38
|
+
|
|
39
|
+
if (typeof template !== "string") {
|
|
40
|
+
issues.push({
|
|
41
|
+
kind: "invalid_locale_value",
|
|
42
|
+
path: localePath,
|
|
43
|
+
message: `expected string, got ${typeof template}`,
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = parseTemplate(template);
|
|
49
|
+
if (!parsed.ok) {
|
|
50
|
+
issues.push({
|
|
51
|
+
kind: "icu_syntax_error",
|
|
52
|
+
path: localePath,
|
|
53
|
+
message: parsed.message,
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
locales[locale] = { template, args: parsed.args };
|
|
59
|
+
localeMetas.push(parsed.meta);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (localeMetas.length > 0) {
|
|
63
|
+
const merged = mergeVariableMetaAcrossLocales(localeMetas);
|
|
64
|
+
if (!merged.ok) {
|
|
65
|
+
const localeArgs: Record<string, ParsedKeyEntry["mergedArgs"]> = {};
|
|
66
|
+
for (const [loc, entry] of Object.entries(locales)) {
|
|
67
|
+
localeArgs[loc] = entry.args;
|
|
68
|
+
}
|
|
69
|
+
localeArgsMismatch = {
|
|
70
|
+
kind: "locale_args_mismatch",
|
|
71
|
+
path: [...keyPathPrefix, key],
|
|
72
|
+
locales: localeArgs,
|
|
73
|
+
message: merged.message,
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
mergedArgs = merged.merged;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (localeArgsMismatch) {
|
|
81
|
+
issues.push(localeArgsMismatch);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { entry: { locales, mergedArgs }, issues };
|
|
85
|
+
}
|
|
86
|
+
|
|
24
87
|
function normalizeKeyDictionary(
|
|
25
88
|
input: Record<string, unknown>,
|
|
26
89
|
requiredKeys: readonly string[],
|
|
@@ -44,60 +107,70 @@ function normalizeKeyDictionary(
|
|
|
44
107
|
continue;
|
|
45
108
|
}
|
|
46
109
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const [locale, template] of Object.entries(keyValue)) {
|
|
53
|
-
const localePath = [...keyPathPrefix, key, locale];
|
|
54
|
-
|
|
55
|
-
if (typeof template !== "string") {
|
|
56
|
-
issues.push({
|
|
57
|
-
kind: "invalid_locale_value",
|
|
58
|
-
path: localePath,
|
|
59
|
-
message: `expected string, got ${typeof template}`,
|
|
60
|
-
});
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
110
|
+
const result = normalizeSingleKey(key, keyValue, keyPathPrefix);
|
|
111
|
+
issues.push(...result.issues);
|
|
112
|
+
keys[key] = result.entry;
|
|
113
|
+
}
|
|
63
114
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
kind: "icu_syntax_error",
|
|
68
|
-
path: localePath,
|
|
69
|
-
message: parsed.message,
|
|
70
|
-
});
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
115
|
+
if (issues.length > 0) {
|
|
116
|
+
return { ok: false, issues };
|
|
117
|
+
}
|
|
73
118
|
|
|
74
|
-
|
|
75
|
-
|
|
119
|
+
return { ok: true, data: keys };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validation step 1 (partial): coerce external input into a normalized key dictionary
|
|
124
|
+
* for only the keys present in the payload.
|
|
125
|
+
*/
|
|
126
|
+
export function normalizeKeyDictionaryPartial(
|
|
127
|
+
input: unknown,
|
|
128
|
+
knownKeys: readonly string[],
|
|
129
|
+
_argsByKey: Readonly<Record<string, VariableSpec>>,
|
|
130
|
+
keyPathPrefix: readonly string[]
|
|
131
|
+
): ValidationResult<NormalizedKeyDictionary> {
|
|
132
|
+
if (!isPlainObject(input)) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
issues: [
|
|
136
|
+
{
|
|
137
|
+
kind: "invalid_input",
|
|
138
|
+
message: `Expected object, got ${input === null ? "null" : typeof input}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const knownKeysSet = new Set(knownKeys);
|
|
145
|
+
const issues: ValidationIssue[] = [];
|
|
146
|
+
const keys: Record<string, ParsedKeyEntry> = {};
|
|
147
|
+
|
|
148
|
+
for (const key of Object.keys(input)) {
|
|
149
|
+
if (!knownKeysSet.has(key)) {
|
|
150
|
+
issues.push({ kind: "unknown_key", path: [...keyPathPrefix, key] });
|
|
151
|
+
continue;
|
|
76
152
|
}
|
|
77
153
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
localeArgsMismatch = {
|
|
86
|
-
kind: "locale_args_mismatch",
|
|
87
|
-
path: [...keyPathPrefix, key],
|
|
88
|
-
locales: localeArgs,
|
|
89
|
-
message: merged.message,
|
|
90
|
-
};
|
|
91
|
-
} else {
|
|
92
|
-
mergedArgs = merged.merged;
|
|
93
|
-
}
|
|
154
|
+
const keyValue = input[key];
|
|
155
|
+
if (!isPlainObject(keyValue)) {
|
|
156
|
+
issues.push({
|
|
157
|
+
kind: "invalid_input",
|
|
158
|
+
message: `Expected object for key "${key}", got ${typeof keyValue}`,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
|
-
if (
|
|
97
|
-
issues.push(
|
|
163
|
+
if (Object.keys(keyValue).length === 0) {
|
|
164
|
+
issues.push({
|
|
165
|
+
kind: "invalid_input",
|
|
166
|
+
message: `Key "${key}" must include at least one locale`,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
98
169
|
}
|
|
99
170
|
|
|
100
|
-
|
|
171
|
+
const result = normalizeSingleKey(key, keyValue, keyPathPrefix);
|
|
172
|
+
issues.push(...result.issues);
|
|
173
|
+
keys[key] = result.entry;
|
|
101
174
|
}
|
|
102
175
|
|
|
103
176
|
if (issues.length > 0) {
|
|
@@ -167,3 +240,60 @@ export function normalizeDictionary(
|
|
|
167
240
|
|
|
168
241
|
return success({ mode: "multi", namespaces });
|
|
169
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validation step 1 (partial, multi): coerce external input into normalized namespace
|
|
246
|
+
* slices for only the namespaces and keys present in the payload.
|
|
247
|
+
*/
|
|
248
|
+
export function normalizeDictionaryPartial(
|
|
249
|
+
input: unknown,
|
|
250
|
+
spec: Extract<DictionarySpec, { mode: "multi" }>
|
|
251
|
+
): ValidationResult<NormalizedDictionary> {
|
|
252
|
+
if (!isPlainObject(input)) {
|
|
253
|
+
return failure([
|
|
254
|
+
{
|
|
255
|
+
kind: "invalid_input",
|
|
256
|
+
message: `Expected object, got ${input === null ? "null" : typeof input}`,
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const knownNamespaces = Object.keys(spec.requiredKeys);
|
|
262
|
+
const knownNamespacesSet = new Set(knownNamespaces);
|
|
263
|
+
const issues: ValidationIssue[] = [];
|
|
264
|
+
const namespaces: Record<string, NormalizedKeyDictionary> = {};
|
|
265
|
+
|
|
266
|
+
for (const namespace of Object.keys(input)) {
|
|
267
|
+
if (!knownNamespacesSet.has(namespace)) {
|
|
268
|
+
issues.push({ kind: "unknown_key", path: [namespace] });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const namespaceValue = input[namespace];
|
|
273
|
+
if (!isPlainObject(namespaceValue)) {
|
|
274
|
+
issues.push({
|
|
275
|
+
kind: "invalid_input",
|
|
276
|
+
message: `Expected object for namespace "${namespace}", got ${typeof namespaceValue}`,
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result = normalizeKeyDictionaryPartial(
|
|
282
|
+
namespaceValue,
|
|
283
|
+
spec.requiredKeys[namespace] ?? [],
|
|
284
|
+
spec.argsByKey[namespace] ?? {},
|
|
285
|
+
[namespace]
|
|
286
|
+
);
|
|
287
|
+
if (!result.ok) {
|
|
288
|
+
issues.push(...result.issues);
|
|
289
|
+
} else {
|
|
290
|
+
namespaces[namespace] = result.data;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (issues.length > 0) {
|
|
295
|
+
return failure(issues);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return success({ mode: "multi", namespaces });
|
|
299
|
+
}
|
package/src/validation/types.ts
CHANGED
|
@@ -41,6 +41,10 @@ export type ValidationIssue =
|
|
|
41
41
|
readonly kind: "missing_key";
|
|
42
42
|
readonly path: readonly string[];
|
|
43
43
|
}
|
|
44
|
+
| {
|
|
45
|
+
readonly kind: "unknown_key";
|
|
46
|
+
readonly path: readonly string[];
|
|
47
|
+
}
|
|
44
48
|
| {
|
|
45
49
|
readonly kind: "invalid_locale_value";
|
|
46
50
|
readonly path: readonly string[];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { ZodError } from "zod";
|
|
2
2
|
import { mapArgsSchemaError } from "./create-args-schema.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createKeyDictionarySchema,
|
|
5
|
+
createNormalizedDictionarySchema,
|
|
6
|
+
} from "./create-normalized-schema.js";
|
|
4
7
|
import type {
|
|
5
8
|
DictionarySpec,
|
|
6
9
|
NormalizedDictionary,
|
|
10
|
+
NormalizedKeyDictionary,
|
|
7
11
|
ValidationIssue,
|
|
8
12
|
ValidationResult,
|
|
9
13
|
VariableSpec,
|
|
@@ -73,6 +77,44 @@ function mapZodError(
|
|
|
73
77
|
});
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
export function validateNormalizedKeyDictionaryPartial(
|
|
81
|
+
keys: NormalizedKeyDictionary,
|
|
82
|
+
argsByKey: Readonly<Record<string, VariableSpec>>,
|
|
83
|
+
spec: DictionarySpec,
|
|
84
|
+
keyPathPrefix: readonly string[]
|
|
85
|
+
): ValidationResult<NormalizedKeyDictionary> {
|
|
86
|
+
const presentKeys = Object.keys(keys);
|
|
87
|
+
const subsetArgsByKey = Object.fromEntries(
|
|
88
|
+
presentKeys.filter((key) => key in argsByKey).map((key) => [key, argsByKey[key]!])
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (presentKeys.length === 0) {
|
|
92
|
+
return { ok: true, data: keys };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const schema = createKeyDictionarySchema(subsetArgsByKey);
|
|
96
|
+
const result = schema.safeParse(keys);
|
|
97
|
+
|
|
98
|
+
if (result.success) {
|
|
99
|
+
return { ok: true, data: result.data };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const normalized: NormalizedDictionary =
|
|
103
|
+
spec.mode === "single"
|
|
104
|
+
? { mode: "single", keys }
|
|
105
|
+
: {
|
|
106
|
+
mode: "multi",
|
|
107
|
+
namespaces: {
|
|
108
|
+
[keyPathPrefix[0] ?? ""]: keys,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
issues: mapZodError(result.error, spec, normalized),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
76
118
|
export function validateNormalizedDictionary(
|
|
77
119
|
normalized: NormalizedDictionary,
|
|
78
120
|
spec: DictionarySpec
|