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