@flightdev/i18n 0.1.5
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/LICENSE +21 -0
- package/README.md +644 -0
- package/dist/adapters/formatjs.d.ts +61 -0
- package/dist/adapters/formatjs.js +81 -0
- package/dist/adapters/i18next.d.ts +15 -0
- package/dist/adapters/i18next.js +51 -0
- package/dist/adapters/lingui.d.ts +82 -0
- package/dist/adapters/lingui.js +73 -0
- package/dist/adapters/paraglide.d.ts +65 -0
- package/dist/adapters/paraglide.js +43 -0
- package/dist/chunk-F5CV7JNA.js +76 -0
- package/dist/chunk-O3FQ7FPU.js +150 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +18 -0
- package/dist/middleware.d.ts +107 -0
- package/dist/middleware.js +127 -0
- package/dist/routing.d.ts +179 -0
- package/dist/routing.js +22 -0
- package/dist/typegen.d.ts +127 -0
- package/dist/typegen.js +214 -0
- package/package.json +72 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Generation for Type-Safe Translation Keys
|
|
3
|
+
*
|
|
4
|
+
* Generates TypeScript declaration files from JSON translation files,
|
|
5
|
+
* enabling full autocompletion and compile-time validation of translation keys.
|
|
6
|
+
*
|
|
7
|
+
* @example CLI Usage
|
|
8
|
+
* ```bash
|
|
9
|
+
* # Generate types from translation directory
|
|
10
|
+
* npx flight i18n:typegen --input ./locales/en --output ./src/i18n.d.ts
|
|
11
|
+
*
|
|
12
|
+
* # Generate nested types
|
|
13
|
+
* npx flight i18n:typegen --input ./locales/en --output ./src/i18n.d.ts --style nested
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example Programmatic Usage
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { generateTypesAndWrite } from '@flightdev/i18n/typegen';
|
|
19
|
+
*
|
|
20
|
+
* await generateTypesAndWrite({
|
|
21
|
+
* input: './locales/en',
|
|
22
|
+
* output: './src/i18n.d.ts',
|
|
23
|
+
* style: 'flat',
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example Using Generated Types
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import type { TranslationKey } from './i18n.d.ts';
|
|
30
|
+
* import { createTypedI18n } from '@flightdev/i18n';
|
|
31
|
+
*
|
|
32
|
+
* const i18n = createTypedI18n<TranslationKey>(adapter);
|
|
33
|
+
*
|
|
34
|
+
* i18n.t('nav.home'); // Valid - autocomplete works
|
|
35
|
+
* i18n.t('nav.nonexistent'); // TypeScript error
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @module @flightdev/i18n/typegen
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Options for type generation
|
|
42
|
+
*/
|
|
43
|
+
interface TypeGenOptions {
|
|
44
|
+
/** Input directory or JSON file containing translations */
|
|
45
|
+
input: string;
|
|
46
|
+
/** Output path for generated .d.ts file */
|
|
47
|
+
output: string;
|
|
48
|
+
/**
|
|
49
|
+
* Type style:
|
|
50
|
+
* - 'flat': Union of string literal types ("nav.home" | "nav.about")
|
|
51
|
+
* - 'nested': Nested object types matching JSON structure
|
|
52
|
+
* @default 'flat'
|
|
53
|
+
*/
|
|
54
|
+
style?: 'flat' | 'nested';
|
|
55
|
+
/**
|
|
56
|
+
* Custom header comment for generated file
|
|
57
|
+
* @default Auto-generated header with timestamp
|
|
58
|
+
*/
|
|
59
|
+
header?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Module name if generating a module declaration
|
|
62
|
+
* If provided, wraps types in `declare module`
|
|
63
|
+
*/
|
|
64
|
+
moduleName?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Result of type generation
|
|
68
|
+
*/
|
|
69
|
+
interface TypeGenResult {
|
|
70
|
+
/** Generated TypeScript code */
|
|
71
|
+
content: string;
|
|
72
|
+
/** Number of translation keys found */
|
|
73
|
+
keyCount: number;
|
|
74
|
+
/** List of all keys in flat format */
|
|
75
|
+
keys: string[];
|
|
76
|
+
}
|
|
77
|
+
/** Recursive record type for nested translations */
|
|
78
|
+
type NestedRecord = {
|
|
79
|
+
[key: string]: string | NestedRecord;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Extract all translation keys from a nested object in dot notation
|
|
83
|
+
*
|
|
84
|
+
* @param obj - Translation object
|
|
85
|
+
* @param prefix - Current key prefix
|
|
86
|
+
* @returns Array of dot-notation keys
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* extractKeysFromObject({ nav: { home: 'Home', about: 'About' } });
|
|
91
|
+
* // ['nav.home', 'nav.about']
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
declare function extractKeysFromObject(obj: NestedRecord, prefix?: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* Load translation data from file or directory
|
|
97
|
+
*
|
|
98
|
+
* @param inputPath - Path to JSON file or directory containing JSON files
|
|
99
|
+
* @returns Merged translation object
|
|
100
|
+
*/
|
|
101
|
+
declare function loadTranslations(inputPath: string): NestedRecord;
|
|
102
|
+
/**
|
|
103
|
+
* Generate TypeScript types from translation files
|
|
104
|
+
*
|
|
105
|
+
* @param options - Generation options
|
|
106
|
+
* @returns Generated TypeScript content and metadata
|
|
107
|
+
*/
|
|
108
|
+
declare function generateTypes(options: TypeGenOptions): TypeGenResult;
|
|
109
|
+
/**
|
|
110
|
+
* Generate types and write to output file
|
|
111
|
+
*
|
|
112
|
+
* @param options - Generation options
|
|
113
|
+
*/
|
|
114
|
+
declare function generateTypesAndWrite(options: TypeGenOptions): Promise<TypeGenResult>;
|
|
115
|
+
interface WatchOptions extends TypeGenOptions {
|
|
116
|
+
/** Debounce delay in milliseconds */
|
|
117
|
+
debounce?: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Watch translation files and regenerate types on changes
|
|
121
|
+
*
|
|
122
|
+
* @param options - Watch options
|
|
123
|
+
* @returns Cleanup function to stop watching
|
|
124
|
+
*/
|
|
125
|
+
declare function watchAndGenerate(options: WatchOptions): () => void;
|
|
126
|
+
|
|
127
|
+
export { type TypeGenOptions, type TypeGenResult, type WatchOptions, extractKeysFromObject, generateTypes, generateTypesAndWrite, loadTranslations, watchAndGenerate };
|
package/dist/typegen.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/typegen.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
function extractKeysFromObject(obj, prefix = "") {
|
|
5
|
+
const keys = [];
|
|
6
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
7
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
8
|
+
const fullKey = prefix ? `${prefix}.${safeKey}` : safeKey;
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
keys.push(fullKey);
|
|
11
|
+
} else if (typeof value === "object" && value !== null) {
|
|
12
|
+
keys.push(...extractKeysFromObject(value, fullKey));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return keys.sort();
|
|
16
|
+
}
|
|
17
|
+
function loadTranslations(inputPath) {
|
|
18
|
+
const absolutePath = path.resolve(inputPath);
|
|
19
|
+
if (!fs.existsSync(absolutePath)) {
|
|
20
|
+
throw new Error(`Translation path does not exist: ${absolutePath}`);
|
|
21
|
+
}
|
|
22
|
+
const stat = fs.statSync(absolutePath);
|
|
23
|
+
if (stat.isFile()) {
|
|
24
|
+
if (!absolutePath.endsWith(".json")) {
|
|
25
|
+
throw new Error(`Input file must be a JSON file: ${absolutePath}`);
|
|
26
|
+
}
|
|
27
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
}
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
const merged = {};
|
|
32
|
+
const files = fs.readdirSync(absolutePath);
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (!file.endsWith(".json")) continue;
|
|
35
|
+
const namespace = file.replace(".json", "");
|
|
36
|
+
const filePath = path.join(absolutePath, file);
|
|
37
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
38
|
+
try {
|
|
39
|
+
merged[namespace] = JSON.parse(content);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(merged).length === 0) {
|
|
45
|
+
throw new Error(`No JSON files found in directory: ${absolutePath}`);
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Input path is neither a file nor directory: ${absolutePath}`);
|
|
50
|
+
}
|
|
51
|
+
function generateFlatTypes(keys, options) {
|
|
52
|
+
const header = options.header ?? generateDefaultHeader();
|
|
53
|
+
const keyLiterals = keys.map((k) => ` | '${k}'`).join("\n");
|
|
54
|
+
let content = `${header}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* All available translation keys (union type)
|
|
58
|
+
* Use with createTypedI18n for full type safety
|
|
59
|
+
*/
|
|
60
|
+
export type TranslationKey =
|
|
61
|
+
${keyLiterals};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Translation key to value mapping
|
|
65
|
+
*/
|
|
66
|
+
export type TranslationKeys = {
|
|
67
|
+
[K in TranslationKey]: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Helper type to validate translation keys
|
|
72
|
+
*/
|
|
73
|
+
export type ValidTranslationKey<T extends string> = T extends TranslationKey ? T : never;
|
|
74
|
+
`;
|
|
75
|
+
if (options.moduleName) {
|
|
76
|
+
content = `declare module '${options.moduleName}' {
|
|
77
|
+
${content}
|
|
78
|
+
}`;
|
|
79
|
+
}
|
|
80
|
+
return content;
|
|
81
|
+
}
|
|
82
|
+
function generateNestedInterface(obj, indent = 1) {
|
|
83
|
+
const spaces = " ".repeat(indent);
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
86
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
lines.push(`${spaces}${safeKey}: string;`);
|
|
89
|
+
} else if (typeof value === "object" && value !== null) {
|
|
90
|
+
lines.push(`${spaces}${safeKey}: {`);
|
|
91
|
+
lines.push(generateNestedInterface(value, indent + 1));
|
|
92
|
+
lines.push(`${spaces}};`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function generateNestedTypes(obj, options) {
|
|
98
|
+
const header = options.header ?? generateDefaultHeader();
|
|
99
|
+
const interfaceContent = generateNestedInterface(obj);
|
|
100
|
+
let content = `${header}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Nested translation structure matching JSON files
|
|
104
|
+
*/
|
|
105
|
+
export interface TranslationTree {
|
|
106
|
+
${interfaceContent}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Utility type to flatten nested keys to dot notation
|
|
111
|
+
*/
|
|
112
|
+
type FlattenKeys<T, Prefix extends string = ''> = T extends string
|
|
113
|
+
? Prefix
|
|
114
|
+
: {
|
|
115
|
+
[K in keyof T]: K extends string
|
|
116
|
+
? FlattenKeys<T[K], Prefix extends '' ? K : \`\${Prefix}.\${K}\`>
|
|
117
|
+
: never;
|
|
118
|
+
}[keyof T];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* All available translation keys (flattened from nested structure)
|
|
122
|
+
*/
|
|
123
|
+
export type TranslationKey = FlattenKeys<TranslationTree>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Access nested value by dot-notation key
|
|
127
|
+
*/
|
|
128
|
+
type GetNested<T, K extends string> = K extends \`\${infer A}.\${infer B}\`
|
|
129
|
+
? A extends keyof T
|
|
130
|
+
? GetNested<T[A], B>
|
|
131
|
+
: never
|
|
132
|
+
: K extends keyof T
|
|
133
|
+
? T[K]
|
|
134
|
+
: never;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the value type for a specific key
|
|
138
|
+
*/
|
|
139
|
+
export type TranslationValue<K extends TranslationKey> = GetNested<TranslationTree, K>;
|
|
140
|
+
`;
|
|
141
|
+
if (options.moduleName) {
|
|
142
|
+
content = `declare module '${options.moduleName}' {
|
|
143
|
+
${content}
|
|
144
|
+
}`;
|
|
145
|
+
}
|
|
146
|
+
return content;
|
|
147
|
+
}
|
|
148
|
+
function generateDefaultHeader() {
|
|
149
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
150
|
+
return `/**
|
|
151
|
+
* Auto-generated translation types
|
|
152
|
+
* DO NOT EDIT - Generated by @flightdev/i18n
|
|
153
|
+
* Generated at: ${timestamp}
|
|
154
|
+
*
|
|
155
|
+
* To regenerate, run:
|
|
156
|
+
* npx flight i18n:typegen --input <translations-path> --output <output-path>
|
|
157
|
+
*/`;
|
|
158
|
+
}
|
|
159
|
+
function generateTypes(options) {
|
|
160
|
+
const { input, style = "flat" } = options;
|
|
161
|
+
const translations = loadTranslations(input);
|
|
162
|
+
const keys = extractKeysFromObject(translations);
|
|
163
|
+
const content = style === "nested" ? generateNestedTypes(translations, options) : generateFlatTypes(keys, options);
|
|
164
|
+
return {
|
|
165
|
+
content,
|
|
166
|
+
keyCount: keys.length,
|
|
167
|
+
keys
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async function generateTypesAndWrite(options) {
|
|
171
|
+
const result = generateTypes(options);
|
|
172
|
+
const outputPath = path.resolve(options.output);
|
|
173
|
+
const outputDir = path.dirname(outputPath);
|
|
174
|
+
if (!fs.existsSync(outputDir)) {
|
|
175
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(outputPath, result.content, "utf-8");
|
|
178
|
+
console.log(`Generated ${result.keyCount} translation keys at ${outputPath}`);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
function watchAndGenerate(options) {
|
|
182
|
+
const { input, debounce = 100 } = options;
|
|
183
|
+
const absolutePath = path.resolve(input);
|
|
184
|
+
let timeout = null;
|
|
185
|
+
const regenerate = () => {
|
|
186
|
+
try {
|
|
187
|
+
generateTypesAndWrite(options);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("Type generation failed:", error.message);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const debouncedRegenerate = () => {
|
|
193
|
+
if (timeout) clearTimeout(timeout);
|
|
194
|
+
timeout = setTimeout(regenerate, debounce);
|
|
195
|
+
};
|
|
196
|
+
regenerate();
|
|
197
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
198
|
+
if (filename?.endsWith(".json")) {
|
|
199
|
+
console.log(`Translation file changed: ${filename}`);
|
|
200
|
+
debouncedRegenerate();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
watcher.close();
|
|
205
|
+
if (timeout) clearTimeout(timeout);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
extractKeysFromObject,
|
|
210
|
+
generateTypes,
|
|
211
|
+
generateTypesAndWrite,
|
|
212
|
+
loadTranslations,
|
|
213
|
+
watchAndGenerate
|
|
214
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flightdev/i18n",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Agnostic internationalization for Flight Framework. Choose your engine: i18next, Paraglide, FormatJS, Lingui, or custom.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./i18next": {
|
|
12
|
+
"types": "./dist/adapters/i18next.d.ts",
|
|
13
|
+
"import": "./dist/adapters/i18next.js"
|
|
14
|
+
},
|
|
15
|
+
"./paraglide": {
|
|
16
|
+
"types": "./dist/adapters/paraglide.d.ts",
|
|
17
|
+
"import": "./dist/adapters/paraglide.js"
|
|
18
|
+
},
|
|
19
|
+
"./formatjs": {
|
|
20
|
+
"types": "./dist/adapters/formatjs.d.ts",
|
|
21
|
+
"import": "./dist/adapters/formatjs.js"
|
|
22
|
+
},
|
|
23
|
+
"./lingui": {
|
|
24
|
+
"types": "./dist/adapters/lingui.d.ts",
|
|
25
|
+
"import": "./dist/adapters/lingui.js"
|
|
26
|
+
},
|
|
27
|
+
"./routing": {
|
|
28
|
+
"types": "./dist/routing.d.ts",
|
|
29
|
+
"import": "./dist/routing.js"
|
|
30
|
+
},
|
|
31
|
+
"./middleware": {
|
|
32
|
+
"types": "./dist/middleware.d.ts",
|
|
33
|
+
"import": "./dist/middleware.js"
|
|
34
|
+
},
|
|
35
|
+
"./typegen": {
|
|
36
|
+
"types": "./dist/typegen.d.ts",
|
|
37
|
+
"import": "./dist/typegen.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@formatjs/intl": ">=2.0.0",
|
|
45
|
+
"@lingui/core": ">=5.0.0",
|
|
46
|
+
"i18next": ">=23.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@formatjs/intl": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"@lingui/core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"i18next": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.0.0",
|
|
61
|
+
"tsup": "^8.0.0",
|
|
62
|
+
"typescript": "^5.7.0",
|
|
63
|
+
"vitest": "^2.0.0"
|
|
64
|
+
},
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsup",
|
|
68
|
+
"dev": "tsup --watch",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"typecheck": "tsc --noEmit"
|
|
71
|
+
}
|
|
72
|
+
}
|