@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,129 @@
|
|
|
1
|
+
import type { VariableSpec } from "../../icu/extract-variables.js";
|
|
2
|
+
import { GENERATED_FILE_BANNER } from "../paths.js";
|
|
3
|
+
import type { NamespaceEntry } from "../types.js";
|
|
4
|
+
|
|
5
|
+
function formatVariableSpecObject(spec: VariableSpec): string {
|
|
6
|
+
const entries = Object.entries(spec);
|
|
7
|
+
if (entries.length === 0) {
|
|
8
|
+
return "{}";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lines = entries.map(
|
|
12
|
+
([name, type]) => ` ${JSON.stringify(name)}: ${JSON.stringify(type)},`
|
|
13
|
+
);
|
|
14
|
+
return `{\n${lines.join("\n")}\n }`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatDictionarySpecBlock(
|
|
18
|
+
isSingle: boolean,
|
|
19
|
+
entries: NamespaceEntry[],
|
|
20
|
+
argsSpecByNamespace: Record<string, Record<string, VariableSpec>>
|
|
21
|
+
): string {
|
|
22
|
+
if (isSingle) {
|
|
23
|
+
const onlyNamespace = entries[0]!.namespace;
|
|
24
|
+
const argsByKey = argsSpecByNamespace[onlyNamespace] ?? {};
|
|
25
|
+
const requiredKeys = Object.keys(argsByKey)
|
|
26
|
+
.map((key) => JSON.stringify(key))
|
|
27
|
+
.join(", ");
|
|
28
|
+
const argsLines = Object.entries(argsByKey)
|
|
29
|
+
.map(([key, spec]) => ` ${JSON.stringify(key)}: ${formatVariableSpecObject(spec)},`)
|
|
30
|
+
.join("\n");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
`export const DICTIONARY_SPEC = {\n` +
|
|
34
|
+
` mode: 'single' as const,\n` +
|
|
35
|
+
` requiredKeys: [${requiredKeys}] as const,\n` +
|
|
36
|
+
` argsByKey: {\n${argsLines}\n },\n` +
|
|
37
|
+
`} satisfies DictionarySpec;\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const requiredKeysLines = entries
|
|
42
|
+
.map((entry) => {
|
|
43
|
+
const keys = Object.keys(argsSpecByNamespace[entry.namespace] ?? {})
|
|
44
|
+
.map((key) => JSON.stringify(key))
|
|
45
|
+
.join(", ");
|
|
46
|
+
return ` ${JSON.stringify(entry.namespace)}: [${keys}] as const,`;
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
const argsByKeyLines = entries
|
|
51
|
+
.map((entry) => {
|
|
52
|
+
const argsByKey = argsSpecByNamespace[entry.namespace] ?? {};
|
|
53
|
+
const keyLines = Object.entries(argsByKey)
|
|
54
|
+
.map(([key, spec]) => ` ${JSON.stringify(key)}: ${formatVariableSpecObject(spec)},`)
|
|
55
|
+
.join("\n");
|
|
56
|
+
return ` ${JSON.stringify(entry.namespace)}: {\n${keyLines}\n },`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
`export const DICTIONARY_SPEC = {\n` +
|
|
62
|
+
` mode: 'multi' as const,\n` +
|
|
63
|
+
` requiredKeys: {\n${requiredKeysLines}\n },\n` +
|
|
64
|
+
` argsByKey: {\n${argsByKeyLines}\n },\n` +
|
|
65
|
+
`} satisfies DictionarySpec;\n`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatDictionarySchemaFile(
|
|
70
|
+
schemaTypeName: string,
|
|
71
|
+
typesModule: string,
|
|
72
|
+
isSingle: boolean,
|
|
73
|
+
dictionarySpecBlock: string
|
|
74
|
+
): string {
|
|
75
|
+
const namespaceImport = isSingle
|
|
76
|
+
? ""
|
|
77
|
+
: ` validateExternalNamespace as validateExternalNamespaceImpl,\n`;
|
|
78
|
+
const namespaceValidator = isSingle
|
|
79
|
+
? ""
|
|
80
|
+
: `export function validateExternalNamespace<NS extends keyof ${schemaTypeName}>(\n` +
|
|
81
|
+
` namespace: NS,\n` +
|
|
82
|
+
` input: unknown,\n` +
|
|
83
|
+
`) {\n` +
|
|
84
|
+
` return validateExternalNamespaceImpl<${schemaTypeName}[NS]>(\n` +
|
|
85
|
+
` namespace as string,\n` +
|
|
86
|
+
` input,\n` +
|
|
87
|
+
` DICTIONARY_SPEC,\n` +
|
|
88
|
+
` );\n` +
|
|
89
|
+
`}\n\n`;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
`${GENERATED_FILE_BANNER}` +
|
|
93
|
+
`import {\n` +
|
|
94
|
+
` normalizeDictionary,\n` +
|
|
95
|
+
` validateNormalizedDictionary,\n` +
|
|
96
|
+
namespaceImport +
|
|
97
|
+
` toDictionary,\n` +
|
|
98
|
+
` type DictionarySpec,\n` +
|
|
99
|
+
` type NormalizedDictionary,\n` +
|
|
100
|
+
` type ValidationResult,\n` +
|
|
101
|
+
`} from '@xndrjs/i18n/validation';\n` +
|
|
102
|
+
`import type { ${schemaTypeName} } from './${typesModule}.js';\n\n` +
|
|
103
|
+
`${dictionarySpecBlock}\n` +
|
|
104
|
+
`export function normalizeExternalDictionary(\n` +
|
|
105
|
+
` input: unknown,\n` +
|
|
106
|
+
`): ValidationResult<NormalizedDictionary> {\n` +
|
|
107
|
+
` return normalizeDictionary(input, DICTIONARY_SPEC);\n` +
|
|
108
|
+
`}\n\n` +
|
|
109
|
+
`export function validateNormalizedExternalDictionary(\n` +
|
|
110
|
+
` normalized: NormalizedDictionary,\n` +
|
|
111
|
+
`): ValidationResult<NormalizedDictionary> {\n` +
|
|
112
|
+
` return validateNormalizedDictionary(normalized, DICTIONARY_SPEC);\n` +
|
|
113
|
+
`}\n\n` +
|
|
114
|
+
`export function validateExternalDictionary(\n` +
|
|
115
|
+
` input: unknown,\n` +
|
|
116
|
+
`): ValidationResult<${schemaTypeName}> {\n` +
|
|
117
|
+
` const step1 = normalizeExternalDictionary(input);\n` +
|
|
118
|
+
` if (!step1.ok) {\n` +
|
|
119
|
+
` return step1;\n` +
|
|
120
|
+
` }\n\n` +
|
|
121
|
+
` const step2 = validateNormalizedExternalDictionary(step1.data);\n` +
|
|
122
|
+
` if (!step2.ok) {\n` +
|
|
123
|
+
` return step2;\n` +
|
|
124
|
+
` }\n\n` +
|
|
125
|
+
` return { ok: true, data: toDictionary(step2.data) as ${schemaTypeName} };\n` +
|
|
126
|
+
`}\n\n` +
|
|
127
|
+
namespaceValidator
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { GENERATED_FILE_BANNER, toModuleBasename } from "../paths.js";
|
|
2
|
+
|
|
3
|
+
export interface InstanceFileOptions {
|
|
4
|
+
isSingle: boolean;
|
|
5
|
+
hasLazy: boolean;
|
|
6
|
+
typesOutputPath: string;
|
|
7
|
+
dictionaryOutputPath: string;
|
|
8
|
+
paramsTypeName: string;
|
|
9
|
+
schemaTypeName: string;
|
|
10
|
+
localeTypeName: string;
|
|
11
|
+
localeFallbackConstName: string;
|
|
12
|
+
factoryName: string;
|
|
13
|
+
hasLocaleFallback: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatInstanceFile(options: InstanceFileOptions): string {
|
|
17
|
+
const {
|
|
18
|
+
isSingle,
|
|
19
|
+
hasLazy,
|
|
20
|
+
typesOutputPath,
|
|
21
|
+
dictionaryOutputPath,
|
|
22
|
+
paramsTypeName,
|
|
23
|
+
schemaTypeName,
|
|
24
|
+
localeTypeName,
|
|
25
|
+
localeFallbackConstName,
|
|
26
|
+
factoryName,
|
|
27
|
+
hasLocaleFallback,
|
|
28
|
+
} = options;
|
|
29
|
+
|
|
30
|
+
const providerClass = isSingle ? "IcuTranslationProviderSingle" : "IcuTranslationProviderMulti";
|
|
31
|
+
const typesModule = toModuleBasename(typesOutputPath);
|
|
32
|
+
const dictionaryModule = toModuleBasename(dictionaryOutputPath);
|
|
33
|
+
const initialDictionaryType = hasLazy ? "InitialSchema" : schemaTypeName;
|
|
34
|
+
const initialDictionaryImport = hasLazy
|
|
35
|
+
? `import type { ${paramsTypeName}, ${schemaTypeName}, InitialSchema } from './${typesModule}.js';\n`
|
|
36
|
+
: `import type { ${paramsTypeName}, ${schemaTypeName} } from './${typesModule}.js';\n`;
|
|
37
|
+
const providerTypeArgs = hasLocaleFallback
|
|
38
|
+
? `${schemaTypeName}, ${paramsTypeName}, ${localeTypeName}, typeof ${localeFallbackConstName}`
|
|
39
|
+
: `${schemaTypeName}, ${paramsTypeName}`;
|
|
40
|
+
const providerOptions = hasLocaleFallback
|
|
41
|
+
? `, {\n localeFallback: ${localeFallbackConstName},\n }`
|
|
42
|
+
: "";
|
|
43
|
+
const fallbackImport = hasLocaleFallback
|
|
44
|
+
? `import { ${localeFallbackConstName}, type ${localeTypeName} } from './${typesModule}.js';\n`
|
|
45
|
+
: "";
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
`${GENERATED_FILE_BANNER}` +
|
|
49
|
+
`import { ${providerClass} } from '@xndrjs/i18n';\n` +
|
|
50
|
+
`import { dictionary } from './${dictionaryModule}.js';\n` +
|
|
51
|
+
initialDictionaryImport +
|
|
52
|
+
fallbackImport +
|
|
53
|
+
`\n` +
|
|
54
|
+
`export function ${factoryName}(\n` +
|
|
55
|
+
` initialDictionary: ${initialDictionaryType} = dictionary,\n` +
|
|
56
|
+
`) {\n` +
|
|
57
|
+
` return new ${providerClass}<${providerTypeArgs}>(initialDictionary${providerOptions});\n` +
|
|
58
|
+
`}\n`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { GENERATED_FILE_BANNER, toImportPath } from "../paths.js";
|
|
2
|
+
import type { NamespaceEntry } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export function formatNamespaceLoadersFile(
|
|
5
|
+
loadersOutputPath: string,
|
|
6
|
+
lazyEntries: (NamespaceEntry & { absolutePath: string })[],
|
|
7
|
+
schemaTypeName: string,
|
|
8
|
+
typesModule: string,
|
|
9
|
+
dictionarySchemaModule: string,
|
|
10
|
+
instanceModule: string,
|
|
11
|
+
factoryName: string
|
|
12
|
+
): string {
|
|
13
|
+
const loaderEntries = lazyEntries
|
|
14
|
+
.map((entry) => {
|
|
15
|
+
const importPath = toImportPath(loadersOutputPath, entry.absolutePath);
|
|
16
|
+
return ` ${entry.namespace}: () => import('${importPath}.json').then((m) => m.default),`;
|
|
17
|
+
})
|
|
18
|
+
.join("\n");
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
`${GENERATED_FILE_BANNER}` +
|
|
22
|
+
`import { ensureNamespacesLoadedImpl } from '@xndrjs/i18n';\n` +
|
|
23
|
+
`import { validateExternalNamespace } from './${dictionarySchemaModule}.js';\n` +
|
|
24
|
+
`import type { ${schemaTypeName}, LazyNamespace } from './${typesModule}.js';\n` +
|
|
25
|
+
`import type { ${factoryName} } from './${instanceModule}.js';\n\n` +
|
|
26
|
+
`export const namespaceLoaders: Record<\n` +
|
|
27
|
+
` LazyNamespace,\n` +
|
|
28
|
+
` () => Promise<${schemaTypeName}[LazyNamespace]>\n` +
|
|
29
|
+
`> = {\n${loaderEntries}\n};\n\n` +
|
|
30
|
+
`export async function ensureNamespacesLoaded(\n` +
|
|
31
|
+
` i18n: ReturnType<typeof ${factoryName}>,\n` +
|
|
32
|
+
` namespaces: LazyNamespace[],\n` +
|
|
33
|
+
`): Promise<void> {\n` +
|
|
34
|
+
` return ensureNamespacesLoadedImpl<${schemaTypeName}, LazyNamespace>(\n` +
|
|
35
|
+
` {\n` +
|
|
36
|
+
` provider: i18n,\n` +
|
|
37
|
+
` resolveLoader: (namespace) => namespaceLoaders[namespace],\n` +
|
|
38
|
+
` validate: (namespace, raw) => validateExternalNamespace(namespace, raw),\n` +
|
|
39
|
+
` },\n` +
|
|
40
|
+
` namespaces,\n` +
|
|
41
|
+
` );\n` +
|
|
42
|
+
`}\n`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatLocaleFallbackBlock } from "../locale-fallback.js";
|
|
3
|
+
import { GENERATED_FILE_BANNER, toImportPath } from "../paths.js";
|
|
4
|
+
import type { NamespaceEntry } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export function formatLazyTypesBlock(
|
|
7
|
+
loadOnInitSet: Set<string>,
|
|
8
|
+
lazyEntries: NamespaceEntry[],
|
|
9
|
+
schemaTypeName: string
|
|
10
|
+
): string {
|
|
11
|
+
const loadOnInitUnion = [...loadOnInitSet]
|
|
12
|
+
.sort()
|
|
13
|
+
.map((namespace) => `'${namespace}'`)
|
|
14
|
+
.join(" | ");
|
|
15
|
+
const lazyUnion = lazyEntries.map((entry) => `'${entry.namespace}'`).join(" | ");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
`export type LoadOnInitNamespace = ${loadOnInitUnion};\n` +
|
|
19
|
+
`export type LazyNamespace = ${lazyUnion};\n` +
|
|
20
|
+
`export type InitialSchema = Pick<${schemaTypeName}, LoadOnInitNamespace>;\n\n`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TypesFileOptions {
|
|
25
|
+
isSingle: boolean;
|
|
26
|
+
entries: NamespaceEntry[];
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
typesOutputPath: string;
|
|
29
|
+
paramsTypeName: string;
|
|
30
|
+
schemaTypeName: string;
|
|
31
|
+
localeTypeName: string;
|
|
32
|
+
localeFallbackConstName: string;
|
|
33
|
+
localeFallbackTypeName: string;
|
|
34
|
+
localeFallback?: Record<string, string | null> | undefined;
|
|
35
|
+
paramsByNamespace: Record<string, Record<string, string>>;
|
|
36
|
+
requestLocaleUnion: string;
|
|
37
|
+
hasLazy: boolean;
|
|
38
|
+
loadOnInitSet: Set<string>;
|
|
39
|
+
lazyEntries: NamespaceEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatTypesFile(options: TypesFileOptions): string {
|
|
43
|
+
const {
|
|
44
|
+
isSingle,
|
|
45
|
+
entries,
|
|
46
|
+
projectRoot,
|
|
47
|
+
typesOutputPath,
|
|
48
|
+
paramsTypeName,
|
|
49
|
+
schemaTypeName,
|
|
50
|
+
localeTypeName,
|
|
51
|
+
localeFallbackConstName,
|
|
52
|
+
localeFallbackTypeName,
|
|
53
|
+
localeFallback,
|
|
54
|
+
paramsByNamespace,
|
|
55
|
+
requestLocaleUnion,
|
|
56
|
+
hasLazy,
|
|
57
|
+
loadOnInitSet,
|
|
58
|
+
lazyEntries,
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
61
|
+
const localeBlock = requestLocaleUnion
|
|
62
|
+
? `${localeFallback ? formatLocaleFallbackBlock(localeFallback, localeFallbackConstName, localeFallbackTypeName) : ""}` +
|
|
63
|
+
`export type ${localeTypeName} = ${requestLocaleUnion};\n\n`
|
|
64
|
+
: "";
|
|
65
|
+
|
|
66
|
+
let paramsBlock: string;
|
|
67
|
+
let schemaBlock: string;
|
|
68
|
+
|
|
69
|
+
if (isSingle) {
|
|
70
|
+
const onlyNamespace = entries[0]!.namespace;
|
|
71
|
+
const keyTypes = paramsByNamespace[onlyNamespace] ?? {};
|
|
72
|
+
const paramsLines = Object.entries(keyTypes)
|
|
73
|
+
.map(([key, type]) => ` ${key}: ${type};`)
|
|
74
|
+
.join("\n");
|
|
75
|
+
|
|
76
|
+
paramsBlock = `export type ${paramsTypeName} = {\n${paramsLines}\n};`;
|
|
77
|
+
|
|
78
|
+
const importPath = toImportPath(
|
|
79
|
+
typesOutputPath,
|
|
80
|
+
path.resolve(projectRoot, entries[0]!.filePath)
|
|
81
|
+
);
|
|
82
|
+
schemaBlock = `export type ${schemaTypeName} = typeof import('${importPath}.json');`;
|
|
83
|
+
} else {
|
|
84
|
+
const namespaceBlocks = entries
|
|
85
|
+
.map((entry) => {
|
|
86
|
+
const keyTypes = paramsByNamespace[entry.namespace] ?? {};
|
|
87
|
+
const lines = Object.entries(keyTypes)
|
|
88
|
+
.map(([key, type]) => ` ${key}: ${type};`)
|
|
89
|
+
.join("\n");
|
|
90
|
+
return ` ${entry.namespace}: {\n${lines}\n };`;
|
|
91
|
+
})
|
|
92
|
+
.join("\n");
|
|
93
|
+
|
|
94
|
+
paramsBlock = `export type ${paramsTypeName} = {\n${namespaceBlocks}\n};`;
|
|
95
|
+
|
|
96
|
+
const schemaLines = entries
|
|
97
|
+
.map((entry) => {
|
|
98
|
+
const importPath = toImportPath(typesOutputPath, path.resolve(projectRoot, entry.filePath));
|
|
99
|
+
return ` ${entry.namespace}: typeof import('${importPath}.json');`;
|
|
100
|
+
})
|
|
101
|
+
.join("\n");
|
|
102
|
+
|
|
103
|
+
schemaBlock = `export type ${schemaTypeName} = {\n${schemaLines}\n};`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lazyTypesBlock = hasLazy
|
|
107
|
+
? formatLazyTypesBlock(loadOnInitSet, lazyEntries, schemaTypeName)
|
|
108
|
+
: "";
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
`${GENERATED_FILE_BANNER}` +
|
|
112
|
+
`export const I18N_MODE = '${isSingle ? "single" : "multi"}' as const;\n\n` +
|
|
113
|
+
`${localeBlock}` +
|
|
114
|
+
`${paramsBlock}\n\n` +
|
|
115
|
+
`${schemaBlock}\n` +
|
|
116
|
+
`${lazyTypesBlock}`
|
|
117
|
+
);
|
|
118
|
+
}
|