i18next-cli 1.24.13 ā 1.24.14
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.
- package/dist/cjs/cli.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/package.json +6 -6
- package/types/cli.d.ts +3 -1
- package/types/cli.d.ts.map +1 -1
- package/CHANGELOG.md +0 -599
- package/src/cli.ts +0 -283
- package/src/config.ts +0 -215
- package/src/extractor/core/ast-visitors.ts +0 -259
- package/src/extractor/core/extractor.ts +0 -250
- package/src/extractor/core/key-finder.ts +0 -142
- package/src/extractor/core/translation-manager.ts +0 -750
- package/src/extractor/index.ts +0 -7
- package/src/extractor/parsers/ast-utils.ts +0 -87
- package/src/extractor/parsers/call-expression-handler.ts +0 -793
- package/src/extractor/parsers/comment-parser.ts +0 -424
- package/src/extractor/parsers/expression-resolver.ts +0 -391
- package/src/extractor/parsers/jsx-handler.ts +0 -488
- package/src/extractor/parsers/jsx-parser.ts +0 -1463
- package/src/extractor/parsers/scope-manager.ts +0 -445
- package/src/extractor/plugin-manager.ts +0 -116
- package/src/extractor.ts +0 -15
- package/src/heuristic-config.ts +0 -92
- package/src/index.ts +0 -22
- package/src/init.ts +0 -175
- package/src/linter.ts +0 -345
- package/src/locize.ts +0 -263
- package/src/migrator.ts +0 -208
- package/src/rename-key.ts +0 -398
- package/src/status.ts +0 -380
- package/src/syncer.ts +0 -133
- package/src/types-generator.ts +0 -139
- package/src/types.ts +0 -577
- package/src/utils/default-value.ts +0 -45
- package/src/utils/file-utils.ts +0 -167
- package/src/utils/funnel-msg-tracker.ts +0 -84
- package/src/utils/logger.ts +0 -36
- package/src/utils/nested-object.ts +0 -135
- package/src/utils/validation.ts +0 -72
package/src/rename-key.ts
DELETED
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
import { glob } from 'glob'
|
|
2
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
-
import type { I18nextToolkitConfig, Logger, RenameKeyResult } from './types'
|
|
4
|
-
import { ConsoleLogger } from './utils/logger'
|
|
5
|
-
import { loadTranslationFile, serializeTranslationFile, getOutputPath } from './utils/file-utils'
|
|
6
|
-
import { resolve } from 'node:path'
|
|
7
|
-
import { getNestedValue, setNestedValue } from './utils/nested-object'
|
|
8
|
-
import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker'
|
|
9
|
-
import chalk from 'chalk'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Renames a translation key across all source files and translation files.
|
|
13
|
-
*
|
|
14
|
-
* This function performs a comprehensive key rename operation:
|
|
15
|
-
* 1. Validates the old and new key names
|
|
16
|
-
* 2. Checks for conflicts in translation files
|
|
17
|
-
* 3. Updates all occurrences in source code (AST-based)
|
|
18
|
-
* 4. Updates all translation files for all locales
|
|
19
|
-
* 5. Preserves the original translation values
|
|
20
|
-
*
|
|
21
|
-
* @param config - The i18next toolkit configuration
|
|
22
|
-
* @param oldKey - The current key to rename (may include namespace prefix)
|
|
23
|
-
* @param newKey - The new key name (may include namespace prefix)
|
|
24
|
-
* @param options - Rename options (dry-run mode, etc.)
|
|
25
|
-
* @param logger - Logger instance for output
|
|
26
|
-
* @returns Result object with update status and file lists
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* // Basic rename
|
|
31
|
-
* const result = await runRenameKey(config, 'old.key', 'new.key')
|
|
32
|
-
*
|
|
33
|
-
* // With namespace
|
|
34
|
-
* const result = await runRenameKey(config, 'common:button.submit', 'common:button.save')
|
|
35
|
-
*
|
|
36
|
-
* // Dry run to preview changes
|
|
37
|
-
* const result = await runRenameKey(config, 'old.key', 'new.key', { dryRun: true })
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export async function runRenameKey (
|
|
41
|
-
config: I18nextToolkitConfig,
|
|
42
|
-
oldKey: string,
|
|
43
|
-
newKey: string,
|
|
44
|
-
options: {
|
|
45
|
-
dryRun?: boolean
|
|
46
|
-
} = {},
|
|
47
|
-
logger: Logger = new ConsoleLogger()
|
|
48
|
-
): Promise<RenameKeyResult> {
|
|
49
|
-
const { dryRun = false } = options
|
|
50
|
-
|
|
51
|
-
// Validate keys
|
|
52
|
-
const validation = validateKeys(oldKey, newKey, config)
|
|
53
|
-
if (!validation.valid) {
|
|
54
|
-
return {
|
|
55
|
-
success: false,
|
|
56
|
-
sourceFiles: [],
|
|
57
|
-
translationFiles: [],
|
|
58
|
-
error: validation.error
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Parse namespace from keys
|
|
63
|
-
const oldParts = parseKeyWithNamespace(oldKey, config)
|
|
64
|
-
const newParts = parseKeyWithNamespace(newKey, config)
|
|
65
|
-
|
|
66
|
-
// Check for conflicts in translation files
|
|
67
|
-
const conflicts = await checkConflicts(newParts, config)
|
|
68
|
-
if (conflicts.length > 0) {
|
|
69
|
-
return {
|
|
70
|
-
success: false,
|
|
71
|
-
sourceFiles: [],
|
|
72
|
-
translationFiles: [],
|
|
73
|
-
conflicts,
|
|
74
|
-
error: 'Target key already exists in translation files'
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
logger.info(`š Scanning for usages of "${oldKey}"...`)
|
|
79
|
-
|
|
80
|
-
// Find and update source files
|
|
81
|
-
const sourceResults = await updateSourceFiles(oldParts, newParts, config, dryRun, logger)
|
|
82
|
-
|
|
83
|
-
// Update translation files
|
|
84
|
-
const translationResults = await updateTranslationFiles(oldParts, newParts, config, dryRun, logger)
|
|
85
|
-
|
|
86
|
-
const totalChanges = sourceResults.reduce((sum, r) => sum + r.changes, 0)
|
|
87
|
-
|
|
88
|
-
if (!dryRun && totalChanges > 0) {
|
|
89
|
-
logger.info('\n⨠Successfully renamed key!')
|
|
90
|
-
logger.info(` Old: "${oldKey}"`)
|
|
91
|
-
logger.info(` New: "${newKey}"`)
|
|
92
|
-
|
|
93
|
-
// Show locize funnel after successful rename
|
|
94
|
-
await printLocizeFunnel()
|
|
95
|
-
} else if (totalChanges === 0) {
|
|
96
|
-
logger.info(`\nā ļø No usages found for "${oldKey}"`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
success: true,
|
|
101
|
-
sourceFiles: sourceResults,
|
|
102
|
-
translationFiles: translationResults
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Prints a promotional message for the locize rename/move workflow.
|
|
108
|
-
* This message is shown after a successful key rename operation.
|
|
109
|
-
*/
|
|
110
|
-
async function printLocizeFunnel () {
|
|
111
|
-
if (!(await shouldShowFunnel('rename-key'))) return
|
|
112
|
-
|
|
113
|
-
console.log(chalk.yellow.bold('\nš” Tip: Managing translations across multiple projects?'))
|
|
114
|
-
console.log(' With locize, you can rename, move, and copy translation keys directly')
|
|
115
|
-
console.log(' in the web interfaceāno CLI needed. Perfect for collaboration with')
|
|
116
|
-
console.log(' translators and managing complex refactoring across namespaces.')
|
|
117
|
-
console.log(` Learn more: ${chalk.cyan('https://www.locize.com/docs/how-can-a-segment-key-be-copied-moved-or-renamed')}`)
|
|
118
|
-
|
|
119
|
-
return recordFunnelShown('rename-key')
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface KeyParts {
|
|
123
|
-
namespace?: string
|
|
124
|
-
key: string
|
|
125
|
-
fullKey: string
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parseKeyWithNamespace (key: string, config: I18nextToolkitConfig): KeyParts {
|
|
129
|
-
const nsSeparator = config.extract.nsSeparator ?? ':'
|
|
130
|
-
|
|
131
|
-
if (nsSeparator && key.includes(nsSeparator)) {
|
|
132
|
-
const [ns, ...rest] = key.split(nsSeparator)
|
|
133
|
-
return {
|
|
134
|
-
namespace: ns,
|
|
135
|
-
key: rest.join(nsSeparator),
|
|
136
|
-
fullKey: key
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
namespace: config.extract.defaultNS || 'translation',
|
|
142
|
-
key,
|
|
143
|
-
fullKey: key
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function validateKeys (oldKey: string, newKey: string, config: I18nextToolkitConfig): { valid: boolean; error?: string } {
|
|
148
|
-
if (!oldKey || !oldKey.trim()) {
|
|
149
|
-
return { valid: false, error: 'Old key cannot be empty' }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!newKey || !newKey.trim()) {
|
|
153
|
-
return { valid: false, error: 'New key cannot be empty' }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (oldKey === newKey) {
|
|
157
|
-
return { valid: false, error: 'Old and new keys are identical' }
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return { valid: true }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function checkConflicts (newParts: KeyParts, config: I18nextToolkitConfig): Promise<string[]> {
|
|
164
|
-
const conflicts: string[] = []
|
|
165
|
-
|
|
166
|
-
for (const locale of config.locales) {
|
|
167
|
-
const outputPath = getOutputPath(config.extract.output, locale, newParts.namespace)
|
|
168
|
-
const fullPath = resolve(process.cwd(), outputPath)
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const existingTranslations = await loadTranslationFile(fullPath)
|
|
172
|
-
if (existingTranslations) {
|
|
173
|
-
const keySeparator = config.extract.keySeparator ?? '.'
|
|
174
|
-
const value = getNestedValue(existingTranslations, newParts.key, keySeparator)
|
|
175
|
-
if (value !== undefined) {
|
|
176
|
-
conflicts.push(`${locale}:${newParts.fullKey}`)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
// File doesn't exist, no conflict
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return conflicts
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function updateSourceFiles (
|
|
188
|
-
oldParts: KeyParts,
|
|
189
|
-
newParts: KeyParts,
|
|
190
|
-
config: I18nextToolkitConfig,
|
|
191
|
-
dryRun: boolean,
|
|
192
|
-
logger: Logger
|
|
193
|
-
): Promise<Array<{ path: string; changes: number }>> {
|
|
194
|
-
const defaultIgnore = ['node_modules/**']
|
|
195
|
-
const userIgnore = Array.isArray(config.extract.ignore)
|
|
196
|
-
? config.extract.ignore
|
|
197
|
-
: config.extract.ignore ? [config.extract.ignore] : []
|
|
198
|
-
|
|
199
|
-
// Normalize input patterns for cross-platform compatibility
|
|
200
|
-
const inputPatterns = Array.isArray(config.extract.input)
|
|
201
|
-
? config.extract.input
|
|
202
|
-
: [config.extract.input]
|
|
203
|
-
|
|
204
|
-
const normalizedPatterns = inputPatterns.map(pattern =>
|
|
205
|
-
pattern.replace(/\\/g, '/')
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
const sourceFiles = await glob(normalizedPatterns, {
|
|
209
|
-
ignore: [...defaultIgnore, ...userIgnore],
|
|
210
|
-
cwd: process.cwd()
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const results: Array<{ path: string; changes: number }> = []
|
|
214
|
-
|
|
215
|
-
for (const file of sourceFiles) {
|
|
216
|
-
const code = await readFile(file, 'utf-8')
|
|
217
|
-
const { newCode, changes } = await replaceKeyInSource(code, oldParts, newParts, config)
|
|
218
|
-
|
|
219
|
-
if (changes > 0) {
|
|
220
|
-
if (!dryRun) {
|
|
221
|
-
await writeFile(file, newCode, 'utf-8')
|
|
222
|
-
}
|
|
223
|
-
results.push({ path: file, changes })
|
|
224
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${file} (${changes} ${changes === 1 ? 'change' : 'changes'})`)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (results.length > 0) {
|
|
229
|
-
logger.info(`\nš Source file changes: ${results.length} file${results.length === 1 ? '' : 's'}`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return results
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function replaceKeyInSource (
|
|
236
|
-
code: string,
|
|
237
|
-
oldParts: KeyParts,
|
|
238
|
-
newParts: KeyParts,
|
|
239
|
-
config: I18nextToolkitConfig
|
|
240
|
-
): Promise<{ newCode: string; changes: number }> {
|
|
241
|
-
// Use regex-based replacement which is more reliable than AST manipulation
|
|
242
|
-
return replaceKeyWithRegex(code, oldParts, newParts, config)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function replaceKeyWithRegex (
|
|
246
|
-
code: string,
|
|
247
|
-
oldParts: KeyParts,
|
|
248
|
-
newParts: KeyParts,
|
|
249
|
-
config: I18nextToolkitConfig
|
|
250
|
-
): { newCode: string; changes: number } {
|
|
251
|
-
let changes = 0
|
|
252
|
-
let newCode = code
|
|
253
|
-
const nsSeparator = config.extract.nsSeparator ?? ':'
|
|
254
|
-
|
|
255
|
-
// Helper to determine which key form to use in replacement
|
|
256
|
-
const getReplacementKey = (originalKey: string): string => {
|
|
257
|
-
const hasNamespace = nsSeparator && originalKey.includes(String(nsSeparator))
|
|
258
|
-
return hasNamespace ? newParts.fullKey : newParts.key
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Pattern 1: Function calls - respect configured functions
|
|
262
|
-
const configuredFunctions = config.extract.functions || ['t', '*.t']
|
|
263
|
-
const functionPatterns: Array<{ pattern: RegExp; original: string }> = []
|
|
264
|
-
|
|
265
|
-
for (const fnPattern of configuredFunctions) {
|
|
266
|
-
if (fnPattern.startsWith('*.')) {
|
|
267
|
-
// Wildcard pattern like '*.t' - match any prefix
|
|
268
|
-
const suffix = fnPattern.substring(1) // '.t'
|
|
269
|
-
const escapedSuffix = escapeRegex(suffix)
|
|
270
|
-
|
|
271
|
-
// Match: anyIdentifier.t('key')
|
|
272
|
-
functionPatterns.push({
|
|
273
|
-
pattern: new RegExp(`\\w+${escapedSuffix}\\((['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'),
|
|
274
|
-
original: oldParts.fullKey
|
|
275
|
-
})
|
|
276
|
-
functionPatterns.push({
|
|
277
|
-
pattern: new RegExp(`\\w+${escapedSuffix}\\((['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'),
|
|
278
|
-
original: oldParts.key
|
|
279
|
-
})
|
|
280
|
-
} else {
|
|
281
|
-
// Exact function name
|
|
282
|
-
const escapedFn = escapeRegex(fnPattern)
|
|
283
|
-
functionPatterns.push({
|
|
284
|
-
pattern: new RegExp(`\\b${escapedFn}\\((['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'),
|
|
285
|
-
original: oldParts.fullKey
|
|
286
|
-
})
|
|
287
|
-
functionPatterns.push({
|
|
288
|
-
pattern: new RegExp(`\\b${escapedFn}\\((['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'),
|
|
289
|
-
original: oldParts.key
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const { pattern, original } of functionPatterns) {
|
|
295
|
-
if (pattern.test(newCode)) {
|
|
296
|
-
const replacement = getReplacementKey(original)
|
|
297
|
-
newCode = newCode.replace(pattern, (match, quote) => {
|
|
298
|
-
changes++
|
|
299
|
-
// Preserve the function name part, only replace the key
|
|
300
|
-
const functionNameMatch = match.match(/^(\w+(?:\.\w+)*)\(/)
|
|
301
|
-
if (functionNameMatch) {
|
|
302
|
-
return `${functionNameMatch[1]}(${quote}${replacement}${quote}`
|
|
303
|
-
}
|
|
304
|
-
return match
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Pattern 2: JSX i18nKey attribute - respect configured transComponents
|
|
310
|
-
// const transComponents = config.extract.transComponents || ['Trans']
|
|
311
|
-
|
|
312
|
-
// Create a pattern that matches i18nKey on any of the configured components
|
|
313
|
-
// This is a simplified approach - for more complex cases, consider AST-based replacement
|
|
314
|
-
const i18nKeyPatterns = [
|
|
315
|
-
{ pattern: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'), original: oldParts.fullKey },
|
|
316
|
-
{ pattern: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'), original: oldParts.key }
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
for (const { pattern, original } of i18nKeyPatterns) {
|
|
320
|
-
if (pattern.test(newCode)) {
|
|
321
|
-
const replacement = getReplacementKey(original)
|
|
322
|
-
newCode = newCode.replace(pattern, (match, quote) => {
|
|
323
|
-
changes++
|
|
324
|
-
return `i18nKey=${quote}${replacement}${quote}`
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return { newCode, changes }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function escapeRegex (str: string): string {
|
|
333
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function updateTranslationFiles (
|
|
337
|
-
oldParts: KeyParts,
|
|
338
|
-
newParts: KeyParts,
|
|
339
|
-
config: I18nextToolkitConfig,
|
|
340
|
-
dryRun: boolean,
|
|
341
|
-
logger: Logger
|
|
342
|
-
): Promise<Array<{ path: string; updated: boolean }>> {
|
|
343
|
-
const results: Array<{ path: string; updated: boolean }> = []
|
|
344
|
-
const keySeparator = config.extract.keySeparator ?? '.'
|
|
345
|
-
|
|
346
|
-
for (const locale of config.locales) {
|
|
347
|
-
const outputPath = getOutputPath(config.extract.output, locale, oldParts.namespace)
|
|
348
|
-
const fullPath = resolve(process.cwd(), outputPath)
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const translations = await loadTranslationFile(fullPath)
|
|
352
|
-
if (!translations) continue
|
|
353
|
-
|
|
354
|
-
const oldValue = getNestedValue(translations, oldParts.key, keySeparator)
|
|
355
|
-
if (oldValue === undefined) continue
|
|
356
|
-
|
|
357
|
-
// Remove old key
|
|
358
|
-
deleteNestedValue(translations, oldParts.key, keySeparator)
|
|
359
|
-
|
|
360
|
-
// Add new key with same value
|
|
361
|
-
setNestedValue(translations, newParts.key, oldValue, keySeparator)
|
|
362
|
-
|
|
363
|
-
if (!dryRun) {
|
|
364
|
-
const content = serializeTranslationFile(
|
|
365
|
-
translations,
|
|
366
|
-
config.extract.outputFormat,
|
|
367
|
-
config.extract.indentation
|
|
368
|
-
)
|
|
369
|
-
await writeFile(fullPath, content, 'utf-8')
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
results.push({ path: fullPath, updated: true })
|
|
373
|
-
logger.info(` ${dryRun ? '(dry-run) ' : ''}ā ${fullPath}`)
|
|
374
|
-
} catch (error) {
|
|
375
|
-
// File doesn't exist or couldn't be processed
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (results.length > 0) {
|
|
380
|
-
logger.info(`\nš¦ Translation file updates: ${results.length} file${results.length === 1 ? '' : 's'}`)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return results
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function deleteNestedValue (obj: any, path: string, separator: string | boolean): void {
|
|
387
|
-
if (separator === false) {
|
|
388
|
-
delete obj[path]
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
const keys = path.split(String(separator))
|
|
392
|
-
let current = obj
|
|
393
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
394
|
-
if (!current[keys[i]]) return
|
|
395
|
-
current = current[keys[i]]
|
|
396
|
-
}
|
|
397
|
-
delete current[keys[keys.length - 1]]
|
|
398
|
-
}
|