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.
Files changed (127) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +489 -0
  4. package/dist/cjs/cli.js +2 -0
  5. package/dist/cjs/config.js +1 -0
  6. package/dist/cjs/extractor/core/extractor.js +1 -0
  7. package/dist/cjs/extractor/core/key-finder.js +1 -0
  8. package/dist/cjs/extractor/core/translation-manager.js +1 -0
  9. package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
  10. package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
  11. package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
  12. package/dist/cjs/extractor/plugin-manager.js +1 -0
  13. package/dist/cjs/heuristic-config.js +1 -0
  14. package/dist/cjs/index.js +1 -0
  15. package/dist/cjs/init.js +1 -0
  16. package/dist/cjs/linter.js +1 -0
  17. package/dist/cjs/locize.js +1 -0
  18. package/dist/cjs/migrator.js +1 -0
  19. package/dist/cjs/package.json +1 -0
  20. package/dist/cjs/status.js +1 -0
  21. package/dist/cjs/syncer.js +1 -0
  22. package/dist/cjs/types-generator.js +1 -0
  23. package/dist/cjs/utils/file-utils.js +1 -0
  24. package/dist/cjs/utils/logger.js +1 -0
  25. package/dist/cjs/utils/nested-object.js +1 -0
  26. package/dist/cjs/utils/validation.js +1 -0
  27. package/dist/esm/cli.js +2 -0
  28. package/dist/esm/config.js +1 -0
  29. package/dist/esm/extractor/core/extractor.js +1 -0
  30. package/dist/esm/extractor/core/key-finder.js +1 -0
  31. package/dist/esm/extractor/core/translation-manager.js +1 -0
  32. package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
  33. package/dist/esm/extractor/parsers/comment-parser.js +1 -0
  34. package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
  35. package/dist/esm/extractor/plugin-manager.js +1 -0
  36. package/dist/esm/heuristic-config.js +1 -0
  37. package/dist/esm/index.js +1 -0
  38. package/dist/esm/init.js +1 -0
  39. package/dist/esm/linter.js +1 -0
  40. package/dist/esm/locize.js +1 -0
  41. package/dist/esm/migrator.js +1 -0
  42. package/dist/esm/status.js +1 -0
  43. package/dist/esm/syncer.js +1 -0
  44. package/dist/esm/types-generator.js +1 -0
  45. package/dist/esm/utils/file-utils.js +1 -0
  46. package/dist/esm/utils/logger.js +1 -0
  47. package/dist/esm/utils/nested-object.js +1 -0
  48. package/dist/esm/utils/validation.js +1 -0
  49. package/package.json +81 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +158 -0
  52. package/src/extractor/core/extractor.ts +195 -0
  53. package/src/extractor/core/key-finder.ts +70 -0
  54. package/src/extractor/core/translation-manager.ts +115 -0
  55. package/src/extractor/index.ts +7 -0
  56. package/src/extractor/parsers/ast-visitors.ts +637 -0
  57. package/src/extractor/parsers/comment-parser.ts +125 -0
  58. package/src/extractor/parsers/jsx-parser.ts +166 -0
  59. package/src/extractor/plugin-manager.ts +54 -0
  60. package/src/extractor.ts +15 -0
  61. package/src/heuristic-config.ts +64 -0
  62. package/src/index.ts +12 -0
  63. package/src/init.ts +156 -0
  64. package/src/linter.ts +191 -0
  65. package/src/locize.ts +251 -0
  66. package/src/migrator.ts +139 -0
  67. package/src/status.ts +192 -0
  68. package/src/syncer.ts +114 -0
  69. package/src/types-generator.ts +116 -0
  70. package/src/types.ts +312 -0
  71. package/src/utils/file-utils.ts +81 -0
  72. package/src/utils/logger.ts +36 -0
  73. package/src/utils/nested-object.ts +113 -0
  74. package/src/utils/validation.ts +69 -0
  75. package/tryme.js +8 -0
  76. package/tsconfig.json +71 -0
  77. package/types/cli.d.ts +3 -0
  78. package/types/cli.d.ts.map +1 -0
  79. package/types/config.d.ts +50 -0
  80. package/types/config.d.ts.map +1 -0
  81. package/types/extractor/core/extractor.d.ts +66 -0
  82. package/types/extractor/core/extractor.d.ts.map +1 -0
  83. package/types/extractor/core/key-finder.d.ts +31 -0
  84. package/types/extractor/core/key-finder.d.ts.map +1 -0
  85. package/types/extractor/core/translation-manager.d.ts +31 -0
  86. package/types/extractor/core/translation-manager.d.ts.map +1 -0
  87. package/types/extractor/index.d.ts +8 -0
  88. package/types/extractor/index.d.ts.map +1 -0
  89. package/types/extractor/parsers/ast-visitors.d.ts +235 -0
  90. package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
  91. package/types/extractor/parsers/comment-parser.d.ts +24 -0
  92. package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
  93. package/types/extractor/parsers/jsx-parser.d.ts +35 -0
  94. package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
  95. package/types/extractor/plugin-manager.d.ts +37 -0
  96. package/types/extractor/plugin-manager.d.ts.map +1 -0
  97. package/types/extractor.d.ts +7 -0
  98. package/types/extractor.d.ts.map +1 -0
  99. package/types/heuristic-config.d.ts +10 -0
  100. package/types/heuristic-config.d.ts.map +1 -0
  101. package/types/index.d.ts +4 -0
  102. package/types/index.d.ts.map +1 -0
  103. package/types/init.d.ts +29 -0
  104. package/types/init.d.ts.map +1 -0
  105. package/types/linter.d.ts +33 -0
  106. package/types/linter.d.ts.map +1 -0
  107. package/types/locize.d.ts +5 -0
  108. package/types/locize.d.ts.map +1 -0
  109. package/types/migrator.d.ts +37 -0
  110. package/types/migrator.d.ts.map +1 -0
  111. package/types/status.d.ts +20 -0
  112. package/types/status.d.ts.map +1 -0
  113. package/types/syncer.d.ts +33 -0
  114. package/types/syncer.d.ts.map +1 -0
  115. package/types/types-generator.d.ts +29 -0
  116. package/types/types-generator.d.ts.map +1 -0
  117. package/types/types.d.ts +268 -0
  118. package/types/types.d.ts.map +1 -0
  119. package/types/utils/file-utils.d.ts +61 -0
  120. package/types/utils/file-utils.d.ts.map +1 -0
  121. package/types/utils/logger.d.ts +34 -0
  122. package/types/utils/logger.d.ts.map +1 -0
  123. package/types/utils/nested-object.d.ts +71 -0
  124. package/types/utils/nested-object.d.ts.map +1 -0
  125. package/types/utils/validation.d.ts +47 -0
  126. package/types/utils/validation.d.ts.map +1 -0
  127. 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
+ }
package/tryme.js ADDED
@@ -0,0 +1,8 @@
1
+ import { locize } from './dist/esm/index.js'
2
+ locize.runLocizeSync({
3
+ extract: {
4
+ output: 'tmp-test'
5
+ }
6
+ }, {
7
+ dryRun: true
8
+ })