i18next-cli 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +489 -0
  4. package/dist/cjs/cli.js +2 -0
  5. package/dist/cjs/config.js +1 -0
  6. package/dist/cjs/extractor/core/extractor.js +1 -0
  7. package/dist/cjs/extractor/core/key-finder.js +1 -0
  8. package/dist/cjs/extractor/core/translation-manager.js +1 -0
  9. package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
  10. package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
  11. package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
  12. package/dist/cjs/extractor/plugin-manager.js +1 -0
  13. package/dist/cjs/heuristic-config.js +1 -0
  14. package/dist/cjs/index.js +1 -0
  15. package/dist/cjs/init.js +1 -0
  16. package/dist/cjs/linter.js +1 -0
  17. package/dist/cjs/locize.js +1 -0
  18. package/dist/cjs/migrator.js +1 -0
  19. package/dist/cjs/package.json +1 -0
  20. package/dist/cjs/status.js +1 -0
  21. package/dist/cjs/syncer.js +1 -0
  22. package/dist/cjs/types-generator.js +1 -0
  23. package/dist/cjs/utils/file-utils.js +1 -0
  24. package/dist/cjs/utils/logger.js +1 -0
  25. package/dist/cjs/utils/nested-object.js +1 -0
  26. package/dist/cjs/utils/validation.js +1 -0
  27. package/dist/esm/cli.js +2 -0
  28. package/dist/esm/config.js +1 -0
  29. package/dist/esm/extractor/core/extractor.js +1 -0
  30. package/dist/esm/extractor/core/key-finder.js +1 -0
  31. package/dist/esm/extractor/core/translation-manager.js +1 -0
  32. package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
  33. package/dist/esm/extractor/parsers/comment-parser.js +1 -0
  34. package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
  35. package/dist/esm/extractor/plugin-manager.js +1 -0
  36. package/dist/esm/heuristic-config.js +1 -0
  37. package/dist/esm/index.js +1 -0
  38. package/dist/esm/init.js +1 -0
  39. package/dist/esm/linter.js +1 -0
  40. package/dist/esm/locize.js +1 -0
  41. package/dist/esm/migrator.js +1 -0
  42. package/dist/esm/status.js +1 -0
  43. package/dist/esm/syncer.js +1 -0
  44. package/dist/esm/types-generator.js +1 -0
  45. package/dist/esm/utils/file-utils.js +1 -0
  46. package/dist/esm/utils/logger.js +1 -0
  47. package/dist/esm/utils/nested-object.js +1 -0
  48. package/dist/esm/utils/validation.js +1 -0
  49. package/package.json +81 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +158 -0
  52. package/src/extractor/core/extractor.ts +195 -0
  53. package/src/extractor/core/key-finder.ts +70 -0
  54. package/src/extractor/core/translation-manager.ts +115 -0
  55. package/src/extractor/index.ts +7 -0
  56. package/src/extractor/parsers/ast-visitors.ts +637 -0
  57. package/src/extractor/parsers/comment-parser.ts +125 -0
  58. package/src/extractor/parsers/jsx-parser.ts +166 -0
  59. package/src/extractor/plugin-manager.ts +54 -0
  60. package/src/extractor.ts +15 -0
  61. package/src/heuristic-config.ts +64 -0
  62. package/src/index.ts +12 -0
  63. package/src/init.ts +156 -0
  64. package/src/linter.ts +191 -0
  65. package/src/locize.ts +251 -0
  66. package/src/migrator.ts +139 -0
  67. package/src/status.ts +192 -0
  68. package/src/syncer.ts +114 -0
  69. package/src/types-generator.ts +116 -0
  70. package/src/types.ts +312 -0
  71. package/src/utils/file-utils.ts +81 -0
  72. package/src/utils/logger.ts +36 -0
  73. package/src/utils/nested-object.ts +113 -0
  74. package/src/utils/validation.ts +69 -0
  75. package/tryme.js +8 -0
  76. package/tsconfig.json +71 -0
  77. package/types/cli.d.ts +3 -0
  78. package/types/cli.d.ts.map +1 -0
  79. package/types/config.d.ts +50 -0
  80. package/types/config.d.ts.map +1 -0
  81. package/types/extractor/core/extractor.d.ts +66 -0
  82. package/types/extractor/core/extractor.d.ts.map +1 -0
  83. package/types/extractor/core/key-finder.d.ts +31 -0
  84. package/types/extractor/core/key-finder.d.ts.map +1 -0
  85. package/types/extractor/core/translation-manager.d.ts +31 -0
  86. package/types/extractor/core/translation-manager.d.ts.map +1 -0
  87. package/types/extractor/index.d.ts +8 -0
  88. package/types/extractor/index.d.ts.map +1 -0
  89. package/types/extractor/parsers/ast-visitors.d.ts +235 -0
  90. package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
  91. package/types/extractor/parsers/comment-parser.d.ts +24 -0
  92. package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
  93. package/types/extractor/parsers/jsx-parser.d.ts +35 -0
  94. package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
  95. package/types/extractor/plugin-manager.d.ts +37 -0
  96. package/types/extractor/plugin-manager.d.ts.map +1 -0
  97. package/types/extractor.d.ts +7 -0
  98. package/types/extractor.d.ts.map +1 -0
  99. package/types/heuristic-config.d.ts +10 -0
  100. package/types/heuristic-config.d.ts.map +1 -0
  101. package/types/index.d.ts +4 -0
  102. package/types/index.d.ts.map +1 -0
  103. package/types/init.d.ts +29 -0
  104. package/types/init.d.ts.map +1 -0
  105. package/types/linter.d.ts +33 -0
  106. package/types/linter.d.ts.map +1 -0
  107. package/types/locize.d.ts +5 -0
  108. package/types/locize.d.ts.map +1 -0
  109. package/types/migrator.d.ts +37 -0
  110. package/types/migrator.d.ts.map +1 -0
  111. package/types/status.d.ts +20 -0
  112. package/types/status.d.ts.map +1 -0
  113. package/types/syncer.d.ts +33 -0
  114. package/types/syncer.d.ts.map +1 -0
  115. package/types/types-generator.d.ts +29 -0
  116. package/types/types-generator.d.ts.map +1 -0
  117. package/types/types.d.ts +268 -0
  118. package/types/types.d.ts.map +1 -0
  119. package/types/utils/file-utils.d.ts +61 -0
  120. package/types/utils/file-utils.d.ts.map +1 -0
  121. package/types/utils/logger.d.ts +34 -0
  122. package/types/utils/logger.d.ts.map +1 -0
  123. package/types/utils/nested-object.d.ts +71 -0
  124. package/types/utils/nested-object.d.ts.map +1 -0
  125. package/types/utils/validation.d.ts +47 -0
  126. package/types/utils/validation.d.ts.map +1 -0
  127. package/vitest.config.ts +13 -0
