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/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.0...v1.0.0) - 2025-xx-yy
9
+
10
+ - not yet released
11
+
12
+ ## [0.9.0] - 2025-09-25
13
+
14
+ ### Added
15
+
16
+ This is the initial public release of `i18next-cli`, a complete, high-performance replacement for `i18next-parser` and `i18next-scanner`.
17
+
18
+ #### Core Engine & Extractor
19
+ - Initial high-performance, SWC-based parsing engine for JavaScript and TypeScript.
20
+ - Advanced, scope-aware AST analysis for intelligent key extraction.
21
+ - Support for `t()` functions, `<Trans>` components, `useTranslation` hooks (including `keyPrefix` and aliasing), and `getFixedT`.
22
+ - Handles complex i18next features: namespaces (via ns-separator, options, and hooks), plurals, and context.
23
+ - Support for the type-safe selector API (`t($=>$.key.path)`).
24
+ - Extraction from commented-out code to support documentation-driven keys.
25
+
26
+ #### Commands
27
+ - **`init`**: Interactive wizard to create a new configuration file (`i18next.config.ts` or `.js`).
28
+ - **`extract`**: Extracts keys and updates translation files, with `--watch` and `--ci` modes.
29
+ - **`types`**: Generates TypeScript definitions for type-safe i18next usage and autocompletion.
30
+ - **`sync`**: Synchronizes secondary language files with a primary language file, adding missing keys and removing unused ones.
31
+ - **`lint`**: Lints the codebase for potential issues like hardcoded strings.
32
+ - **`status`**: Displays a project health dashboard with a summary view and a detailed, key-by-key view (`status [locale]`).
33
+ - **`migrate-config`**: Provides an automatic migration path from a legacy `i18next-parser.config.js`.
34
+ - **`locize-*`**: Full suite of commands (`sync`, `download`, `migrate`) for seamless integration with the locize TMS, including an interactive setup for credentials.
35
+
36
+ #### Configuration & DX
37
+ - **Zero-Config Mode**: `status` and `lint` commands work out-of-the-box on most projects by heuristically detecting the project structure.
38
+ - **Typed Configuration**: Fully-typed config file (`i18next.config.ts`) with a `defineConfig` helper.
39
+ - **Robust TS Support**: On-the-fly TypeScript config file loading (`.ts`) is supported in consumer projects via `jiti`.
40
+ - **Dynamic Key Support**: `preservePatterns` option to support dynamic keys.
41
+ - **Configurable Linter**: The linter can be customized with an `ignoredAttributes` option.
42
+ - **Polished UX**: Consistent `ora` spinners provide clear feedback on all asynchronous commands.
43
+ - **Dual CJS/ESM Support**: Modern package structure for broad compatibility.
44
+
45
+ #### Plugin System
46
+ - Initial plugin architecture with `setup`, `onLoad`, `onVisitNode`, and `onEnd` hooks, allowing for custom extraction logic and support for other file types (e.g., HTML, Handlebars).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present i18next
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,489 @@
1
+ # i18next-cli šŸš€
2
+
3
+ A unified, high-performance i18next CLI toolchain, powered by SWC.
4
+
5
+ [![Tests](https://github.com/i18next/i18next-cli/workflows/node/badge.svg)](https://github.com/i18next/i18next-cli/actions?query=workflow%3Anode)
6
+ [![npm version](https://img.shields.io/npm/v/i18next-cli.svg?style=flat-square)](https://www.npmjs.com/package/i18next-cli)
7
+
8
+ ---
9
+
10
+ `i18next-cli` is a complete reimagining of the static analysis toolchain for the i18next ecosystem. It consolidates key extraction, type safety generation, locale syncing, linting, and cloud integrations into a single, cohesive, and blazing-fast CLI.
11
+
12
+ > ### šŸš€ Try it Now - Zero Config!
13
+ > You can get an instant analysis of your existing i18next project **without any configuration**. Just run this command in your repository's root directory:
14
+ >
15
+ > ```bash
16
+ > npx i18next-cli status
17
+ > ```
18
+ > Or find hardcoded strings:
19
+ >
20
+ > ```bash
21
+ > npx i18next-cli lint
22
+ > ```
23
+
24
+ ## Why i18next-cli?
25
+
26
+ `i18next-cli` is built from the ground up to meet the demands of modern web development.
27
+
28
+ - **šŸš€ Performance:** By leveraging a native Rust-based parser (SWC), it delivers orders-of-magnitude faster performance than JavaScript-based parsers.
29
+ - **🧠 Intelligence:** A stateful, scope-aware analyzer correctly understands complex patterns like `useTranslation('ns1', { keyPrefix: '...' })`, `getFixedT`, and aliased `t` functions, minimizing the need for manual workarounds.
30
+ - **āœ… Unified Workflow:** One tool, one configuration file, one integrated workflow. It replaces various syncing scripts.
31
+ - **šŸ”Œ Extensibility:** A modern plugin architecture allows the tool to adapt to any framework or custom workflow.
32
+ - **šŸ§‘ā€šŸ’» Developer Experience:** A fully-typed configuration file, live `--watch` modes, CLI output, and a migration from legacy tools.
33
+
34
+ ## Features
35
+
36
+ - **Key Extraction**: Extract translation keys from JavaScript/TypeScript files with advanced AST analysis.
37
+ - **Type Safety**: Generate TypeScript definitions for full autocomplete and type safety.
38
+ - **Locale Synchronization**: Keep all language files in sync with your primary language.
39
+ - **Accurate Code Linting**: Detect hardcoded strings with high precision and configurable rules.
40
+ - **Translation Status**: Get a high-level overview or a detailed, key-by-key report of your project's translation completeness.
41
+ - **Plugin System**: Extensible architecture for custom extraction patterns and file types (e.g., HTML, Handlebars).
42
+ - **Legacy Migration**: Automatic migration from `i18next-parser` configurations.
43
+ - **Cloud Integration**: Seamless integration with the [locize](https://locize.com) translation management platform.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install --save-dev i18next-cli
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ### 1. Initialize Configuration
54
+
55
+ Create a configuration interactively:
56
+
57
+ ```bash
58
+ npx i18next-cli init
59
+ ```
60
+
61
+ Or manually create `i18next.config.ts` in your project root:
62
+
63
+ ```typescript
64
+ import { defineConfig } from 'i18next-cli';
65
+
66
+ export default defineConfig({
67
+ locales: ['en', 'de'],
68
+ extract: {
69
+ input: ['src/**/*.{js,jsx,ts,tsx}'],
70
+ output: 'public/locales/{{language}}/{{namespace}}.json',
71
+ },
72
+ });
73
+ ```
74
+
75
+ ### 2. Check your Translation Status
76
+
77
+ Get an overview of your project's localization health:
78
+
79
+ ```bash
80
+ npx i18next-cli status
81
+ ```
82
+
83
+ ### 3. Extract Translation Keys
84
+
85
+ ```bash
86
+ npx i18next-cli extract
87
+ ```
88
+
89
+ ### 4. Generate Types (Optional)
90
+
91
+ ```bash
92
+ npx i18next-cli types
93
+ ```
94
+
95
+ ## Commands
96
+
97
+ ### `init`
98
+ Interactive setup wizard to create your configuration file.
99
+
100
+ ```bash
101
+ npx i18next-cli init
102
+ ```
103
+
104
+ ### `extract`
105
+ Parses source files, extracts keys, and updates your JSON translation files.
106
+
107
+ ```bash
108
+ npx i18next-cli extract [options]
109
+ ```
110
+
111
+ **Options:**
112
+ - `--watch, -w`: Re-run automatically when files change
113
+ - `--ci`: Exit with non-zero status if any files are updated (for CI/CD)
114
+
115
+ **Examples:**
116
+ ```bash
117
+ # One-time extraction
118
+ npx i18next-cli extract
119
+
120
+ # Watch mode for development
121
+ npx i18next-cli extract --watch
122
+
123
+ # CI mode (fails if files changed)
124
+ npx i18next-cli extract --ci
125
+ ```
126
+
127
+ ### `status [locale]`
128
+
129
+ Displays a health check of your project's translation status. Can run without a config file.
130
+
131
+ - Run `npx i18next-cli status` for a high-level summary.
132
+ - Run `npx i18next-cli status <locale>` for a detailed, key-by-key report grouped by namespace.
133
+
134
+ This command provides:
135
+ - Total number of translation keys found in your source code
136
+ - Translation progress for each secondary language with visual progress bars
137
+ - Percentage and key counts for easy tracking
138
+
139
+ ### `types`
140
+ Generates TypeScript definitions from your translation files for full type-safety and autocompletion.
141
+
142
+ ```bash
143
+ npx i18next-cli types [options]
144
+ ```
145
+
146
+ **Options:**
147
+ - `--watch, -w`: Re-run automatically when translation files change
148
+
149
+ ### `sync`
150
+ Synchronizes secondary language files against your primary language file, adding missing keys and removing extraneous ones.
151
+
152
+ ```bash
153
+ npx i18next-cli sync
154
+ ```
155
+
156
+ ### `lint`
157
+ Analyzes your source code for internationalization issues like hardcoded strings. Can run without a config file.
158
+
159
+ ```bash
160
+ npx i18next-cli lint
161
+ ```
162
+
163
+ ### `migrate-config`
164
+ Automatically migrates a legacy `i18next-parser.config.js` file to the new `i18next.config.ts` format.
165
+
166
+ ```bash
167
+ npx i18next-cli migrate-config
168
+ ```
169
+
170
+ ### Locize Integration
171
+
172
+ **Prerequisites:** The locize commands require `locize-cli` to be installed:
173
+
174
+ ```bash
175
+ # Install globally (recommended)
176
+ npm install -g locize-cli
177
+ ```
178
+
179
+ Sync translations with the Locize translation management platform:
180
+
181
+ ```bash
182
+ # Download translations from Locize
183
+ npx i18next-cli locize-download
184
+
185
+ # Upload/sync translations to Locize
186
+ npx i18next-cli locize-sync
187
+
188
+ # Migrate local translations to Locize
189
+ npx i18next-cli locize-migrate
190
+ ```
191
+
192
+ **Locize Command Options:**
193
+
194
+ The `locize-sync` command supports additional options:
195
+
196
+ ```bash
197
+ npx i18next-cli locize-sync [options]
198
+ ```
199
+
200
+ **Options:**
201
+ - `--update-values`: Update values of existing translations on locize
202
+ - `--src-lng-only`: Check for changes in source language only
203
+ - `--compare-mtime`: Compare modification times when syncing
204
+ - `--dry-run`: Run the command without making any changes
205
+
206
+ **Interactive Setup:** If your locize credentials are missing or invalid, the toolkit will guide you through an interactive setup process to configure your Project ID, API Key, and version.
207
+
208
+ ## Configuration
209
+
210
+ The configuration file supports both TypeScript (`.ts`) and JavaScript (`.js`) formats. Use the `defineConfig` helper for type safety and IntelliSense.
211
+
212
+ ### Basic Configuration
213
+
214
+ ```typescript
215
+ // i18next.config.ts
216
+ import { defineConfig } from 'i18next-cli';
217
+
218
+ export default defineConfig({
219
+ locales: ['en', 'de', 'fr'],
220
+ extract: {
221
+ input: ['src/**/*.{ts,tsx,js,jsx}'],
222
+ output: 'locales/{{language}}/{{namespace}}.json',
223
+ },
224
+ });
225
+ ```
226
+
227
+ ### Advanced Configuration
228
+
229
+ ```typescript
230
+ import { defineConfig } from 'i18next-cli';
231
+
232
+ export default defineConfig({
233
+ locales: ['en', 'de', 'fr'],
234
+
235
+ // Key extraction settings
236
+ extract: {
237
+ input: ['src/**/*.{ts,tsx}'],
238
+ output: 'locales/{{language}}/{{namespace}}.json',
239
+
240
+ // Translation functions to detect
241
+ functions: ['t', 'i18n.t', 'i18next.t'],
242
+
243
+ // React components to analyze
244
+ transComponents: ['Trans', 'Translation'],
245
+
246
+ // useTranslation hook variations
247
+ useTranslationNames: ['useTranslation', 'useAppTranslation'],
248
+
249
+ // Add custom JSX attributes to ignore during linting
250
+ ignoredAttributes: ['data-testid', 'aria-label'],
251
+
252
+ // Namespace and key configuration
253
+ defaultNS: 'translation',
254
+ nsSeparator: ':',
255
+ keySeparator: '.',
256
+ contextSeparator: '_',
257
+ pluralSeparator: '_',
258
+
259
+ // Preserve dynamic keys matching patterns
260
+ preservePatterns: [
261
+ 'dynamic.feature.*',
262
+ 'generated.*.key'
263
+ ],
264
+
265
+ // Output formatting
266
+ sort: true,
267
+ indentation: 2,
268
+
269
+ // Primary language settings
270
+ primaryLanguage: 'en',
271
+ secondaryLanguages: ['de', 'fr'],
272
+
273
+ defaultValue: '', // Default value for missing keys
274
+ },
275
+
276
+ // TypeScript type generation
277
+ types: {
278
+ input: ['locales/en/*.json'],
279
+ output: 'src/types/i18next.d.ts',
280
+ resourcesFile: 'src/types/resources.d.ts',
281
+ enableSelector: true, // Enable type-safe key selection
282
+ },
283
+
284
+ // Locize integration
285
+ locize: {
286
+ projectId: 'your-project-id',
287
+ apiKey: process.env.LOCIZE_API_KEY, // Recommended: use environment variables
288
+ version: 'latest',
289
+ },
290
+
291
+ // Plugin system
292
+ plugins: [
293
+ // Add custom plugins here
294
+ ],
295
+ });
296
+ ```
297
+
298
+ ## Advanced Features
299
+
300
+ ### Plugin System
301
+
302
+ Create custom plugins to extend extraction capabilities. The plugin system is powerful enough to support non-JavaScript files (e.g., HTML, Handlebars) by using the `onEnd` hook with custom parsers.
303
+
304
+ ```typescript
305
+ import { defineConfig, Plugin } from 'i18next-cli';
306
+
307
+ const myCustomPlugin = (): Plugin => ({
308
+ name: 'my-custom-plugin',
309
+
310
+ async setup() {
311
+ // Initialize plugin
312
+ },
313
+
314
+ async onLoad(code: string, file: string) {
315
+ // Transform code before parsing
316
+ return code;
317
+ },
318
+
319
+ onVisitNode(node: any, context: PluginContext) {
320
+ // Custom AST node processing
321
+ if (node.type === 'CallExpression') {
322
+ // Extract custom translation patterns
323
+ context.addKey({
324
+ key: 'custom.key',
325
+ defaultValue: 'Custom Value',
326
+ ns: 'custom'
327
+ });
328
+ }
329
+ },
330
+
331
+ async onEnd(allKeys: Map<string, ExtractedKey>) {
332
+ // Process all extracted keys or add additional keys from non-JS files
333
+ // Example: Parse HTML files for data-i18n attributes
334
+ const htmlFiles = await glob('src/**/*.html');
335
+ for (const file of htmlFiles) {
336
+ const content = await readFile(file, 'utf-8');
337
+ const matches = content.match(/data-i18n="([^"]+)"/g) || [];
338
+ for (const match of matches) {
339
+ const key = match.replace(/data-i18n="([^"]+)"/, '$1');
340
+ allKeys.set(`translation:${key}`, { key, ns: 'translation' });
341
+ }
342
+ }
343
+ }
344
+ });
345
+
346
+ export default defineConfig({
347
+ locales: ['en', 'de'],
348
+ plugins: [myCustomPlugin()],
349
+ // ... other config
350
+ });
351
+ ```
352
+
353
+ ### Dynamic Key Preservation
354
+
355
+ Use `preservePatterns` to maintain dynamically generated keys:
356
+
357
+ ```typescript
358
+ // Code like this:
359
+ const key = `user.${role}.permission`;
360
+ t(key);
361
+
362
+ // With this config:
363
+ export default defineConfig({
364
+ extract: {
365
+ preservePatterns: ['user.*.permission']
366
+ }
367
+ });
368
+
369
+ // Will preserve existing keys matching the pattern
370
+ ```
371
+
372
+ ### Comment-Based Extraction
373
+
374
+ Extract keys from comments for documentation or edge cases:
375
+
376
+ ```javascript
377
+ // t('welcome.message', 'Welcome to our app!')
378
+ // t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
379
+ ```
380
+
381
+ ## Migration from i18next-parser
382
+
383
+ Automatically migrate from legacy `i18next-parser.config.js`:
384
+
385
+ ```bash
386
+ npx i18next-cli migrate-config
387
+ ```
388
+
389
+ This will:
390
+ - Convert your existing configuration to the new format
391
+ - Map old options to new equivalents
392
+ - Preserve custom settings where possible
393
+ - Create a new `i18next.config.ts` file
394
+
395
+ ## CI/CD Integration
396
+
397
+ Use the `--ci` flag to fail builds when translations are outdated:
398
+
399
+ ```yaml
400
+ # GitHub Actions example
401
+ - name: Check translations
402
+ run: npx i18next-cli extract --ci
403
+ ```
404
+
405
+ ## Watch Mode
406
+
407
+ For development, use watch mode to automatically update translations:
408
+
409
+ ```bash
410
+ npx i18next-cli extract --watch
411
+ ```
412
+
413
+ ## Type Safety
414
+
415
+ Generate TypeScript definitions for full type safety:
416
+
417
+ ```typescript
418
+ // Generated types enable autocomplete and validation
419
+ t('user.profile.name'); // āœ… Valid key
420
+ t('invalid.key'); // āŒ TypeScript error
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Supported Patterns
426
+
427
+ The toolkit automatically detects these i18next usage patterns:
428
+
429
+ ### Function Calls
430
+ ```javascript
431
+ // Basic usage
432
+ t('key')
433
+ t('key', 'Default value')
434
+ t('key', { defaultValue: 'Default' })
435
+
436
+ // With namespaces
437
+ t('ns:key')
438
+ t('key', { ns: 'namespace' })
439
+
440
+ // With interpolation
441
+ t('key', { name: 'John' })
442
+ ```
443
+
444
+ ### React Components
445
+ ```jsx
446
+ // Trans component
447
+ <Trans i18nKey="welcome">Welcome {{name}}</Trans>
448
+ <Trans ns="common">user.greeting</Trans>
449
+
450
+ // useTranslation hook
451
+ const { t } = useTranslation('namespace');
452
+ const { t } = useTranslation(['ns1', 'ns2']);
453
+ ```
454
+
455
+ ### Complex Patterns
456
+ ```javascript
457
+ // Aliased functions
458
+ const translate = t;
459
+ translate('key');
460
+
461
+ // Destructured hooks
462
+ const { t: translate } = useTranslation();
463
+
464
+ // getFixedT
465
+ const fixedT = getFixedT('en', 'namespace');
466
+ fixedT('key');
467
+ ```
468
+
469
+ ---
470
+
471
+ <h3 align="center">Gold Sponsors</h3>
472
+
473
+ <p align="center">
474
+ <a href="https://www.locize.com/" target="_blank">
475
+ <img src="https://raw.githubusercontent.com/i18next/i18next/master/assets/locize_sponsor_240.gif" width="240px">
476
+ </a>
477
+ </p>
478
+
479
+ ---
480
+
481
+ **From the creators of i18next: localization as a service - locize.com**
482
+
483
+ A translation management system built around the i18next ecosystem - [locize.com](https://www.locize.com).
484
+
485
+ ![locize](https://cdn.prod.website-files.com/67a323e323a50df7f24f0a6f/67b8bbb29365c3a3c21c0898_github_locize.png)
486
+
487
+ With using [locize](https://locize.com/?utm_source=i18next_readme&utm_medium=github) you directly support the future of i18next.
488
+
489
+ ---
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var e=require("commander"),o=require("chokidar"),t=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("node:path");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),f=require("./locize.js");const p=new e.Command;p.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.0"),p.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const o=await r.runExtractor(a);e.ci&&o&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("šŸ’” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),p.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").action(async e=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e})}),p.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),p.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),p.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),p.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),p.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await a.detectConfig();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=o}await d.runLinter(e)}),p.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeSync(o,e)}),p.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeDownload(o,e)}),p.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeMigrate(o,e)}),p.parse(process.argv);
@@ -0,0 +1 @@
1
+ "use strict";var e=require("node:path"),r=require("node:url"),n=require("node:fs/promises"),o=require("jiti"),t=require("inquirer"),i=require("chalk"),a=require("./init.js"),c="undefined"!=typeof document?document.currentScript:null;const s=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"];async function u(){const t=await async function(){for(const r of s){const o=e.resolve(process.cwd(),r);try{return await n.access(o),o}catch{}}return null}();if(!t)return null;try{let e;if(t.endsWith(".ts")){const r=o.createJiti("undefined"==typeof document?require("url").pathToFileURL(__filename).href:c&&"SCRIPT"===c.tagName.toUpperCase()&&c.src||new URL("config.js",document.baseURI).href),n=await r.import(t,{default:!0});e=n}else{const n=r.pathToFileURL(t).href,o=await import(`${n}?t=${Date.now()}`);e=o.default}return e?(e.extract||={},e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(r=>r!==e.extract.primaryLanguage),e):(console.error(`Error: No default export found in ${t}`),null)}catch(e){return console.error(`Error loading configuration from ${t}`),console.error(e),null}}exports.defineConfig=function(e){return e},exports.ensureConfig=async function(){let e=await u();if(e)return e;const{shouldInit:r}=await t.prompt([{type:"confirm",name:"shouldInit",message:i.yellow("Configuration file not found. Would you like to create one now?"),default:!0}]);if(r){if(await a.runInit(),console.log(i.green("Configuration created. Resuming command...")),e=await u(),e)return e;console.error(i.red("Error: Failed to load configuration after creation. Please try running the command again.")),process.exit(1)}else console.log("Operation cancelled. Please create a configuration file to proceed."),process.exit(0)},exports.loadConfig=u;
@@ -0,0 +1 @@
1
+ "use strict";var t=require("ora"),e=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),s=require("./key-finder.js"),o=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),u=require("../parsers/comment-parser.js"),l=require("../parsers/ast-visitors.js"),g=require("../../utils/logger.js");function f(t,e,r){if(t&&"object"==typeof t){for(const a of e)try{a.onVisitNode?.(t,r)}catch(t){console.warn(`Plugin ${a.name} onVisitNode failed:`,t)}for(const a of Object.keys(t)){const n=t[a];if(Array.isArray(n))for(const t of n)t&&"object"==typeof t&&f(t,e,r);else n&&"object"==typeof n&&f(n,e,r)}}}exports.extract=async function(t){t.extract.primaryLanguage||(t.extract.primaryLanguage=t.locales[0]),t.extract.secondaryLanguages||(t.extract.secondaryLanguages=t.locales.filter(e=>e!==t?.extract?.primaryLanguage)),t.extract.functions||(t.extract.functions=["t"]),t.extract.transComponents||(t.extract.transComponents=["Trans"]);const e=await s.findKeys(t);return o.getTranslations(e,t)},exports.processFile=async function(t,e,n,s){try{let o=await a.readFile(t,"utf-8");for(const r of e.plugins||[])o=await(r.onLoad?.(o,t))??o;const i=await r.parse(o,{syntax:"typescript",tsx:!0,comments:!0}),g=c.createPluginContext(s);u.extractKeysFromComments(o,e.extract.functions||["t"],g,e);new l.ASTVisitors(e,g,n).visit(i),(e.plugins||[]).length>0&&f(i,e.plugins||[],g)}catch(e){throw new i.ExtractorError("Failed to process file",t,e)}},exports.runExtractor=async function(r,c=new g.ConsoleLogger){r.extract.primaryLanguage||(r.extract.primaryLanguage=r.locales[0]||"en"),r.extract.secondaryLanguages||(r.extract.secondaryLanguages=r.locales.filter(t=>t!==r?.extract?.primaryLanguage)),i.validateExtractorConfig(r);const u=t("Running i18next key extractor...\n").start();try{const t=await s.findKeys(r,c);u.text=`Found ${t.size} unique keys. Updating translation files...`;const i=await o.getTranslations(t,r);let l=!1;for(const t of i)t.updated&&(l=!0,await a.mkdir(n.dirname(t.path),{recursive:!0}),await a.writeFile(t.path,JSON.stringify(t.newTranslations,null,2)),c.info(e.green(`Updated: ${t.path}`)));return u.succeed(e.bold("Extraction complete!")),l}catch(t){throw u.fail(e.red("Extraction failed.")),t}};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("glob"),n=require("./extractor.js"),r=require("../../utils/logger.js"),i=require("../plugin-manager.js");exports.findKeys=async function(o,s=new r.ConsoleLogger){const t=await async function(n){return await e.glob(n.extract.input,{ignore:"node_modules/**",cwd:process.cwd()})}(o),a=new Map;await i.initializePlugins(o.plugins||[]);for(const e of t)await n.processFile(e,o,s,a);for(const e of o.plugins||[])await(e.onEnd?.(a));return a};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("node:fs/promises"),t=require("node:path"),a=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function r(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}exports.getTranslations=async function(n,o){const c=o.extract.defaultNS??"translation",u=o.extract.keySeparator??".",l=(o.extract.preservePatterns??[]).map(r);o.extract.primaryLanguage||(o.extract.primaryLanguage=o.locales[0]||"en"),o.extract.secondaryLanguages||(o.extract.secondaryLanguages=o.locales.filter(e=>e!==o.extract.primaryLanguage));const i=new Map;for(const e of n.values()){const t=e.ns||c;i.has(t)||i.set(t,[]),i.get(t).push(e)}const g=[];for(const r of o.locales)for(const[n,c]of i.entries()){const i=s.getOutputPath(o.extract.output,r,n),p=t.resolve(process.cwd(),i);let f="",d={};try{f=await e.readFile(p,"utf-8"),d=JSON.parse(f)}catch(e){}const x={},y=a.getNestedKeys(d,u);for(const e of y)if(l.some(t=>t.test(e))){const t=a.getNestedValue(d,e,u);a.setNestedValue(x,e,t,u)}const m=!1===o.extract.sort?c:c.sort((e,t)=>e.key.localeCompare(t.key));for(const{key:e,defaultValue:t}of m){const s=a.getNestedValue(d,e,u)??(r===o.extract?.primaryLanguage?t:"");a.setNestedValue(x,e,s,u)}const N=o.extract.indentation??2,h=JSON.stringify(x,null,N);g.push({path:p,updated:h!==f,newTranslations:x,existingTranslations:d})}return g};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;"ObjectExpression"===r?.type&&(s=this.getObjectPropValue(r,"keyPrefix")),this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=(this.config.extract.functions||[]).includes(t.value),i=this.getVarFromScope(t.value);if(!n&&!(void 0!==i))return;if(0===e.arguments.length)return;const r=e.arguments[0].expression;let s,a=null;if("StringLiteral"===r.type?a=r.value:"ArrowFunctionExpression"===r.type&&(a=this.extractKeyFromSelector(r)),!a)return;let o=a;const l=e.arguments.length>1?e.arguments[1].expression:void 0;"ObjectExpression"===l?.type&&(s=this.getObjectPropValue(l,"ns")),!s&&i?.defaultNs&&(s=i.defaultNs);const p=this.config.extract.nsSeparator??":",u=this.config.extract.contextSeparator??"_";if(!s&&p&&a.includes(p)){const e=a.split(p);s=e.shift(),a=e.join(p),o=a}if(s||(s=this.config.extract.defaultNS),i?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${i.keyPrefix}${e}${a}`}const c="StringLiteral"===r.type?this.getDefaultValue(e,a):a;if("ObjectExpression"===l?.type){const e=this.getObjectPropValue(l,"context");if(e)return void this.pluginContext.addKey({key:`${o}${u}${e}`,ns:s,defaultValue:c});if(void 0!==this.getObjectPropValue(l,"count"))return void this.handlePluralKeys(o,c,s)}this.pluginContext.addKey({key:o,ns:s,defaultValue:c})}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}getDefaultValue(e,t){if(e.arguments.length<=1)return t;const n=e.arguments[1].expression;return"StringLiteral"===n.type?n.value||t:"ObjectExpression"===n.type&&this.getObjectPropValue(n,"defaultValue")||t}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}n.ns||(n.ns=this.config.extract.defaultNS),n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}};
@@ -0,0 +1 @@
1
+ "use strict";function e(e){const t=/^\s*,\s*(['"])(.*?)\1/.exec(e);if(t)return t[2];const s=/^\s*,\s*\{[^}]*defaultValue\s*:\s*(['"])(.*?)\1/.exec(e);return s?s[2]:void 0}function t(e){const t=/^\s*,\s*\{[^}]*ns\s*:\s*(['"])(.*?)\1/.exec(e);if(t)return t[2]}exports.extractKeysFromComments=function(s,n,c,o){const r=n.map(e=>e.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join("|"),u=new RegExp(`(?:${r})\\s*\\(\\s*(['"])([^'"]+)\\1`,"g"),i=function(e){const t=[],s=new Set,n=/\/\/(.*)|\/\*([\s\S]*?)\*\//g;let c;for(;null!==(c=n.exec(e));){const e=(c[1]??c[2]).trim();e&&!s.has(e)&&(s.add(e),t.push(e))}return t}(s);for(const s of i){let n;for(;null!==(n=u.exec(s));){let r,u=n[2];const i=s.slice(n.index+n[0].length),l=e(i);r=t(i);const a=o.extract.nsSeparator??":";if(!r&&a&&u.includes(a)){const e=u.split(a);r=e.shift(),u=e.join(a)}r||(r=o.extract.defaultNS),c.addKey({key:u,ns:r,defaultValue:l??u})}}};
@@ -0,0 +1 @@
1
+ "use strict";function e(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);return function e(t){let i="";return t.forEach((t,r)=>{if("JSXText"===t.type)i+=t.value;else if("JSXExpressionContainer"===t.type){const e=t.expression;if("StringLiteral"===e.type)i+=e.value;else if("Identifier"===e.type)i+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const t=e.properties[0];t&&"Identifier"===t.type&&(i+=`{{${t.value}}}`)}}else if("JSXElement"===t.type){let a;"Identifier"===t.opening.name.type&&(a=t.opening.name.value);const l=e(t.children);a&&n.has(a)?i+=`<${a}>${l}</${a}>`:i+=`<${r}>${l}</${r}>`}else"JSXFragment"===t.type&&(i+=e(t.children))}),i}(e).trim().replace(/\s{2,}/g," ")}exports.extractFromTransComponent=function(t,n){const i=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),l=!!a;let u;if(u="JSXAttribute"===i?.type&&"StringLiteral"===i.value?.type?i.value.value:e(t.children,n),!u)return null;const p=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value),s="JSXAttribute"===p?.type&&"StringLiteral"===p.value?.type?p.value.value:void 0;let o=n.extract.defaultValue||"";return o="JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type?r.value.value:e(t.children,n),{key:u,ns:s,defaultValue:o||u,hasCount:l}};
@@ -0,0 +1 @@
1
+ "use strict";exports.createPluginContext=function(t){return{addKey:e=>{const n=`${e.ns??"translation"}:${e.key}`;if(!t.has(n)){const s=e.defaultValue??e.key;t.set(n,{...e,defaultValue:s})}}}},exports.initializePlugins=async function(t){for(const e of t)await(e.setup?.())};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const o=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json"];exports.detectConfig=async function(){for(const t of o){const o=await e.glob(t,{ignore:"node_modules/**"});if(o.length>0){const e=o[0],t=n.dirname(n.dirname(e));try{let e=(await s.readdir(t)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}"],output:n.join(t,"{{language}}","{{namespace}}.json"),primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./config.js"),r=require("./extractor/core/extractor.js"),t=require("./extractor/core/key-finder.js"),o=require("./extractor/core/translation-manager.js");exports.defineConfig=e.defineConfig,exports.extract=r.extract,exports.findKeys=t.findKeys,exports.getTranslations=o.getTranslations;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("inquirer"),t=require("node:fs/promises"),n=require("node:path");exports.runInit=async function(){console.log("Welcome to the i18next-cli setup wizard!");const i=await e.prompt([{type:"list",name:"fileType",message:"What kind of configuration file do you want?",choices:["TypeScript (i18next.config.ts)","JavaScript (i18next.config.js)"]},{type:"input",name:"locales",message:"What locales does your project support? (comma-separated)",default:"en,de,fr",filter:e=>e.split(",").map(e=>e.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:"public/locales/{{language}}/{{namespace}}.json"}]),o=i.fileType.includes("TypeScript"),r=await async function(){try{const e=n.resolve(process.cwd(),"package.json"),i=await t.readFile(e,"utf-8");return"module"===JSON.parse(i).type}catch{return!0}}(),s=o?"i18next.config.ts":"i18next.config.js",a={locales:i.locales,extract:{input:i.input,output:i.output}};function p(e,t=2,n=0){const i=e=>" ".repeat(e*t),o=i(n),r=i(n+1);if(null===e||"number"==typeof e||"boolean"==typeof e)return JSON.stringify(e);if("string"==typeof e)return JSON.stringify(e);if(Array.isArray(e)){if(0===e.length)return"[]";return`[\n${e.map(e=>`${r}${p(e,t,n+1)}`).join(",\n")}\n${o}]`}if("object"==typeof e){const i=Object.keys(e);if(0===i.length)return"{}";return`{\n${i.map(i=>{const o=/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(i)?i:JSON.stringify(i);return`${r}${o}: ${p(e[i],t,n+1)}`}).join(",\n")}\n${o}}`}return JSON.stringify(e)}let f="";f=o?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${p(a)});`:r?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${p(a)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${p(a)});`;const u=n.resolve(process.cwd(),s);await t.writeFile(u,f.trim()),console.log(`āœ… Configuration file created at: ${u}`)};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("glob"),t=require("node:fs/promises"),r=require("@swc/core"),n=require("swc-walk"),s=require("chalk"),o=require("ora");function i(e,t,r){const s=[],o=[0];for(let e=0;e<t.length;e++)"\n"===t[e]&&o.push(e+1);const i=e=>{let t=1;for(let r=0;r<o.length&&!(o[r]>e);r++)t=r+1;return t},a=r.extract.transComponents||["Trans"],l=r.extract.ignoredAttributes||[],c=new Set(["className","key","id","style","href","i18nKey","defaults","type",...l]);return n.ancestor(e,{JSXText(e,t){const r=t[t.length-2],n=r?.opening?.name?.value;if(n&&(a.includes(n)||"script"===n||"style"===n))return;if(t.some(e=>{if("JSXElement"!==e.type)return!1;const t=e.opening?.name?.value;return a.includes(t)||["script","style","code"].includes(t)}))return;const o=e.value.trim();o&&isNaN(Number(o))&&!o.startsWith("{{")&&s.push({text:o,line:i(e.span.start)})},StringLiteral(e,t){const r=t[t.length-2];if("JSXAttribute"===r?.type&&!c.has(r.name.value)){const t=e.value.trim();t&&isNaN(Number(t))&&s.push({text:t,line:i(e.span.start)})}}}),s}exports.runLinter=async function(n){const a=o("Analyzing source files...\n").start();try{const o=await e.glob(n.extract.input);let l=0;const c=new Map;for(const e of o){const s=await t.readFile(e,"utf-8"),o=i(await r.parse(s,{syntax:"typescript",tsx:!0}),s,n);o.length>0&&(l+=o.length,c.set(e,o))}if(l>0){a.fail(s.red.bold(`Linter found ${l} potential issues.`));for(const[e,t]of c.entries())console.log(s.yellow(`\n${e}`)),t.forEach(({text:e,line:t})=>{console.log(` ${s.gray(`${t}:`)} ${s.red("Error:")} Found hardcoded string: "${e}"`)});process.exit(1)}else a.succeed(s.green.bold("No issues found."))}catch(e){a.fail(s.red("Linter failed to run.")),console.error(e),process.exit(1)}};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),r=require("node:path");function i(e,o={},n={}){const t=[];if("sync"===e){(o.updateValues??n.updateValues)&&t.push("--update-values","true");(o.srcLngOnly??n.sourceLanguageOnly)&&t.push("--reference-language-only","true");(o.compareMtime??n.compareModificationTime)&&t.push("--compare-modification-time","true");(o.dryRun??n.dryRun)&&t.push("--dry","true")}return t}async function s(s,c,a={}){await async function(){try{await e.execa("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const l=n(`Running 'locize ${s}'...\n`).start(),u=c.locize||{},{projectId:p,apiKey:d,version:y}=u;let g=[s];p&&g.push("--project-id",p),d&&g.push("--api-key",d),y&&g.push("--ver",y),g.push(...i(s,a,u));const m=r.resolve(process.cwd(),c.extract.output.split("/{{language}}/")[0]);g.push("--path",m);try{console.log(o.cyan(`\nRunning 'locize ${g.join(" ")}'...`));const n=await e.execa("locize",g,{stdio:"pipe"});l.succeed(o.green(`'locize ${s}' completed successfully.`)),n?.stdout&&console.log(n.stdout)}catch(n){const r=n.stderr||"";if(r.includes("missing required argument")){const n=await async function(e){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const n=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!n.projectId)return void console.error(o.red("Project ID is required to continue."));e.locize={projectId:n.projectId,apiKey:n.apiKey,version:n.version};const{save:r}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(r){const e=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${n.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${n.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${n.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(e)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return e.locize}(c);if(n){g=[s],n.projectId&&g.push("--project-id",n.projectId),n.apiKey&&g.push("--api-key",n.apiKey),n.version&&g.push("--ver",n.version),g.push(...i(s,a,u)),g.push("--path",m);try{l.start("Retrying with new credentials...");const n=await e.execa("locize",g,{stdio:"pipe"});l.succeed(o.green("Retry successful!")),n?.stdout&&console.log(n.stdout)}catch(e){l.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else l.fail("Operation cancelled."),process.exit(1)}else l.fail(o.red(`Error executing 'locize ${s}'.`)),console.error(r||n.message),process.exit(1)}console.log(o.green(`\nāœ… 'locize ${s}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>s("download",e,o),exports.runLocizeMigrate=(e,o)=>s("migrate",e,o),exports.runLocizeSync=(e,o)=>s("sync",e,o);