i18next-cli 1.33.5 → 1.34.1

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 (65) hide show
  1. package/README.md +2 -1
  2. package/dist/cjs/cli.js +271 -1
  3. package/dist/cjs/config.js +211 -1
  4. package/dist/cjs/extractor/core/ast-visitors.js +364 -1
  5. package/dist/cjs/extractor/core/extractor.js +245 -1
  6. package/dist/cjs/extractor/core/key-finder.js +132 -1
  7. package/dist/cjs/extractor/core/translation-manager.js +745 -1
  8. package/dist/cjs/extractor/parsers/ast-utils.js +85 -1
  9. package/dist/cjs/extractor/parsers/call-expression-handler.js +941 -1
  10. package/dist/cjs/extractor/parsers/comment-parser.js +375 -1
  11. package/dist/cjs/extractor/parsers/expression-resolver.js +362 -1
  12. package/dist/cjs/extractor/parsers/jsx-handler.js +492 -1
  13. package/dist/cjs/extractor/parsers/jsx-parser.js +355 -1
  14. package/dist/cjs/extractor/parsers/scope-manager.js +408 -1
  15. package/dist/cjs/extractor/plugin-manager.js +106 -1
  16. package/dist/cjs/heuristic-config.js +99 -1
  17. package/dist/cjs/index.js +28 -1
  18. package/dist/cjs/init.js +174 -1
  19. package/dist/cjs/linter.js +431 -1
  20. package/dist/cjs/locize.js +269 -1
  21. package/dist/cjs/migrator.js +196 -1
  22. package/dist/cjs/rename-key.js +354 -1
  23. package/dist/cjs/status.js +336 -1
  24. package/dist/cjs/syncer.js +120 -1
  25. package/dist/cjs/types-generator.js +165 -1
  26. package/dist/cjs/utils/default-value.js +43 -1
  27. package/dist/cjs/utils/file-utils.js +136 -1
  28. package/dist/cjs/utils/funnel-msg-tracker.js +75 -1
  29. package/dist/cjs/utils/logger.js +36 -1
  30. package/dist/cjs/utils/nested-object.js +124 -1
  31. package/dist/cjs/utils/validation.js +71 -1
  32. package/dist/esm/cli.js +269 -1
  33. package/dist/esm/config.js +206 -1
  34. package/dist/esm/extractor/core/ast-visitors.js +362 -1
  35. package/dist/esm/extractor/core/extractor.js +241 -1
  36. package/dist/esm/extractor/core/key-finder.js +130 -1
  37. package/dist/esm/extractor/core/translation-manager.js +743 -1
  38. package/dist/esm/extractor/parsers/ast-utils.js +80 -1
  39. package/dist/esm/extractor/parsers/call-expression-handler.js +939 -1
  40. package/dist/esm/extractor/parsers/comment-parser.js +373 -1
  41. package/dist/esm/extractor/parsers/expression-resolver.js +360 -1
  42. package/dist/esm/extractor/parsers/jsx-handler.js +490 -1
  43. package/dist/esm/extractor/parsers/jsx-parser.js +334 -1
  44. package/dist/esm/extractor/parsers/scope-manager.js +406 -1
  45. package/dist/esm/extractor/plugin-manager.js +103 -1
  46. package/dist/esm/heuristic-config.js +97 -1
  47. package/dist/esm/index.js +11 -1
  48. package/dist/esm/init.js +172 -1
  49. package/dist/esm/linter.js +425 -1
  50. package/dist/esm/locize.js +265 -1
  51. package/dist/esm/migrator.js +194 -1
  52. package/dist/esm/rename-key.js +352 -1
  53. package/dist/esm/status.js +334 -1
  54. package/dist/esm/syncer.js +118 -1
  55. package/dist/esm/types-generator.js +163 -1
  56. package/dist/esm/utils/default-value.js +41 -1
  57. package/dist/esm/utils/file-utils.js +131 -1
  58. package/dist/esm/utils/funnel-msg-tracker.js +72 -1
  59. package/dist/esm/utils/logger.js +34 -1
  60. package/dist/esm/utils/nested-object.js +120 -1
  61. package/dist/esm/utils/validation.js +68 -1
  62. package/package.json +4 -2
  63. package/types/extractor/core/extractor.d.ts.map +1 -1
  64. package/types/extractor/parsers/jsx-parser.d.ts.map +1 -1
  65. package/types/locize.d.ts.map +1 -1