package/src/linter.ts ADDED
@@ -0,0 +1,191 @@
1
+ import { glob } from 'glob'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { parse } from '@swc/core'
4
+ import { ancestor } from 'swc-walk'
5
+ import chalk from 'chalk'
6
+ import ora from 'ora'
7
+ import type { I18nextToolkitConfig } from './types'
8
+
9
+ /**
10
+ * Runs the i18next linter to detect hardcoded strings and other potential issues.
11
+ *
12
+ * This function performs static analysis on source files to identify:
13
+ * - Hardcoded text strings in JSX elements
14
+ * - Hardcoded strings in JSX attributes (like alt text, titles, etc.)
15
+ * - Text that should be extracted for translation
16
+ *
17
+ * The linter respects configuration settings:
18
+ * - Uses the same input patterns as the extractor
19
+ * - Ignores content inside configured Trans components
20
+ * - Skips technical content like script/style tags
21
+ * - Identifies numeric values and interpolation syntax to avoid false positives
22
+ *
23
+ * @param config - The toolkit configuration with input patterns and component names
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const config = {
28
+ * extract: {
29
+ * input: ['src/**\/*.{ts,tsx}'],
30
+ * transComponents: ['Trans', 'Translation']
31
+ * }
32
+ * }
33
+ *
34
+ * await runLinter(config)
35
+ * // Outputs issues found or success message
36
+ * // Exits with code 1 if issues found, 0 if clean
37
+ * ```
38
+ */
39
+ export async function runLinter (config: I18nextToolkitConfig) {
40
+ const spinner = ora('Analyzing source files...\n').start()
41
+
42
+ try {
43
+ const sourceFiles = await glob(config.extract.input)
44
+ let totalIssues = 0
45
+ const issuesByFile = new Map<string, HardcodedString[]>()
46
+
47
+ for (const file of sourceFiles) {
48
+ const code = await readFile(file, 'utf-8')
49
+ const ast = await parse(code, { syntax: 'typescript', tsx: true })
50
+ const hardcodedStrings = findHardcodedStrings(ast, code, config)
51
+
52
+ if (hardcodedStrings.length > 0) {
53
+ totalIssues += hardcodedStrings.length
54
+ issuesByFile.set(file, hardcodedStrings)
55
+ }
56
+ }
57
+
58
+ if (totalIssues > 0) {
59
+ spinner.fail(chalk.red.bold(`Linter found ${totalIssues} potential issues.`))
60
+
61
+ // Print detailed report after spinner fails
62
+ for (const [file, issues] of issuesByFile.entries()) {
63
+ console.log(chalk.yellow(`\n${file}`))
64
+ issues.forEach(({ text, line }) => {
65
+ console.log(` ${chalk.gray(`${line}:`)} ${chalk.red('Error:')} Found hardcoded string: "${text}"`)
66
+ })
67
+ }
68
+ process.exit(1)
69
+ } else {
70
+ spinner.succeed(chalk.green.bold('No issues found.'))
71
+ }
72
+ } catch (error) {
73
+ spinner.fail(chalk.red('Linter failed to run.'))
74
+ console.error(error)
75
+ process.exit(1)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Represents a found hardcoded string with its location information.
81
+ */
82
+ interface HardcodedString {
83
+ /** The hardcoded text content */
84
+ text: string;
85
+ /** Line number where the string was found */
86
+ line: number;
87
+ }
88
+
89
+ /**
90
+ * Analyzes an AST to find potentially hardcoded strings that should be translated.
91
+ *
92
+ * This function traverses the syntax tree looking for:
93
+ * 1. JSX text nodes with translatable content
94
+ * 2. String literals in JSX attributes that might need translation
95
+ *
96
+ * It applies several filters to reduce false positives:
97
+ * - Ignores content inside Trans components (already handled)
98
+ * - Skips script and style tag content (technical, not user-facing)
99
+ * - Filters out numeric values (usually not translatable)
100
+ * - Ignores interpolation syntax starting with `{{`
101
+ * - Only reports non-empty, trimmed strings
102
+ *
103
+ * @param ast - The parsed AST to analyze
104
+ * @param code - Original source code for line number calculation
105
+ * @param config - Configuration containing Trans component names
106
+ * @returns Array of found hardcoded strings with location info
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const issues = findHardcodedStrings(ast, sourceCode, config)
111
+ * issues.forEach(issue => {
112
+ * console.log(`Line ${issue.line}: "${issue.text}"`)
113
+ * })
114
+ * ```
115
+ */
116
+ function findHardcodedStrings (ast: any, code: string, config: I18nextToolkitConfig): HardcodedString[] {
117
+ const issues: HardcodedString[] = []
118
+ const lineStarts: number[] = [0]
119
+ for (let i = 0; i < code.length; i++) {
120
+ if (code[i] === '\n') lineStarts.push(i + 1)
121
+ }
122
+
123
+ /**
124
+ * Converts a character position to a line number.
125
+ *
126
+ * @param pos - Character position in the source code
127
+ * @returns Line number (1-based)
128
+ */
129
+ const getLineNumber = (pos: number): number => {
130
+ let line = 1
131
+ for (let i = 0; i < lineStarts.length; i++) {
132
+ if (lineStarts[i] > pos) break
133
+ line = i + 1
134
+ }
135
+ return line
136
+ }
137
+
138
+ const transComponents = config.extract.transComponents || ['Trans']
139
+ const defaultIgnoredAttributes = ['className', 'key', 'id', 'style', 'href', 'i18nKey', 'defaults', 'type']
140
+ const customIgnoredAttributes = config.extract.ignoredAttributes || []
141
+ const ignoredAttributes = new Set([...defaultIgnoredAttributes, ...customIgnoredAttributes])
142
+
143
+ ancestor(ast, {
144
+ /**
145
+ * Processes JSX text nodes to identify hardcoded content.
146
+ *
147
+ * @param node - JSX text node
148
+ * @param ancestors - Array of ancestor nodes for context
149
+ */
150
+ JSXText (node: any, ancestors: any[]) {
151
+ const parent = ancestors[ancestors.length - 2]
152
+ const parentName = parent?.opening?.name?.value
153
+
154
+ if (parentName && (transComponents.includes(parentName) || parentName === 'script' || parentName === 'style')) {
155
+ return
156
+ }
157
+
158
+ const isIgnored = ancestors.some(ancestorNode => {
159
+ if (ancestorNode.type !== 'JSXElement') return false
160
+ const elementName = ancestorNode.opening?.name?.value
161
+ return transComponents.includes(elementName) || ['script', 'style', 'code'].includes(elementName)
162
+ })
163
+
164
+ if (isIgnored) return
165
+
166
+ const text = node.value.trim()
167
+ if (text && isNaN(Number(text)) && !text.startsWith('{{')) {
168
+ issues.push({ text, line: getLineNumber(node.span.start) })
169
+ }
170
+ },
171
+
172
+ /**
173
+ * Processes string literals in JSX attributes.
174
+ *
175
+ * @param node - String literal node
176
+ * @param ancestors - Array of ancestor nodes for context
177
+ */
178
+ StringLiteral (node: any, ancestors: any[]) {
179
+ const parent = ancestors[ancestors.length - 2]
180
+
181
+ // This check now uses the new combined Set
182
+ if (parent?.type === 'JSXAttribute' && !ignoredAttributes.has(parent.name.value)) {
183
+ const text = node.value.trim()
184
+ if (text && isNaN(Number(text))) {
185
+ issues.push({ text, line: getLineNumber(node.span.start) })
186
+ }
187
+ }
188
+ },
189
+ })
190
+ return issues
191
+ }
package/src/locize.ts ADDED
@@ -0,0 +1,251 @@
1
+ import { execa } from 'execa'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import inquirer from 'inquirer'
5
+ import { resolve } from 'node:path'
6
+ import type { I18nextToolkitConfig } from './types'
7
+
8
+ /**
9
+ * Verifies that the locize-cli tool is installed and accessible.
10
+ *
11
+ * @throws Exits the process with error code 1 if locize-cli is not found
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * await checkLocizeCliExists()
16
+ * // Continues execution if locize-cli is available
17
+ * // Otherwise exits with installation instructions
18
+ * ```
19
+ */
20
+ async function checkLocizeCliExists (): Promise<void> {
21
+ try {
22
+ await execa('locize', ['--version'])
23
+ } catch (error: any) {
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
+ /**
34
+ * Interactive setup wizard for configuring Locize credentials.
35
+ *
36
+ * This function guides users through setting up their Locize integration when
37
+ * configuration is missing or invalid. It:
38
+ * 1. Prompts for Project ID, API Key, and version
39
+ * 2. Validates required fields
40
+ * 3. Temporarily sets credentials for the current run
41
+ * 4. Provides security recommendations for storing credentials
42
+ * 5. Shows code examples for proper configuration
43
+ *
44
+ * @param config - Configuration object to update with new credentials
45
+ * @returns Promise resolving to the Locize configuration or undefined if setup was cancelled
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const locizeConfig = await interactiveCredentialSetup(config)
50
+ * if (locizeConfig) {
51
+ * // Proceed with sync using the new credentials
52
+ * }
53
+ * ```
54
+ */
55
+ async function interactiveCredentialSetup (config: I18nextToolkitConfig): Promise<{ projectId?: string, apiKey?: string, version?: string } | undefined> {
56
+ console.log(chalk.yellow('\nLocize configuration is missing or invalid. Let\'s set it up!'))
57
+
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
+
79
+ if (!answers.projectId) {
80
+ console.error(chalk.red('Project ID is required to continue.'))
81
+ return undefined
82
+ }
83
+
84
+ // Use the entered credentials for the current run
85
+ config.locize = {
86
+ projectId: answers.projectId,
87
+ apiKey: answers.apiKey,
88
+ version: answers.version,
89
+ }
90
+
91
+ const { save } = await inquirer.prompt([{
92
+ type: 'confirm',
93
+ name: 'save',
94
+ message: 'Would you like to see how to save these credentials for future use?',
95
+ default: true,
96
+ }])
97
+
98
+ if (save) {
99
+ const envSnippet = `
100
+ # Add this to your .env file (and ensure .env is in your .gitignore!)
101
+ LOCIZE_API_KEY=${answers.apiKey}
102
+ `
103
+ const configSnippet = `
104
+ // Add this to your i18next.config.ts file
105
+ locize: {
106
+ projectId: '${answers.projectId}',
107
+ // For security, apiKey is best set via an environment variable
108
+ apiKey: process.env.LOCIZE_API_KEY,
109
+ version: '${answers.version}',
110
+ },`
111
+
112
+ console.log(chalk.cyan('\nGreat! For the best security, we recommend using environment variables for your API key.'))
113
+ console.log(chalk.bold('\nRecommended approach (.env file):'))
114
+ console.log(chalk.green(envSnippet))
115
+ console.log(chalk.bold('Then, in your i18next.config.ts:'))
116
+ console.log(chalk.green(configSnippet))
117
+ }
118
+
119
+ return config.locize
120
+ }
121
+
122
+ /**
123
+ * Converts CLI options and configuration into locize-cli command arguments.
124
+ *
125
+ * Maps toolkit configuration and CLI flags to the appropriate locize-cli arguments:
126
+ * - `updateValues` → `--update-values`
127
+ * - `sourceLanguageOnly` → `--reference-language-only`
128
+ * - `compareModificationTime` → `--compare-modification-time`
129
+ * - `dryRun` → `--dry`
130
+ *
131
+ * @param command - The locize command being executed
132
+ * @param cliOptions - CLI options passed to the command
133
+ * @param locizeConfig - Locize configuration from the config file
134
+ * @returns Array of command-line arguments
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const args = cliOptionsToArgs('sync', { updateValues: true }, { dryRun: false })
139
+ * // Returns: ['--update-values', 'true']
140
+ * ```
141
+ */
142
+ function cliOptionsToArgs (command: 'sync' | 'download' | 'migrate', cliOptions: any = {}, locizeConfig: any = {}) {
143
+ const commandArgs: string[] = []
144
+
145
+ // Pass-through options
146
+ if (command === 'sync') {
147
+ const updateValues = cliOptions.updateValues ?? locizeConfig.updateValues
148
+ if (updateValues) commandArgs.push('--update-values', 'true')
149
+ const srcLngOnly = cliOptions.srcLngOnly ?? locizeConfig.sourceLanguageOnly
150
+ if (srcLngOnly) commandArgs.push('--reference-language-only', 'true')
151
+ const compareMtime = cliOptions.compareMtime ?? locizeConfig.compareModificationTime
152
+ if (compareMtime) commandArgs.push('--compare-modification-time', 'true')
153
+ const dryRun = cliOptions.dryRun ?? locizeConfig.dryRun
154
+ if (dryRun) commandArgs.push('--dry', 'true')
155
+ }
156
+
157
+ return commandArgs
158
+ }
159
+
160
+ /**
161
+ * Executes a locize-cli command with proper error handling and credential management.
162
+ *
163
+ * This is the core function that:
164
+ * 1. Validates that locize-cli is installed
165
+ * 2. Builds command arguments from configuration and CLI options
166
+ * 3. Executes the locize command with proper credential handling
167
+ * 4. Provides interactive credential setup on authentication errors
168
+ * 5. Handles retries with new credentials
169
+ * 6. Reports success or failure with appropriate exit codes
170
+ *
171
+ * @param command - The locize command to execute ('sync', 'download', or 'migrate')
172
+ * @param config - The toolkit configuration with locize settings
173
+ * @param cliOptions - Additional options passed from CLI arguments
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * // Sync local files to Locize
178
+ * await runLocizeCommand('sync', config, { updateValues: true })
179
+ *
180
+ * // Download translations from Locize
181
+ * await runLocizeCommand('download', config)
182
+ *
183
+ * // Migrate local files to a new Locize project
184
+ * await runLocizeCommand('migrate', config)
185
+ * ```
186
+ */
187
+ async function runLocizeCommand (command: 'sync' | 'download' | 'migrate', config: I18nextToolkitConfig, cliOptions: any = {}) {
188
+ await checkLocizeCliExists()
189
+
190
+ const spinner = ora(`Running 'locize ${command}'...\n`).start()
191
+
192
+ const locizeConfig = config.locize || {}
193
+ const { projectId, apiKey, version } = locizeConfig
194
+ let commandArgs: string[] = [command]
195
+
196
+ if (projectId) commandArgs.push('--project-id', projectId)
197
+ if (apiKey) commandArgs.push('--api-key', apiKey)
198
+ if (version) commandArgs.push('--ver', version)
199
+ // TODO: there might be more configurable locize-cli options in future
200
+
201
+ commandArgs.push(...cliOptionsToArgs(command, cliOptions, locizeConfig))
202
+
203
+ const basePath = resolve(process.cwd(), config.extract.output.split('/{{language}}/')[0])
204
+ commandArgs.push('--path', basePath)
205
+
206
+ try {
207
+ console.log(chalk.cyan(`\nRunning 'locize ${commandArgs.join(' ')}'...`))
208
+ const result = await execa('locize', commandArgs, { stdio: 'pipe' })
209
+ spinner.succeed(chalk.green(`'locize ${command}' completed successfully.`))
210
+ if (result?.stdout) console.log(result.stdout) // Print captured output on success
211
+ } catch (error: any) {
212
+ const stderr = error.stderr || ''
213
+ if (stderr.includes('missing required argument')) {
214
+ // Fallback to interactive setup
215
+ const newCredentials = await interactiveCredentialSetup(config)
216
+ if (newCredentials) {
217
+ // Retry the command with the new credentials
218
+ commandArgs = [command]
219
+ if (newCredentials.projectId) commandArgs.push('--project-id', newCredentials.projectId)
220
+ if (newCredentials.apiKey) commandArgs.push('--api-key', newCredentials.apiKey)
221
+ if (newCredentials.version) commandArgs.push('--ver', newCredentials.version)
222
+ // TODO: there might be more configurable locize-cli options in future
223
+ commandArgs.push(...cliOptionsToArgs(command, cliOptions, locizeConfig))
224
+ commandArgs.push('--path', basePath)
225
+ try {
226
+ spinner.start('Retrying with new credentials...') // Restart spinner
227
+ const result = await execa('locize', commandArgs, { stdio: 'pipe' })
228
+ spinner.succeed(chalk.green('Retry successful!'))
229
+ if (result?.stdout) console.log(result.stdout) // Print captured output on success
230
+ } catch (retryError: any) {
231
+ spinner.fail(chalk.red('Error during retry.'))
232
+ console.error(retryError.stderr || retryError.message)
233
+ process.exit(1)
234
+ }
235
+ } else {
236
+ spinner.fail('Operation cancelled.')
237
+ process.exit(1) // User aborted the prompt
238
+ }
239
+ } else {
240
+ // Handle other errors
241
+ spinner.fail(chalk.red(`Error executing 'locize ${command}'.`))
242
+ console.error(stderr || error.message)
243
+ process.exit(1)
244
+ }
245
+ }
246
+ console.log(chalk.green(`\n✅ 'locize ${command}' completed successfully.`))
247
+ }
248
+
249
+ export const runLocizeSync = (config: I18nextToolkitConfig, cliOptions?: any) => runLocizeCommand('sync', config, cliOptions)
250
+ export const runLocizeDownload = (config: I18nextToolkitConfig, cliOptions?: any) => runLocizeCommand('download', config, cliOptions)
251
+ export const runLocizeMigrate = (config: I18nextToolkitConfig, cliOptions?: any) => runLocizeCommand('migrate', config, cliOptions)
@@ -0,0 +1,139 @@
1
+ import { resolve } from 'node:path'
2
+ import { writeFile, access } from 'node:fs/promises'
3
+ import { pathToFileURL } from 'node:url'
4
+
5
+ /**
6
+ * Path to the legacy i18next-parser configuration file
7
+ */
8
+ const oldConfigPath = resolve(process.cwd(), 'i18next-parser.config.js')
9
+
10
+ /**
11
+ * Path where the new configuration file will be created
12
+ */
13
+ const newConfigPath = resolve(process.cwd(), 'i18next.config.ts')
14
+
15
+ /**
16
+ * List of possible new configuration file names that would prevent migration
17
+ */
18
+ const POSSIBLE_NEW_CONFIGS = [
19
+ 'i18next.config.ts',
20
+ 'i18next.config.js',
21
+ 'i18next.config.mjs',
22
+ 'i18next.config.cjs',
23
+ ]
24
+
25
+ /**
26
+ * Migrates a legacy i18next-parser.config.js configuration file to the new
27
+ * i18next-cli configuration format.
28
+ *
29
+ * This function:
30
+ * 1. Checks if a legacy config file exists
31
+ * 2. Prevents migration if any new config file already exists
32
+ * 3. Dynamically imports the old configuration
33
+ * 4. Maps old configuration properties to new format:
34
+ * - `$LOCALE` → `{{language}}`
35
+ * - `$NAMESPACE` → `{{namespace}}`
36
+ * - Maps lexer functions and components
37
+ * - Creates sensible defaults for new features
38
+ * 5. Generates a new TypeScript configuration file
39
+ * 6. Provides warnings for deprecated features
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // Legacy config (i18next-parser.config.js):
44
+ * module.exports = {
45
+ * locales: ['en', 'de'],
46
+ * output: 'locales/$LOCALE/$NAMESPACE.json',
47
+ * input: ['src/**\/*.js']
48
+ * }
49
+ *
50
+ * // After migration (i18next.config.ts):
51
+ * export default defineConfig({
52
+ * locales: ['en', 'de'],
53
+ * extract: {
54
+ * input: ['src/**\/*.js'],
55
+ * output: 'locales/{{language}}/{{namespace}}.json'
56
+ * }
57
+ * })
58
+ * ```
59
+ */
60
+ export async function runMigrator () {
61
+ console.log('Attempting to migrate legacy i18next-parser.config.js...')
62
+
63
+ try {
64
+ await access(oldConfigPath)
65
+ } catch (e) {
66
+ console.log('No i18next-parser.config.js found. Nothing to migrate.')
67
+ return
68
+ }
69
+
70
+ try {
71
+ await access(oldConfigPath)
72
+ } catch (e) {
73
+ console.log('No i18next-parser.config.js found. Nothing to migrate.')
74
+ return
75
+ }
76
+
77
+ // Check if ANY new config file already exists
78
+ for (const configFile of POSSIBLE_NEW_CONFIGS) {
79
+ try {
80
+ const fullPath = resolve(process.cwd(), configFile)
81
+ await access(fullPath)
82
+ console.warn(`Warning: A new configuration file already exists at "${configFile}". Migration skipped to avoid overwriting.`)
83
+ return
84
+ } catch (e) {
85
+ // File doesn't exist, which is good
86
+ }
87
+ }
88
+
89
+ // Dynamically import the CJS config file
90
+ const oldConfigUrl = pathToFileURL(oldConfigPath).href
91
+ const oldConfigModule = await import(oldConfigUrl)
92
+ const oldConfig = oldConfigModule.default
93
+
94
+ if (!oldConfig) {
95
+ console.error('Could not read the legacy config file.')
96
+ return
97
+ }
98
+
99
+ // --- Start Migration Logic ---
100
+ const newConfig = {
101
+ locales: oldConfig.locales || ['en'],
102
+ extract: {
103
+ input: oldConfig.input || 'src/**/*.{js,jsx,ts,tsx}',
104
+ output: (oldConfig.output || 'locales/$LOCALE/$NAMESPACE.json').replace('$LOCALE', '{{language}}').replace('$NAMESPACE', '{{namespace}}'),
105
+ defaultNS: oldConfig.defaultNamespace || 'translation',
106
+ keySeparator: oldConfig.keySeparator,
107
+ nsSeparator: oldConfig.namespaceSeparator,
108
+ contextSeparator: oldConfig.contextSeparator,
109
+ // A simple mapping for functions
110
+ functions: oldConfig.lexers?.js?.functions || ['t'],
111
+ transComponents: oldConfig.lexers?.js?.components || ['Trans'],
112
+ },
113
+ typesafe: {
114
+ input: 'locales/{{language}}/{{namespace}}.json', // Sensible default
115
+ output: 'src/types/i18next.d.ts', // Sensible default
116
+ },
117
+ sync: {
118
+ primaryLanguage: oldConfig.locales?.[0] || 'en',
119
+ secondaryLanguages: oldConfig.locales.filter((l: string) => l !== (oldConfig.locales?.[0] || 'en'))
120
+ },
121
+ }
122
+ // --- End Migration Logic ---
123
+
124
+ // Generate the new file content as a string
125
+ const newConfigFileContent = `
126
+ import { defineConfig } from 'i18next-cli';
127
+
128
+ export default defineConfig(${JSON.stringify(newConfig, null, 2)});
129
+ `
130
+
131
+ await writeFile(newConfigPath, newConfigFileContent.trim())
132
+
133
+ console.log('✅ Success! Migration complete.')
134
+ console.log(`New configuration file created at: ${newConfigPath}`)
135
+ console.warn('\nPlease review the generated file and adjust paths for "typesafe.input" if necessary.')
136
+ if (oldConfig.keepRemoved) {
137
+ console.warn('Warning: The "keepRemoved" option is deprecated. Consider using the "preservePatterns" feature for dynamic keys.')
138
+ }
139
+ }