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