@@ -1 +1,265 @@
1
- import{execa as e}from"execa";import o from"chalk";import t from"ora";import n from"inquirer";import{sep as r,resolve as s}from"node:path";function i(e,o,t){const{locize:n={},extract:i}=o,{projectId:c,apiKey:a,version:l}=n,u=[e];if(c&&u.push("--project-id",c),a&&u.push("--api-key",a),l&&u.push("--ver",l),"sync"===e){(t.updateValues??n.updateValues)&&u.push("--update-values","true");(t.srcLngOnly??n.sourceLanguageOnly)&&u.push("--reference-language-only","true");(t.compareMtime??n.compareModificationTime)&&u.push("--compare-modification-time","true");const e=t.cdnType??n.cdnType;e&&u.push("--cdn-type",e);(t.dryRun??n.dryRun)&&u.push("--dry","true")}let p;try{if("string"==typeof i.output){const e=i.output.replace(/\\/g,"/"),o=(e.includes("/{{language}}/")?e.split("/{{language}}/")[0]:e.replace("{{language}}","")).split("/").join(r);p=s(process.cwd(),o)}else if("function"==typeof i.output)try{const e=i.output(o.extract.primaryLanguage||"en"),t=String(e).replace(/\\/g,"/"),n=t.includes("/"+(o.extract.primaryLanguage||"en")+"/")?t.split("/"+(o.extract.primaryLanguage||"en")+"/")[0]:t.replace(o.extract.primaryLanguage||"en","");p=s(process.cwd(),n.split("/").join(r))}catch{p=s(process.cwd(),".")}else p=s(process.cwd(),".")}catch{p=s(process.cwd(),".")}return u.push("--path",p),u}async function c(r,s,c={}){await async function(){try{await e("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 a=t(`Running 'locize ${r}'...\n`).start();let l=s;try{const t=i(r,l,c);console.log(o.cyan(`\nRunning 'locize ${t.join(" ")}'...`));const n=await e("locize",t,{stdio:"pipe"});a.succeed(o.green(`'locize ${r}' completed successfully.`)),n?.stdout&&console.log(n.stdout)}catch(t){const s=t.stderr||"";if(s.includes("missing required argument")){const t=await async function(){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const e=await n.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(!e.projectId)return void console.error(o.red("Project ID is required to continue."));const{save:t}=await n.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(t){const t=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${e.apiKey}\n`,n=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${e.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${e.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(t)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(n))}return{projectId:e.projectId,apiKey:e.apiKey,version:e.version}}();if(t){l={...l,locize:t},a.start("Retrying with new credentials...");try{const t=i(r,l,c);console.log(o.cyan(`\nRunning 'locize ${t.join(" ")}'...`));const n=await e("locize",t,{stdio:"pipe"});a.succeed(o.green("Retry successful!")),n?.stdout&&console.log(n.stdout)}catch(e){a.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else a.fail("Operation cancelled."),process.exit(1)}else a.fail(o.red(`Error executing 'locize ${r}'.`)),console.error(s||t.message),process.exit(1)}console.log(o.green(`\n✅ 'locize ${r}' completed successfully.`))}const a=(e,o)=>c("sync",e,o),l=(e,o)=>c("download",e,o),u=(e,o)=>c("migrate",e,o);export{l as runLocizeDownload,u as runLocizeMigrate,a as runLocizeSync};
1
+ import { execa } from 'execa';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import inquirer from 'inquirer';
5
+ import { sep, resolve } from 'node:path';
6
+
7
+ /**
8
+ * Verifies that the locize-cli tool is installed and accessible.
9
+ *
10
+ * @throws Exits the process with error code 1 if locize-cli is not found
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * await checkLocizeCliExists()
15
+ * // Continues execution if locize-cli is available
16
+ * // Otherwise exits with installation instructions
17
+ * ```
18
+ */
19
+ async function checkLocizeCliExists() {
20
+ try {
21
+ await execa('locize', ['--version']);
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT') {
25
+ console.error(chalk.red('Error: `locize-cli` command not found.'));
26
+ console.log(chalk.yellow('Please install it globally to use the locize integration:'));
27
+ console.log(chalk.cyan('npm install -g locize-cli'));
28
+ process.exit(1);
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Interactive setup wizard for configuring Locize credentials.
34
+ *
35
+ * This function guides users through setting up their Locize integration when
36
+ * configuration is missing or invalid. It:
37
+ * 1. Prompts for Project ID, API Key, and version
38
+ * 2. Validates required fields
39
+ * 3. Temporarily sets credentials for the current run
40
+ * 4. Provides security recommendations for storing credentials
41
+ * 5. Shows code examples for proper configuration
42
+ *
43
+ * @param config - Configuration object to update with new credentials
44
+ * @returns Promise resolving to the Locize configuration or undefined if setup was cancelled
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const locizeConfig = await interactiveCredentialSetup(config)
49
+ * if (locizeConfig) {
50
+ * // Proceed with sync using the new credentials
51
+ * }
52
+ * ```
53
+ */
54
+ async function interactiveCredentialSetup(config) {
55
+ console.log(chalk.yellow('\nLocize configuration is missing or invalid. Let\'s set it up!'));
56
+ const answers = await inquirer.prompt([
57
+ {
58
+ type: 'input',
59
+ name: 'projectId',
60
+ message: 'What is your locize Project ID? (Find this in your project settings on www.locize.app)',
61
+ validate: input => !!input || 'Project ID cannot be empty.',
62
+ },
63
+ {
64
+ type: 'password',
65
+ name: 'apiKey',
66
+ message: 'What is your locize API key? (Create or use one in your project settings > "API Keys")',
67
+ validate: input => !!input || 'API Key cannot be empty.',
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'version',
72
+ message: 'What version do you want to sync with?',
73
+ default: 'latest',
74
+ },
75
+ ]);
76
+ if (!answers.projectId) {
77
+ console.error(chalk.red('Project ID is required to continue.'));
78
+ return undefined;
79
+ }
80
+ const { save } = await inquirer.prompt([{
81
+ type: 'confirm',
82
+ name: 'save',
83
+ message: 'Would you like to see how to save these credentials for future use?',
84
+ default: true,
85
+ }]);
86
+ if (save) {
87
+ const envSnippet = `
88
+ # Add this to your .env file (and ensure .env is in your .gitignore!)
89
+ LOCIZE_API_KEY=${answers.apiKey}
90
+ `;
91
+ const configSnippet = `
92
+ // Add this to your i18next.config.ts file
93
+ locize: {
94
+ projectId: '${answers.projectId}',
95
+ // For security, apiKey is best set via an environment variable
96
+ apiKey: process.env.LOCIZE_API_KEY,
97
+ version: '${answers.version}',
98
+ },`;
99
+ console.log(chalk.cyan('\nGreat! For the best security, we recommend using environment variables for your API key.'));
100
+ console.log(chalk.bold('\nRecommended approach (.env file):'));
101
+ console.log(chalk.green(envSnippet));
102
+ console.log(chalk.bold('Then, in your i18next.config.ts:'));
103
+ console.log(chalk.green(configSnippet));
104
+ }
105
+ return {
106
+ projectId: answers.projectId,
107
+ apiKey: answers.apiKey,
108
+ version: answers.version,
109
+ };
110
+ }
111
+ /**
112
+ * Helper function to build the array of arguments for the execa call.
113
+ * This ensures the logic is consistent for both the initial run and the retry.
114
+ */
115
+ function buildArgs(command, config, cliOptions) {
116
+ const { locize: locizeConfig = {}, extract } = config;
117
+ const commandArgs = [command];
118
+ const projectId = cliOptions.projectId ?? locizeConfig.projectId;
119
+ if (projectId)
120
+ commandArgs.push('--project-id', projectId);
121
+ const apiKey = cliOptions.apiKey ?? locizeConfig.apiKey;
122
+ if (apiKey)
123
+ commandArgs.push('--api-key', apiKey);
124
+ const version = cliOptions.version ?? locizeConfig.version;
125
+ if (version)
126
+ commandArgs.push('--ver', version);
127
+ const cdnType = cliOptions.cdnType ?? locizeConfig.cdnType;
128
+ if (cdnType)
129
+ commandArgs.push('--cdn-type', cdnType);
130
+ // TODO: there might be more configurable locize-cli options in future
131
+ // Pass-through options from the CLI
132
+ if (command === 'sync') {
133
+ const updateValues = cliOptions.updateValues ?? locizeConfig.updateValues;
134
+ if (updateValues)
135
+ commandArgs.push('--update-values', 'true');
136
+ const srcLngOnly = cliOptions.srcLngOnly ?? locizeConfig.sourceLanguageOnly;
137
+ if (srcLngOnly)
138
+ commandArgs.push('--reference-language-only', 'true');
139
+ const compareMtime = cliOptions.compareMtime ?? locizeConfig.compareModificationTime;
140
+ if (compareMtime)
141
+ commandArgs.push('--compare-modification-time', 'true');
142
+ const dryRun = cliOptions.dryRun ?? locizeConfig.dryRun;
143
+ if (dryRun)
144
+ commandArgs.push('--dry', 'true');
145
+ }
146
+ // Derive a sensible base path for locize from the configured output.
147
+ // If output is a string template we can strip the language placeholder.
148
+ // If output is a function we cannot reliably infer the base; fall back to cwd.
149
+ let basePath;
150
+ try {
151
+ if (typeof extract.output === 'string') {
152
+ const outputNormalized = extract.output.replace(/\\/g, '/');
153
+ const baseCandidate = outputNormalized.includes('/{{language}}/')
154
+ ? outputNormalized.split('/{{language}}/')[0]
155
+ : outputNormalized.replace('{{language}}', '');
156
+ const baseCandidateWithSep = baseCandidate.split('/').join(sep);
157
+ basePath = resolve(process.cwd(), baseCandidateWithSep);
158
+ }
159
+ else if (typeof extract.output === 'function') {
160
+ // Try calling the function with the primary language to get an example path,
161
+ // then strip the language folder if present. If that fails, fallback to cwd.
162
+ try {
163
+ const sample = extract.output(config.extract.primaryLanguage || 'en');
164
+ const sampleNormalized = String(sample).replace(/\\/g, '/');
165
+ const baseCandidate = sampleNormalized.includes('/' + (config.extract.primaryLanguage || 'en') + '/')
166
+ ? sampleNormalized.split('/' + (config.extract.primaryLanguage || 'en') + '/')[0]
167
+ : sampleNormalized.replace(config.extract.primaryLanguage || 'en', '');
168
+ basePath = resolve(process.cwd(), baseCandidate.split('/').join(sep));
169
+ }
170
+ catch {
171
+ basePath = resolve(process.cwd(), '.');
172
+ }
173
+ }
174
+ else {
175
+ basePath = resolve(process.cwd(), '.');
176
+ }
177
+ }
178
+ catch {
179
+ basePath = resolve(process.cwd(), '.');
180
+ }
181
+ commandArgs.push('--path', basePath);
182
+ return commandArgs;
183
+ }
184
+ /**
185
+ * Executes a locize-cli command with proper error handling and credential management.
186
+ *
187
+ * This is the core function that:
188
+ * 1. Validates that locize-cli is installed
189
+ * 2. Builds command arguments from configuration and CLI options
190
+ * 3. Executes the locize command with proper credential handling
191
+ * 4. Provides interactive credential setup on authentication errors
192
+ * 5. Handles retries with new credentials
193
+ * 6. Reports success or failure with appropriate exit codes
194
+ *
195
+ * @param command - The locize command to execute ('sync', 'download', or 'migrate')
196
+ * @param config - The toolkit configuration with locize settings
197
+ * @param cliOptions - Additional options passed from CLI arguments
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // Sync local files to Locize
202
+ * await runLocizeCommand('sync', config, { updateValues: true })
203
+ *
204
+ * // Download translations from Locize
205
+ * await runLocizeCommand('download', config)
206
+ *
207
+ * // Migrate local files to a new Locize project
208
+ * await runLocizeCommand('migrate', config)
209
+ * ```
210
+ */
211
+ async function runLocizeCommand(command, config, cliOptions = {}) {
212
+ await checkLocizeCliExists();
213
+ const spinner = ora(`Running 'locize ${command}'...\n`).start();
214
+ let effectiveConfig = config;
215
+ try {
216
+ // 1. First attempt
217
+ const initialArgs = buildArgs(command, effectiveConfig, cliOptions);
218
+ console.log(chalk.cyan(`\nRunning 'locize ${initialArgs.join(' ')}'...`));
219
+ const result = await execa('locize', initialArgs, { stdio: 'pipe' });
220
+ spinner.succeed(chalk.green(`'locize ${command}' completed successfully.`));
221
+ if (result?.stdout)
222
+ console.log(result.stdout); // Print captured output on success
223
+ }
224
+ catch (error) {
225
+ const stderr = error.stderr || '';
226
+ if (stderr.includes('missing required argument')) {
227
+ // 2. Auth failure, trigger interactive setup
228
+ const newCredentials = await interactiveCredentialSetup();
229
+ if (newCredentials) {
230
+ effectiveConfig = { ...effectiveConfig, locize: newCredentials };
231
+ spinner.start('Retrying with new credentials...');
232
+ try {
233
+ // 3. Retry attempt, rebuilding args with the NOW-UPDATED currentConfig object
234
+ const retryArgs = buildArgs(command, effectiveConfig, cliOptions);
235
+ console.log(chalk.cyan(`\nRunning 'locize ${retryArgs.join(' ')}'...`));
236
+ const result = await execa('locize', retryArgs, { stdio: 'pipe' });
237
+ spinner.succeed(chalk.green('Retry successful!'));
238
+ if (result?.stdout)
239
+ console.log(result.stdout);
240
+ }
241
+ catch (retryError) {
242
+ spinner.fail(chalk.red('Error during retry.'));
243
+ console.error(retryError.stderr || retryError.message);
244
+ process.exit(1);
245
+ }
246
+ }
247
+ else {
248
+ spinner.fail('Operation cancelled.');
249
+ process.exit(1); // User aborted the prompt
250
+ }
251
+ }
252
+ else {
253
+ // Handle other errors
254
+ spinner.fail(chalk.red(`Error executing 'locize ${command}'.`));
255
+ console.error(stderr || error.message);
256
+ process.exit(1);
257
+ }
258
+ }
259
+ console.log(chalk.green(`\n✅ 'locize ${command}' completed successfully.`));
260
+ }
261
+ const runLocizeSync = (config, cliOptions) => runLocizeCommand('sync', config, cliOptions);
262
+ const runLocizeDownload = (config, cliOptions) => runLocizeCommand('download', config, cliOptions);
263
+ const runLocizeMigrate = (config, cliOptions) => runLocizeCommand('migrate', config, cliOptions);
264
+
265
+ export { runLocizeDownload, runLocizeMigrate, runLocizeSync };
@@ -1 +1,194 @@
1
- import{resolve as e}from"node:path";import{access as t,writeFile as o}from"node:fs/promises";import{pathToFileURL as n}from"node:url";import{createJiti as i}from"jiti";import{getTsConfigAliases as r}from"./config.js";const s=e(process.cwd(),"i18next.config.ts"),a=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"],c=[".js",".mjs",".cjs",".ts"];async function l(e){if(c.some(t=>e.endsWith(t)))try{return await t(e),e}catch{return null}for(const o of c){const n=`${e}${o}`;try{return await t(n),n}catch{}}return null}async function f(c){let f;if(c){if(f=await l(e(process.cwd(),c)),!f)return console.log(`No legacy config file found at or near: ${c}`),void console.log("Tried extensions: .js, .mjs, .cjs, .ts")}else if(f=await l(e(process.cwd(),"i18next-parser.config")),!f)return console.log("No i18next-parser.config.* found. Nothing to migrate."),void console.log("Tried: i18next-parser.config.js, .mjs, .cjs, .ts");console.log(`Attempting to migrate legacy config from: ${f}...`);for(const o of a)try{const n=e(process.cwd(),o);return await t(n),void console.warn(`Warning: A new configuration file already exists at "${o}". Migration skipped to avoid overwriting.`)}catch(e){}const p=await async function(e){try{let t;if(e.endsWith(".ts")){const o=await r(),n=i(process.cwd(),{alias:o,interopDefault:!1});t=await n.import(e,{default:!0})}else{const o=n(e).href;t=(await import(`${o}?t=${Date.now()}`)).default}return t}catch(t){return console.error(`Error loading legacy config from ${e}:`,t),null}}(f);if(!p)return void console.error("Could not read the legacy config file.");const u={locales:p.locales||["en"],extract:{input:p.input||"src/**/*.{js,jsx,ts,tsx}",output:(p.output||"locales/$LOCALE/$NAMESPACE.json").replace("$LOCALE","{{language}}").replace("$NAMESPACE","{{namespace}}"),defaultNS:p.defaultNamespace||"translation",keySeparator:p.keySeparator,nsSeparator:p.namespaceSeparator,contextSeparator:p.contextSeparator,functions:p.lexers?.js?.functions||["t","*.t"],transComponents:p.lexers?.js?.components||["Trans"]},types:{input:["locales/{{language}}/{{namespace}}.json"],output:"src/types/i18next.d.ts"}};u.extract.functions.includes("t")&&!u.extract.functions.includes("*.t")&&u.extract.functions.push("*.t");const d=`\nimport { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${JSON.stringify(u,null,2)});\n`;await o(s,d.trim()),console.log("✅ Success! Migration complete."),console.log(`New configuration file created at: ${s}`),console.warn('\nPlease review the generated file and adjust paths for "types.input" if necessary.'),p.keepRemoved&&console.warn('Warning: The "keepRemoved" option is deprecated. Consider using the "preservePatterns" feature for dynamic keys.'),"v3"===p.i18nextOptions?.compatibilityJSON&&console.warn('Warning: compatibilityJSON "v3" is not supported in i18next-cli. Only i18next v4 format is supported.')}export{f as runMigrator};
1
+ import { resolve } from 'node:path';
2
+ import { access, writeFile } from 'node:fs/promises';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { createJiti } from 'jiti';
5
+ import { getTsConfigAliases } from './config.js';
6
+
7
+ /**
8
+ * Path where the new configuration file will be created
9
+ */
10
+ const newConfigPath = resolve(process.cwd(), 'i18next.config.ts');
11
+ /**
12
+ * List of possible new configuration file names that would prevent migration
13
+ */
14
+ const POSSIBLE_NEW_CONFIGS = [
15
+ 'i18next.config.ts',
16
+ 'i18next.config.js',
17
+ 'i18next.config.mjs',
18
+ 'i18next.config.cjs',
19
+ ];
20
+ /**
21
+ * List of supported legacy configuration file extensions
22
+ */
23
+ const LEGACY_CONFIG_EXTENSIONS = ['.js', '.mjs', '.cjs', '.ts'];
24
+ /**
25
+ * Helper function to find a legacy config file with various extensions
26
+ */
27
+ async function findLegacyConfigFile(basePath) {
28
+ // If the provided path already has an extension, use it directly
29
+ if (LEGACY_CONFIG_EXTENSIONS.some(ext => basePath.endsWith(ext))) {
30
+ try {
31
+ await access(basePath);
32
+ return basePath;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ // Try different extensions
39
+ for (const ext of LEGACY_CONFIG_EXTENSIONS) {
40
+ const fullPath = `${basePath}${ext}`;
41
+ try {
42
+ await access(fullPath);
43
+ return fullPath;
44
+ }
45
+ catch {
46
+ // Continue to next extension
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * Loads a legacy config file using the appropriate loader (jiti for TS, dynamic import for JS/MJS/CJS)
53
+ */
54
+ async function loadLegacyConfig(configPath) {
55
+ try {
56
+ let config;
57
+ // Use jiti for TypeScript files, native import for JavaScript
58
+ if (configPath.endsWith('.ts')) {
59
+ const aliases = await getTsConfigAliases();
60
+ const jiti = createJiti(process.cwd(), {
61
+ alias: aliases,
62
+ interopDefault: false,
63
+ });
64
+ const configModule = await jiti.import(configPath, { default: true });
65
+ config = configModule;
66
+ }
67
+ else {
68
+ const configUrl = pathToFileURL(configPath).href;
69
+ const configModule = await import(`${configUrl}?t=${Date.now()}`);
70
+ config = configModule.default;
71
+ }
72
+ return config;
73
+ }
74
+ catch (error) {
75
+ console.error(`Error loading legacy config from ${configPath}:`, error);
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Migrates a legacy i18next-parser configuration file to the new
81
+ * i18next-cli configuration format.
82
+ *
83
+ * This function:
84
+ * 1. Checks if a legacy config file exists (supports .js, .mjs, .cjs, .ts)
85
+ * 2. Prevents migration if any new config file already exists
86
+ * 3. Dynamically imports the old configuration using appropriate loader
87
+ * 4. Maps old configuration properties to new format:
88
+ * - `$LOCALE` → `{{language}}`
89
+ * - `$NAMESPACE` → `{{namespace}}`
90
+ * - Maps lexer functions and components
91
+ * - Creates sensible defaults for new features
92
+ * 5. Generates a new TypeScript configuration file
93
+ * 6. Provides warnings for deprecated features
94
+ *
95
+ * @param customConfigPath - Optional custom path to the legacy config file
96
+ *
97
+ * @example
98
+ * ```bash
99
+ * # Migrate default config
100
+ * npx i18next-cli migrate-config
101
+ *
102
+ * # Migrate custom config with extension
103
+ * npx i18next-cli migrate-config i18next-parser.config.mjs
104
+ *
105
+ * # Migrate custom config without extension (will try .js, .mjs, .cjs, .ts)
106
+ * npx i18next-cli migrate-config my-custom-config
107
+ * ```
108
+ */
109
+ async function runMigrator(customConfigPath) {
110
+ let oldConfigPath;
111
+ if (customConfigPath) {
112
+ oldConfigPath = await findLegacyConfigFile(resolve(process.cwd(), customConfigPath));
113
+ if (!oldConfigPath) {
114
+ console.log(`No legacy config file found at or near: ${customConfigPath}`);
115
+ console.log('Tried extensions: .js, .mjs, .cjs, .ts');
116
+ return;
117
+ }
118
+ }
119
+ else {
120
+ // Default behavior: look for i18next-parser.config.* files
121
+ oldConfigPath = await findLegacyConfigFile(resolve(process.cwd(), 'i18next-parser.config'));
122
+ if (!oldConfigPath) {
123
+ console.log('No i18next-parser.config.* found. Nothing to migrate.');
124
+ console.log('Tried: i18next-parser.config.js, .mjs, .cjs, .ts');
125
+ return;
126
+ }
127
+ }
128
+ console.log(`Attempting to migrate legacy config from: ${oldConfigPath}...`);
129
+ // Check if ANY new config file already exists
130
+ for (const configFile of POSSIBLE_NEW_CONFIGS) {
131
+ try {
132
+ const fullPath = resolve(process.cwd(), configFile);
133
+ await access(fullPath);
134
+ console.warn(`Warning: A new configuration file already exists at "${configFile}". Migration skipped to avoid overwriting.`);
135
+ return;
136
+ }
137
+ catch (e) {
138
+ // File doesn't exist, which is good
139
+ }
140
+ }
141
+ // Load the legacy config using the appropriate loader
142
+ const oldConfig = await loadLegacyConfig(oldConfigPath);
143
+ if (!oldConfig) {
144
+ console.error('Could not read the legacy config file.');
145
+ return;
146
+ }
147
+ // --- Start Migration Logic ---
148
+ const newConfig = {
149
+ locales: oldConfig.locales || ['en'],
150
+ extract: {
151
+ input: oldConfig.input || 'src/**/*.{js,jsx,ts,tsx}',
152
+ output: (oldConfig.output || 'locales/$LOCALE/$NAMESPACE.json')
153
+ .replace('$LOCALE', '{{language}}')
154
+ .replace('$NAMESPACE', '{{namespace}}'),
155
+ defaultNS: oldConfig.defaultNamespace || 'translation',
156
+ keySeparator: oldConfig.keySeparator,
157
+ nsSeparator: oldConfig.namespaceSeparator,
158
+ contextSeparator: oldConfig.contextSeparator,
159
+ // A simple mapping for functions
160
+ functions: oldConfig.lexers?.js?.functions || ['t', '*.t'],
161
+ transComponents: oldConfig.lexers?.js?.components || ['Trans'],
162
+ },
163
+ types: {
164
+ input: ['locales/{{language}}/{{namespace}}.json'], // Sensible default
165
+ output: 'src/types/i18next.d.ts', // Sensible default
166
+ },
167
+ };
168
+ // Make the migration smarter: if 't' is a function, also add the '*.t' wildcard
169
+ // to provide better out-of-the-box support for common patterns like `i18n.t`.
170
+ if (newConfig.extract.functions.includes('t') && !newConfig.extract.functions.includes('*.t')) {
171
+ newConfig.extract.functions.push('*.t');
172
+ }
173
+ // --- End Migration Logic ---
174
+ // Generate the new file content as a string
175
+ const newConfigFileContent = `
176
+ import { defineConfig } from 'i18next-cli';
177
+
178
+ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
179
+ `;
180
+ await writeFile(newConfigPath, newConfigFileContent.trim());
181
+ console.log('✅ Success! Migration complete.');
182
+ console.log(`New configuration file created at: ${newConfigPath}`);
183
+ console.warn('\nPlease review the generated file and adjust paths for "types.input" if necessary.');
184
+ // Warning for deprecated features
185
+ if (oldConfig.keepRemoved) {
186
+ console.warn('Warning: The "keepRemoved" option is deprecated. Consider using the "preservePatterns" feature for dynamic keys.');
187
+ }
188
+ // Warning for compatibilityJSON v3
189
+ if (oldConfig.i18nextOptions?.compatibilityJSON === 'v3') {
190
+ console.warn('Warning: compatibilityJSON "v3" is not supported in i18next-cli. Only i18next v4 format is supported.');
191
+ }
192
+ }
193
+
194
+ export { runMigrator };