i18next-cli 0.9.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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/config.js +1 -0
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
- package/dist/cjs/extractor/plugin-manager.js +1 -0
- package/dist/cjs/heuristic-config.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/init.js +1 -0
- package/dist/cjs/linter.js +1 -0
- package/dist/cjs/locize.js +1 -0
- package/dist/cjs/migrator.js +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/status.js +1 -0
- package/dist/cjs/syncer.js +1 -0
- package/dist/cjs/types-generator.js +1 -0
- package/dist/cjs/utils/file-utils.js +1 -0
- package/dist/cjs/utils/logger.js +1 -0
- package/dist/cjs/utils/nested-object.js +1 -0
- package/dist/cjs/utils/validation.js +1 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/config.js +1 -0
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
- package/dist/esm/extractor/parsers/comment-parser.js +1 -0
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
- package/dist/esm/extractor/plugin-manager.js +1 -0
- package/dist/esm/heuristic-config.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/init.js +1 -0
- package/dist/esm/linter.js +1 -0
- package/dist/esm/locize.js +1 -0
- package/dist/esm/migrator.js +1 -0
- package/dist/esm/status.js +1 -0
- package/dist/esm/syncer.js +1 -0
- package/dist/esm/types-generator.js +1 -0
- package/dist/esm/utils/file-utils.js +1 -0
- package/dist/esm/utils/logger.js +1 -0
- package/dist/esm/utils/nested-object.js +1 -0
- package/dist/esm/utils/validation.js +1 -0
- package/package.json +81 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +158 -0
- package/src/extractor/core/extractor.ts +195 -0
- package/src/extractor/core/key-finder.ts +70 -0
- package/src/extractor/core/translation-manager.ts +115 -0
- package/src/extractor/index.ts +7 -0
- package/src/extractor/parsers/ast-visitors.ts +637 -0
- package/src/extractor/parsers/comment-parser.ts +125 -0
- package/src/extractor/parsers/jsx-parser.ts +166 -0
- package/src/extractor/plugin-manager.ts +54 -0
- package/src/extractor.ts +15 -0
- package/src/heuristic-config.ts +64 -0
- package/src/index.ts +12 -0
- package/src/init.ts +156 -0
- package/src/linter.ts +191 -0
- package/src/locize.ts +251 -0
- package/src/migrator.ts +139 -0
- package/src/status.ts +192 -0
- package/src/syncer.ts +114 -0
- package/src/types-generator.ts +116 -0
- package/src/types.ts +312 -0
- package/src/utils/file-utils.ts +81 -0
- package/src/utils/logger.ts +36 -0
- package/src/utils/nested-object.ts +113 -0
- package/src/utils/validation.ts +69 -0
- package/tryme.js +8 -0
- package/tsconfig.json +71 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/config.d.ts +50 -0
- package/types/config.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +66 -0
- package/types/extractor/core/extractor.d.ts.map +1 -0
- package/types/extractor/core/key-finder.d.ts +31 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -0
- package/types/extractor/core/translation-manager.d.ts +31 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -0
- package/types/extractor/index.d.ts +8 -0
- package/types/extractor/index.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +235 -0
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
- package/types/extractor/parsers/comment-parser.d.ts +24 -0
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-parser.d.ts +35 -0
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
- package/types/extractor/plugin-manager.d.ts +37 -0
- package/types/extractor/plugin-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +7 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/heuristic-config.d.ts +10 -0
- package/types/heuristic-config.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/init.d.ts +29 -0
- package/types/init.d.ts.map +1 -0
- package/types/linter.d.ts +33 -0
- package/types/linter.d.ts.map +1 -0
- package/types/locize.d.ts +5 -0
- package/types/locize.d.ts.map +1 -0
- package/types/migrator.d.ts +37 -0
- package/types/migrator.d.ts.map +1 -0
- package/types/status.d.ts +20 -0
- package/types/status.d.ts.map +1 -0
- package/types/syncer.d.ts +33 -0
- package/types/syncer.d.ts.map +1 -0
- package/types/types-generator.d.ts +29 -0
- package/types/types-generator.d.ts.map +1 -0
- package/types/types.d.ts +268 -0
- package/types/types.d.ts.map +1 -0
- package/types/utils/file-utils.d.ts +61 -0
- package/types/utils/file-utils.d.ts.map +1 -0
- package/types/utils/logger.d.ts +34 -0
- package/types/utils/logger.d.ts.map +1 -0
- package/types/utils/nested-object.d.ts +71 -0
- package/types/utils/nested-object.d.ts.map +1 -0
- package/types/utils/validation.d.ts +47 -0
- package/types/utils/validation.d.ts.map +1 -0
- package/vitest.config.ts +13 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { Node } from '@swc/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main configuration interface for the i18next toolkit.
|
|
5
|
+
* Defines all available options for extraction, type generation, synchronization, and integrations.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const config: I18nextToolkitConfig = {
|
|
10
|
+
* locales: ['en', 'de', 'fr'],
|
|
11
|
+
* extract: {
|
|
12
|
+
* input: ['src/**\/*.{ts,tsx}'],
|
|
13
|
+
* output: 'locales/{{language}}/{{namespace}}.json',
|
|
14
|
+
* functions: ['t', 'i18n.t'],
|
|
15
|
+
* transComponents: ['Trans', 'Translation']
|
|
16
|
+
* },
|
|
17
|
+
* types: {
|
|
18
|
+
* input: ['locales/en/*.json'],
|
|
19
|
+
* output: 'src/types/i18next.d.ts'
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export interface I18nextToolkitConfig {
|
|
25
|
+
/** Array of supported locale codes (e.g., ['en', 'de', 'fr']) */
|
|
26
|
+
locales: string[];
|
|
27
|
+
|
|
28
|
+
/** Configuration options for translation key extraction */
|
|
29
|
+
extract: {
|
|
30
|
+
/** Glob pattern(s) for source files to scan for translation keys */
|
|
31
|
+
input: string | string[];
|
|
32
|
+
|
|
33
|
+
/** Output path template with placeholders: {{language}} for locale, {{namespace}} for namespace */
|
|
34
|
+
output: string;
|
|
35
|
+
|
|
36
|
+
/** Default namespace when none is specified (default: 'translation') */
|
|
37
|
+
defaultNS?: string;
|
|
38
|
+
|
|
39
|
+
/** Separator for nested keys, or false for flat keys (default: '.') */
|
|
40
|
+
keySeparator?: string | false | null;
|
|
41
|
+
|
|
42
|
+
/** Separator between namespace and key, or false to disable (default: ':') */
|
|
43
|
+
nsSeparator?: string | false | null;
|
|
44
|
+
|
|
45
|
+
/** Separator for context variants (default: '_') */
|
|
46
|
+
contextSeparator?: string;
|
|
47
|
+
|
|
48
|
+
/** Separator for plural variants (default: '_') */
|
|
49
|
+
pluralSeparator?: string;
|
|
50
|
+
|
|
51
|
+
/** Function names to extract translation calls from (default: ['t']) */
|
|
52
|
+
functions?: string[];
|
|
53
|
+
|
|
54
|
+
/** JSX component names to extract translations from (default: ['Trans']) */
|
|
55
|
+
transComponents?: string[];
|
|
56
|
+
|
|
57
|
+
/** Hook function names that return translation functions (default: ['useTranslation']) */
|
|
58
|
+
useTranslationNames?: string[];
|
|
59
|
+
|
|
60
|
+
/** A list of JSX attribute names to ignore when linting for hardcoded strings. */
|
|
61
|
+
ignoredAttributes?: string[];
|
|
62
|
+
|
|
63
|
+
/** HTML tags to preserve in Trans component serialization (default: ['br', 'strong', 'i']) */
|
|
64
|
+
transKeepBasicHtmlNodesFor?: string[];
|
|
65
|
+
|
|
66
|
+
/** Glob patterns for keys to preserve even if not found in source (for dynamic keys) */
|
|
67
|
+
preservePatterns?: string[];
|
|
68
|
+
|
|
69
|
+
/** Whether to sort keys alphabetically in output files (default: true) */
|
|
70
|
+
sort?: boolean;
|
|
71
|
+
|
|
72
|
+
/** Number of spaces for JSON indentation (default: 2) */
|
|
73
|
+
indentation?: number;
|
|
74
|
+
|
|
75
|
+
/** Default value to use for missing translations in secondary languages */
|
|
76
|
+
defaultValue?: string;
|
|
77
|
+
|
|
78
|
+
/** Primary language that provides default values (default: first locale) */
|
|
79
|
+
primaryLanguage?: string;
|
|
80
|
+
|
|
81
|
+
/** Secondary languages that get empty values initially */
|
|
82
|
+
secondaryLanguages?: string[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Configuration options for TypeScript type generation */
|
|
86
|
+
types?: {
|
|
87
|
+
/** Glob pattern(s) for translation files to generate types from */
|
|
88
|
+
input: string | string[];
|
|
89
|
+
|
|
90
|
+
/** Output path for the main TypeScript definition file */
|
|
91
|
+
output: string;
|
|
92
|
+
|
|
93
|
+
/** Enable type-safe selector API (boolean or 'optimize' for smaller types) */
|
|
94
|
+
enableSelector?: boolean | 'optimize';
|
|
95
|
+
|
|
96
|
+
/** Path for the separate resources interface file */
|
|
97
|
+
resourcesFile?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Array of plugins to extend functionality */
|
|
101
|
+
plugins?: Plugin[];
|
|
102
|
+
|
|
103
|
+
/** Configuration for Locize integration */
|
|
104
|
+
locize?: {
|
|
105
|
+
/** Locize project ID */
|
|
106
|
+
projectId?: string;
|
|
107
|
+
|
|
108
|
+
/** Locize API key (recommended to use environment variables) */
|
|
109
|
+
apiKey?: string;
|
|
110
|
+
|
|
111
|
+
/** Version to sync with (default: 'latest') */
|
|
112
|
+
version?: string;
|
|
113
|
+
|
|
114
|
+
/** Whether to update existing translation values on Locize */
|
|
115
|
+
updateValues?: boolean;
|
|
116
|
+
|
|
117
|
+
/** Only sync the source language to Locize */
|
|
118
|
+
sourceLanguageOnly?: boolean;
|
|
119
|
+
|
|
120
|
+
/** Compare modification times when syncing */
|
|
121
|
+
compareModificationTime?: boolean;
|
|
122
|
+
|
|
123
|
+
/** Preview changes without making them */
|
|
124
|
+
dryRun?: boolean;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Plugin interface for extending the i18next toolkit functionality.
|
|
130
|
+
* Plugins can hook into various stages of the extraction process.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const myPlugin = (): Plugin => ({
|
|
135
|
+
* name: 'my-custom-plugin',
|
|
136
|
+
*
|
|
137
|
+
* setup: async () => {
|
|
138
|
+
* console.log('Plugin initialized')
|
|
139
|
+
* },
|
|
140
|
+
*
|
|
141
|
+
* onLoad: (code, filePath) => {
|
|
142
|
+
* // Transform code before parsing
|
|
143
|
+
* return code.replace(/OLD_PATTERN/g, 'NEW_PATTERN')
|
|
144
|
+
* },
|
|
145
|
+
*
|
|
146
|
+
* onVisitNode: (node, context) => {
|
|
147
|
+
* if (node.type === 'CallExpression') {
|
|
148
|
+
* // Custom extraction logic
|
|
149
|
+
* context.addKey({ key: 'custom.key', defaultValue: 'Custom Value' })
|
|
150
|
+
* }
|
|
151
|
+
* },
|
|
152
|
+
*
|
|
153
|
+
* onEnd: async (allKeys) => {
|
|
154
|
+
* console.log(`Found ${allKeys.size} total keys`)
|
|
155
|
+
* }
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export interface Plugin {
|
|
160
|
+
/** Unique name for the plugin */
|
|
161
|
+
name: string;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hook called once at the beginning of the extraction process.
|
|
165
|
+
* Use for initialization tasks like setting up resources or validating configuration.
|
|
166
|
+
*/
|
|
167
|
+
setup?: () => void | Promise<void>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Hook called for each source file before it's parsed.
|
|
171
|
+
* Allows transformation of source code before AST generation.
|
|
172
|
+
*
|
|
173
|
+
* @param code - The source code content
|
|
174
|
+
* @param path - The file path being processed
|
|
175
|
+
* @returns The transformed code (or undefined to keep original)
|
|
176
|
+
*/
|
|
177
|
+
onLoad?: (code: string, path: string) => string | Promise<string>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Hook called for each AST node during traversal.
|
|
181
|
+
* Enables custom extraction logic by examining syntax nodes.
|
|
182
|
+
*
|
|
183
|
+
* @param node - The current AST node being visited
|
|
184
|
+
* @param context - Context object with helper methods
|
|
185
|
+
*/
|
|
186
|
+
onVisitNode?: (node: Node, context: PluginContext) => void;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Hook called after all files have been processed.
|
|
190
|
+
* Useful for post-processing, validation, or reporting.
|
|
191
|
+
*
|
|
192
|
+
* @param keys - Final map of all extracted keys
|
|
193
|
+
*/
|
|
194
|
+
onEnd?: (keys: Map<string, { key: string; defaultValue?: string }>) => void | Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Represents an extracted translation key with its metadata.
|
|
199
|
+
* Contains all information needed to generate translation files.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* const extractedKey: ExtractedKey = {
|
|
204
|
+
* key: 'user.profile.name',
|
|
205
|
+
* defaultValue: 'Full Name',
|
|
206
|
+
* ns: 'common',
|
|
207
|
+
* hasCount: false
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
export interface ExtractedKey {
|
|
212
|
+
/** The translation key (may be nested with separators) */
|
|
213
|
+
key: string;
|
|
214
|
+
|
|
215
|
+
/** Default value to use in the primary language */
|
|
216
|
+
defaultValue?: string;
|
|
217
|
+
|
|
218
|
+
/** Namespace this key belongs to */
|
|
219
|
+
ns?: string;
|
|
220
|
+
|
|
221
|
+
/** Whether this key is used with pluralization (count parameter) */
|
|
222
|
+
hasCount?: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Result of processing translation files for a specific locale and namespace.
|
|
227
|
+
* Contains the generated translations and metadata about changes.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* const result: TranslationResult = {
|
|
232
|
+
* path: '/project/locales/en/common.json',
|
|
233
|
+
* updated: true,
|
|
234
|
+
* newTranslations: { button: { save: 'Save', cancel: 'Cancel' } },
|
|
235
|
+
* existingTranslations: { button: { save: 'Save' } }
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export interface TranslationResult {
|
|
240
|
+
/** Full file system path where the translation file will be written */
|
|
241
|
+
path: string;
|
|
242
|
+
|
|
243
|
+
/** Whether the file content changed and needs to be written */
|
|
244
|
+
updated: boolean;
|
|
245
|
+
|
|
246
|
+
/** The new translation object to be written to the file */
|
|
247
|
+
newTranslations: Record<string, any>;
|
|
248
|
+
|
|
249
|
+
/** The existing translation object that was read from the file */
|
|
250
|
+
existingTranslations: Record<string, any>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Logger interface for consistent output formatting across the toolkit.
|
|
255
|
+
* Implementations can customize how messages are displayed or stored.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* class FileLogger implements Logger {
|
|
260
|
+
* info(message: string) { fs.appendFileSync('info.log', message) }
|
|
261
|
+
* warn(message: string) { fs.appendFileSync('warn.log', message) }
|
|
262
|
+
* error(message: string) { fs.appendFileSync('error.log', message) }
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export interface Logger {
|
|
267
|
+
/**
|
|
268
|
+
* Logs an informational message.
|
|
269
|
+
* @param message - The message to log
|
|
270
|
+
*/
|
|
271
|
+
info(message: string): void;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Logs a warning message.
|
|
275
|
+
* @param message - The warning message to log
|
|
276
|
+
*/
|
|
277
|
+
warn(message: string): void;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Logs an error message.
|
|
281
|
+
* @param message - The error message to log
|
|
282
|
+
*/
|
|
283
|
+
error(message: string): void;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Context object provided to plugins during AST traversal.
|
|
288
|
+
* Provides helper methods for plugins to interact with the extraction process.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* // Inside a plugin's onVisitNode hook:
|
|
293
|
+
* onVisitNode(node, context) {
|
|
294
|
+
* if (isCustomTranslationCall(node)) {
|
|
295
|
+
* context.addKey({
|
|
296
|
+
* key: extractKeyFromNode(node),
|
|
297
|
+
* defaultValue: extractDefaultFromNode(node),
|
|
298
|
+
* ns: 'custom'
|
|
299
|
+
* })
|
|
300
|
+
* }
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
export interface PluginContext {
|
|
305
|
+
/**
|
|
306
|
+
* Adds a translation key to the extraction results.
|
|
307
|
+
* Keys are automatically deduplicated by their namespace:key combination.
|
|
308
|
+
*
|
|
309
|
+
* @param keyInfo - The extracted key information
|
|
310
|
+
*/
|
|
311
|
+
addKey: (keyInfo: ExtractedKey) => void;
|
|
312
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures that the directory for a given file path exists.
|
|
6
|
+
* Creates all necessary parent directories recursively if they don't exist.
|
|
7
|
+
*
|
|
8
|
+
* @param filePath - The file path for which to ensure the directory exists
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* await ensureDirectoryExists('/path/to/nested/file.json')
|
|
13
|
+
* // Creates /path/to/nested/ directory if it doesn't exist
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export async function ensureDirectoryExists (filePath: string): Promise<void> {
|
|
17
|
+
const dir = dirname(filePath)
|
|
18
|
+
await mkdir(dir, { recursive: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reads a file asynchronously and returns its content as a UTF-8 string.
|
|
23
|
+
*
|
|
24
|
+
* @param filePath - The path to the file to read
|
|
25
|
+
* @returns Promise resolving to the file content as a string
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const content = await readFileAsync('./config.json')
|
|
30
|
+
* const config = JSON.parse(content)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export async function readFileAsync (filePath: string): Promise<string> {
|
|
34
|
+
return await readFile(filePath, 'utf-8')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Writes data to a file asynchronously.
|
|
39
|
+
*
|
|
40
|
+
* @param filePath - The path where to write the file
|
|
41
|
+
* @param data - The string data to write to the file
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const jsonData = JSON.stringify({ key: 'value' }, null, 2)
|
|
46
|
+
* await writeFileAsync('./output.json', jsonData)
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export async function writeFileAsync (filePath: string, data: string): Promise<void> {
|
|
50
|
+
await writeFile(filePath, data)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a file path by replacing template placeholders with actual values.
|
|
55
|
+
* Supports both legacy and modern placeholder formats for language and namespace.
|
|
56
|
+
*
|
|
57
|
+
* @param template - The template string containing placeholders
|
|
58
|
+
* @param locale - The locale/language code to substitute
|
|
59
|
+
* @param namespace - The namespace to substitute
|
|
60
|
+
* @returns The resolved file path with placeholders replaced
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // Modern format
|
|
65
|
+
* getOutputPath('locales/{{language}}/{{namespace}}.json', 'de', 'validation')
|
|
66
|
+
* // Returns: 'locales/de/validation.json'
|
|
67
|
+
*
|
|
68
|
+
* // Legacy format (also supported)
|
|
69
|
+
* getOutputPath('locales/{{lng}}/{{ns}}.json', 'en', 'common')
|
|
70
|
+
* // Returns: 'locales/en/common.json'
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function getOutputPath (
|
|
74
|
+
template: string,
|
|
75
|
+
locale: string,
|
|
76
|
+
namespace: string
|
|
77
|
+
): string {
|
|
78
|
+
return template
|
|
79
|
+
.replace('{{language}}', locale).replace('{{lng}}', locale)
|
|
80
|
+
.replace('{{namespace}}', namespace).replace('{{ns}}', namespace)
|
|
81
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Logger } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default console-based logger implementation for the i18next toolkit.
|
|
5
|
+
* Provides basic logging functionality with different severity levels.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const logger = new ConsoleLogger()
|
|
10
|
+
* logger.info('Extraction started')
|
|
11
|
+
* logger.warn('Deprecated configuration option used')
|
|
12
|
+
* logger.error('Failed to parse file')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class ConsoleLogger implements Logger {
|
|
16
|
+
/**
|
|
17
|
+
* Logs an informational message to the console.
|
|
18
|
+
*
|
|
19
|
+
* @param message - The message to log
|
|
20
|
+
*/
|
|
21
|
+
info (message: string): void { console.log(message) }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Logs a warning message to the console.
|
|
25
|
+
*
|
|
26
|
+
* @param message - The warning message to log
|
|
27
|
+
*/
|
|
28
|
+
warn (message: string): void { console.warn(message) }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Logs an error message to the console.
|
|
32
|
+
*
|
|
33
|
+
* @param message - The error message to log
|
|
34
|
+
*/
|
|
35
|
+
error (message: string): void { console.error(message) }
|
|
36
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets a nested value in an object using a key path and separator.
|
|
3
|
+
* Creates intermediate objects as needed.
|
|
4
|
+
*
|
|
5
|
+
* @param obj - The target object to modify
|
|
6
|
+
* @param path - The key path (e.g., 'user.profile.name')
|
|
7
|
+
* @param value - The value to set
|
|
8
|
+
* @param keySeparator - The separator to use for splitting the path, or false for flat keys
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const obj = {}
|
|
13
|
+
* setNestedValue(obj, 'user.profile.name', 'John', '.')
|
|
14
|
+
* // Result: { user: { profile: { name: 'John' } } }
|
|
15
|
+
*
|
|
16
|
+
* // With flat keys
|
|
17
|
+
* setNestedValue(obj, 'user.name', 'Jane', false)
|
|
18
|
+
* // Result: { 'user.name': 'Jane' }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function setNestedValue (
|
|
22
|
+
obj: Record<string, any>,
|
|
23
|
+
path: string,
|
|
24
|
+
value: any,
|
|
25
|
+
keySeparator: string | false
|
|
26
|
+
): void {
|
|
27
|
+
if (keySeparator === false) {
|
|
28
|
+
obj[path] = value
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const keys = path.split(keySeparator)
|
|
32
|
+
keys.reduce((acc, key, index) => {
|
|
33
|
+
if (index === keys.length - 1) {
|
|
34
|
+
acc[key] = value
|
|
35
|
+
} else {
|
|
36
|
+
acc[key] = acc[key] || {}
|
|
37
|
+
}
|
|
38
|
+
return acc[key]
|
|
39
|
+
}, obj)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves a nested value from an object using a key path and separator.
|
|
44
|
+
*
|
|
45
|
+
* @param obj - The object to search in
|
|
46
|
+
* @param path - The key path (e.g., 'user.profile.name')
|
|
47
|
+
* @param keySeparator - The separator to use for splitting the path, or false for flat keys
|
|
48
|
+
* @returns The found value or undefined if not found
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const obj = { user: { profile: { name: 'John' } } }
|
|
53
|
+
* const name = getNestedValue(obj, 'user.profile.name', '.')
|
|
54
|
+
* // Returns: 'John'
|
|
55
|
+
*
|
|
56
|
+
* // With flat keys
|
|
57
|
+
* const flatObj = { 'user.name': 'Jane' }
|
|
58
|
+
* const name = getNestedValue(flatObj, 'user.name', false)
|
|
59
|
+
* // Returns: 'Jane'
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function getNestedValue (
|
|
63
|
+
obj: Record<string, any>,
|
|
64
|
+
path: string,
|
|
65
|
+
keySeparator: string | false
|
|
66
|
+
): any {
|
|
67
|
+
if (keySeparator === false) {
|
|
68
|
+
return obj[path]
|
|
69
|
+
}
|
|
70
|
+
return path.split(keySeparator).reduce((acc, key) => acc && acc[key], obj)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extracts all nested keys from an object, optionally with a prefix.
|
|
75
|
+
* Recursively traverses the object structure to build a flat list of key paths.
|
|
76
|
+
*
|
|
77
|
+
* @param obj - The object to extract keys from
|
|
78
|
+
* @param keySeparator - The separator to use for joining keys, or false for flat keys
|
|
79
|
+
* @param prefix - Optional prefix to prepend to all keys
|
|
80
|
+
* @returns Array of all nested key paths
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const obj = {
|
|
85
|
+
* user: {
|
|
86
|
+
* profile: { name: 'John', age: 30 },
|
|
87
|
+
* settings: { theme: 'dark' }
|
|
88
|
+
* }
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* const keys = getNestedKeys(obj, '.')
|
|
92
|
+
* // Returns: ['user.profile.name', 'user.profile.age', 'user.settings.theme']
|
|
93
|
+
*
|
|
94
|
+
* // With flat keys
|
|
95
|
+
* const flatObj = { 'user.name': 'Jane', 'user.age': 25 }
|
|
96
|
+
* const flatKeys = getNestedKeys(flatObj, false)
|
|
97
|
+
* // Returns: ['user.name', 'user.age']
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function getNestedKeys (obj: object, keySeparator: string | false, prefix = ''): string[] {
|
|
101
|
+
if (keySeparator === false) {
|
|
102
|
+
return Object.keys(obj)
|
|
103
|
+
}
|
|
104
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
105
|
+
const newKey = prefix ? `${prefix}${keySeparator}${key}` : key
|
|
106
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
107
|
+
acc.push(...getNestedKeys(value, keySeparator, newKey))
|
|
108
|
+
} else {
|
|
109
|
+
acc.push(newKey)
|
|
110
|
+
}
|
|
111
|
+
return acc
|
|
112
|
+
}, [] as string[])
|
|
113
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { I18nextToolkitConfig } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates the extractor configuration to ensure required fields are present and properly formatted.
|
|
5
|
+
*
|
|
6
|
+
* This function performs the following validations:
|
|
7
|
+
* - Ensures extract.input is specified and non-empty
|
|
8
|
+
* - Ensures extract.output is specified
|
|
9
|
+
* - Ensures locales array is specified and non-empty
|
|
10
|
+
* - Ensures extract.output contains the required {{language}} placeholder
|
|
11
|
+
*
|
|
12
|
+
* @param config - The i18next toolkit configuration object to validate
|
|
13
|
+
*
|
|
14
|
+
* @throws {ExtractorError} When any validation rule fails
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* try {
|
|
19
|
+
* validateExtractorConfig(config)
|
|
20
|
+
* console.log('Configuration is valid')
|
|
21
|
+
* } catch (error) {
|
|
22
|
+
* console.error('Invalid configuration:', error.message)
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function validateExtractorConfig (config: I18nextToolkitConfig): void {
|
|
27
|
+
if (!config.extract.input?.length) {
|
|
28
|
+
throw new ExtractorError('extract.input must be specified and non-empty')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!config.extract.output) {
|
|
32
|
+
throw new ExtractorError('extract.output must be specified')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!config.locales?.length) {
|
|
36
|
+
throw new ExtractorError('locales must be specified and non-empty')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.extract.output.includes('{{language}}') && !config.extract.output.includes('{{lng}}')) {
|
|
40
|
+
throw new ExtractorError('extract.output must contain {{language}} placeholder')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Custom error class for extraction-related errors.
|
|
46
|
+
* Provides additional context like file path and underlying cause.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* throw new ExtractorError('Failed to parse file', 'src/component.tsx', syntaxError)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class ExtractorError extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* Creates a new ExtractorError with optional file context and cause.
|
|
56
|
+
*
|
|
57
|
+
* @param message - The error message
|
|
58
|
+
* @param file - Optional file path where the error occurred
|
|
59
|
+
* @param cause - Optional underlying error that caused this error
|
|
60
|
+
*/
|
|
61
|
+
constructor (
|
|
62
|
+
message: string,
|
|
63
|
+
public readonly file?: string,
|
|
64
|
+
public readonly cause?: Error
|
|
65
|
+
) {
|
|
66
|
+
super(file ? `${message} in file ${file}` : message)
|
|
67
|
+
this.name = 'ExtractorError'
|
|
68
|
+
}
|
|
69
|
+
}
|