@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.
Files changed (50) hide show
  1. package/README.md +572 -0
  2. package/bin/codegen.mjs +17 -0
  3. package/bin/setup.mjs +14 -0
  4. package/dist/index.d.ts +124 -0
  5. package/dist/index.js +320 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/types-CFWVT2PP.d.ts +77 -0
  8. package/dist/validation/index.d.ts +44 -0
  9. package/dist/validation/index.js +490 -0
  10. package/dist/validation/index.js.map +1 -0
  11. package/package.json +65 -0
  12. package/src/IcuTranslationProviderMulti.test.ts +290 -0
  13. package/src/IcuTranslationProviderMulti.ts +195 -0
  14. package/src/IcuTranslationProviderSingle.test.ts +314 -0
  15. package/src/IcuTranslationProviderSingle.ts +155 -0
  16. package/src/codegen/config.ts +63 -0
  17. package/src/codegen/emit/dictionary-file.ts +73 -0
  18. package/src/codegen/emit/dictionary-schema-file.ts +129 -0
  19. package/src/codegen/emit/instance-file.ts +60 -0
  20. package/src/codegen/emit/namespace-loaders-file.ts +44 -0
  21. package/src/codegen/emit/types-file.ts +118 -0
  22. package/src/codegen/generate-i18n-types.test.ts +482 -0
  23. package/src/codegen/generate-i18n-types.ts +173 -0
  24. package/src/codegen/icu-analysis.ts +88 -0
  25. package/src/codegen/locale-fallback.ts +64 -0
  26. package/src/codegen/paths.ts +27 -0
  27. package/src/codegen/types.ts +30 -0
  28. package/src/deep-freeze.test.ts +27 -0
  29. package/src/deep-freeze.ts +17 -0
  30. package/src/ensure-namespace.integration.test.ts +66 -0
  31. package/src/ensure-namespace.test.ts +125 -0
  32. package/src/ensure-namespace.ts +70 -0
  33. package/src/icu/extract-variables.ts +87 -0
  34. package/src/icu/parse-template.ts +24 -0
  35. package/src/index.ts +32 -0
  36. package/src/resolve-locale.test.ts +91 -0
  37. package/src/resolve-locale.ts +88 -0
  38. package/src/setup/setup-i18n.test.ts +86 -0
  39. package/src/setup/setup-i18n.ts +179 -0
  40. package/src/setup/type-names.ts +20 -0
  41. package/src/types.ts +34 -0
  42. package/src/validation/create-args-schema.ts +76 -0
  43. package/src/validation/create-normalized-schema.ts +52 -0
  44. package/src/validation/errors.ts +41 -0
  45. package/src/validation/index.ts +90 -0
  46. package/src/validation/normalize.ts +158 -0
  47. package/src/validation/to-dictionary.ts +67 -0
  48. package/src/validation/types.ts +78 -0
  49. package/src/validation/validate-normalized.ts +91 -0
  50. 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
+ }