i18next-cli 1.39.0 → 1.39.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.
package/dist/cjs/cli.js CHANGED
@@ -28,7 +28,7 @@ const program = new commander.Command();
28
28
  program
29
29
  .name('i18next-cli')
30
30
  .description('A unified, high-performance i18next CLI.')
31
- .version('1.39.0'); // This string is replaced with the actual version at build time by rollup
31
+ .version('1.39.1'); // This string is replaced with the actual version at build time by rollup
32
32
  // new: global config override option
33
33
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
34
34
  program
@@ -159,9 +159,10 @@ async function updateSourceFiles(oldParts, newParts, config, dryRun, logger) {
159
159
  ? config.extract.input
160
160
  : [config.extract.input];
161
161
  const normalizedPatterns = inputPatterns.map(pattern => pattern.replace(/\\/g, '/'));
162
+ // glob accepts array of patterns; do not force cwd to avoid accidental path rewriting
162
163
  const sourceFiles = await glob.glob(normalizedPatterns, {
163
164
  ignore: [...defaultIgnore, ...userIgnore],
164
- cwd: process.cwd()
165
+ nodir: true
165
166
  });
166
167
  const results = [];
167
168
  for (const file of sourceFiles) {
@@ -181,122 +182,102 @@ async function updateSourceFiles(oldParts, newParts, config, dryRun, logger) {
181
182
  return results;
182
183
  }
183
184
  async function replaceKeyInSource(code, oldParts, newParts, config) {
184
- // Use regex-based replacement which is more reliable than AST manipulation
185
+ // Simpler and robust regex-based replacement that covers tests' patterns
185
186
  return replaceKeyWithRegex(code, oldParts, newParts, config);
186
187
  }
187
188
  function replaceKeyWithRegex(code, oldParts, newParts, config) {
188
189
  let changes = 0;
189
190
  let newCode = code;
190
191
  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
192
  const configuredFunctions = config.extract.functions || ['t', '*.t'];
198
- const functionPatterns = [];
199
- for (const fnPattern of configuredFunctions) {
193
+ // Helper to create function-prefix regex fragment
194
+ const fnPrefixToRegex = (fnPattern) => {
200
195
  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
- });
196
+ // '*.t' -> match anyIdentifier.t
197
+ const suffix = fnPattern.slice(2);
198
+ return `\\b[\\w$]+\\.${escapeRegex(suffix)}`; // e.g. \b[\w$]+\.t
213
199
  }
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
200
+ // exact function name (may include dot like 'i18n.t' or 'translate')
201
+ return `\\b${escapeRegex(fnPattern)}`;
202
+ };
203
+ // Replace exact string-key usages inside function calls: fn('key') or fn(`key`) or fn("key")
204
+ for (const fnPattern of configuredFunctions) {
205
+ const prefix = fnPrefixToRegex(fnPattern);
206
+ // Match fullKey first (namespace-prefixed in source)
207
+ if (oldParts.fullKey) {
208
+ const regexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g');
209
+ newCode = newCode.replace(regexFull, (match, q) => {
210
+ changes++;
211
+ const replacementKey = (oldParts.fullKey.includes(nsSeparator || ':') ? newParts.fullKey : newParts.key);
212
+ // preserve surrounding characters up to the opening quote
213
+ return match.replace(oldParts.fullKey, replacementKey);
224
214
  });
225
215
  }
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) => {
216
+ // Then match bare key (no namespace in source)
217
+ const regexKey = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g');
218
+ newCode = newCode.replace(regexKey, (match) => {
219
+ changes++;
220
+ const replacementKey = newParts.key;
221
+ return match.replace(new RegExp(escapeRegex(oldParts.key)), replacementKey);
222
+ });
223
+ // Handle ns option in options object: fn('key', { ns: 'oldNs', ... })
224
+ if (oldParts.namespace && newParts.namespace && oldParts.namespace !== newParts.namespace) {
225
+ // We want to change only when key matches and ns value equals oldParts.namespace
226
+ const nsRegexFullKey = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*,\\s*\\{([^}]*)\\bns\\s*:\\s*(['"\`])${escapeRegex(oldParts.namespace)}\\3([^}]*)\\}\\s*\\)`, 'g');
227
+ newCode = newCode.replace(nsRegexFullKey, (match, keyQ, beforeNs, nsQ, afterNs) => {
231
228
  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;
229
+ // replace ns value
230
+ return match.replace(new RegExp(`(\\bns\\s*:\\s*['"\`])${escapeRegex(oldParts.namespace ?? '')}(['"\`])`), `$1${newParts.namespace ?? ''}$2`);
238
231
  });
232
+ // same but if the call used fullKey (ns included inside key string), still update ns option if present
233
+ if (oldParts.fullKey) {
234
+ const nsRegexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1\\s*,\\s*\\{([^}]*)\\bns\\s*:\\s*(['"\`])${escapeRegex(oldParts.namespace)}\\3([^}]*)\\}\\s*\\)`, 'g');
235
+ newCode = newCode.replace(nsRegexFull, (match) => {
236
+ changes++;
237
+ return match.replace(new RegExp(`(\\bns\\s*:\\s*['"\`])${escapeRegex(oldParts.namespace ?? '')}(['"\`])`), `$1${newParts.namespace ?? ''}$2`);
238
+ });
239
+ }
239
240
  }
240
241
  }
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)
242
+ // Selector API: dot-notation: fn(($) => $.old.key)
243
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
- });
244
+ const prefix = fnPrefixToRegex(fnPattern);
245
+ // match forms like: prefix( $ => $.old.key )
246
+ // capture the arrow param name and the rest
247
+ // We'll attempt to match the param and the dotted property chain equal to oldParts.key (exact)
248
+ const dotRegex = new RegExp(`${prefix}\\s*\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*\\1\\.${escapeRegex(oldParts.key)}\\s*\\)`, 'g');
249
+ newCode = newCode.replace(dotRegex, (match, param) => {
250
+ changes++;
251
+ // Determine replacement (if source used namespace in key it would be fullKey, but in selector dot-notation it's always key form)
252
+ const replacementKey = newParts.key;
253
+ return match.replace(`.${oldParts.key}`, `.${replacementKey}`);
254
+ });
255
+ // Bracket notation: fn(($) => $["Old Key"]) etc.
256
+ const bracketRegex = new RegExp(`${prefix}\\s*\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*\\1\\s*\\[\\s*(['"\`])${escapeRegex(oldParts.key)}\\2\\s*\\]\\s*\\)`, 'g');
257
+ newCode = newCode.replace(bracketRegex, (match, param, quote) => {
258
+ changes++;
259
+ const replacementKey = newParts.key;
260
+ // If replacementKey is a valid identifier, convert to dot-notation, otherwise keep bracket form with preserved quote style
261
+ if (/^[A-Za-z_$][\w$]*$/.test(replacementKey)) {
262
+ return match.replace(new RegExp(`\\[\\s*['"\`]${escapeRegex(oldParts.key)}['"\`]\\s*\\]`), `.${replacementKey}`);
266
263
  }
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
- });
264
+ else {
265
+ return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `$1${replacementKey}$1`);
281
266
  }
282
- }
267
+ });
283
268
  }
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 }
269
+ // JSX i18nKey attribute (handles all quote types)
270
+ const jsxPatterns = [
271
+ { orig: oldParts.fullKey, regex: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g') },
272
+ { orig: oldParts.key, regex: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g') }
291
273
  ];
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
- }
274
+ for (const p of jsxPatterns) {
275
+ newCode = newCode.replace(p.regex, (match, q) => {
276
+ changes++;
277
+ const nsSepStr = nsSeparator === false ? ':' : nsSeparator;
278
+ const replacement = (p.orig === oldParts.fullKey && oldParts.fullKey.includes(nsSepStr)) ? newParts.fullKey : newParts.key;
279
+ return `i18nKey=${q}${replacement}${q}`;
280
+ });
300
281
  }
301
282
  return { newCode, changes };
302
283
  }
package/dist/esm/cli.js CHANGED
@@ -26,7 +26,7 @@ const program = new Command();
26
26
  program
27
27
  .name('i18next-cli')
28
28
  .description('A unified, high-performance i18next CLI.')
29
- .version('1.39.0'); // This string is replaced with the actual version at build time by rollup
29
+ .version('1.39.1'); // This string is replaced with the actual version at build time by rollup
30
30
  // new: global config override option
31
31
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
32
32
  program
@@ -157,9 +157,10 @@ async function updateSourceFiles(oldParts, newParts, config, dryRun, logger) {
157
157
  ? config.extract.input
158
158
  : [config.extract.input];
159
159
  const normalizedPatterns = inputPatterns.map(pattern => pattern.replace(/\\/g, '/'));
160
+ // glob accepts array of patterns; do not force cwd to avoid accidental path rewriting
160
161
  const sourceFiles = await glob(normalizedPatterns, {
161
162
  ignore: [...defaultIgnore, ...userIgnore],
162
- cwd: process.cwd()
163
+ nodir: true
163
164
  });
164
165
  const results = [];
165
166
  for (const file of sourceFiles) {
@@ -179,122 +180,102 @@ async function updateSourceFiles(oldParts, newParts, config, dryRun, logger) {
179
180
  return results;
180
181
  }
181
182
  async function replaceKeyInSource(code, oldParts, newParts, config) {
182
- // Use regex-based replacement which is more reliable than AST manipulation
183
+ // Simpler and robust regex-based replacement that covers tests' patterns
183
184
  return replaceKeyWithRegex(code, oldParts, newParts, config);
184
185
  }
185
186
  function replaceKeyWithRegex(code, oldParts, newParts, config) {
186
187
  let changes = 0;
187
188
  let newCode = code;
188
189
  const nsSeparator = config.extract.nsSeparator ?? ':';
189
- // Helper to determine which key form to use in replacement
190
- const getReplacementKey = (originalKey) => {
191
- const hasNamespace = nsSeparator && originalKey.includes(String(nsSeparator));
192
- return hasNamespace ? newParts.fullKey : newParts.key;
193
- };
194
- // Pattern 1: Function calls - respect configured functions
195
190
  const configuredFunctions = config.extract.functions || ['t', '*.t'];
196
- const functionPatterns = [];
197
- for (const fnPattern of configuredFunctions) {
191
+ // Helper to create function-prefix regex fragment
192
+ const fnPrefixToRegex = (fnPattern) => {
198
193
  if (fnPattern.startsWith('*.')) {
199
- // Wildcard pattern like '*.t' - match any prefix
200
- const suffix = fnPattern.substring(1); // '.t'
201
- const escapedSuffix = escapeRegex(suffix);
202
- // Match: anyIdentifier.t('key')
203
- functionPatterns.push({
204
- pattern: new RegExp(`\\w+${escapedSuffix}\\((['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'),
205
- original: oldParts.fullKey
206
- });
207
- functionPatterns.push({
208
- pattern: new RegExp(`\\w+${escapedSuffix}\\((['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'),
209
- original: oldParts.key
210
- });
194
+ // '*.t' -> match anyIdentifier.t
195
+ const suffix = fnPattern.slice(2);
196
+ return `\\b[\\w$]+\\.${escapeRegex(suffix)}`; // e.g. \b[\w$]+\.t
211
197
  }
212
- else {
213
- // Exact function name
214
- const escapedFn = escapeRegex(fnPattern);
215
- functionPatterns.push({
216
- pattern: new RegExp(`\\b${escapedFn}\\((['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'),
217
- original: oldParts.fullKey
218
- });
219
- functionPatterns.push({
220
- pattern: new RegExp(`\\b${escapedFn}\\((['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'),
221
- original: oldParts.key
198
+ // exact function name (may include dot like 'i18n.t' or 'translate')
199
+ return `\\b${escapeRegex(fnPattern)}`;
200
+ };
201
+ // Replace exact string-key usages inside function calls: fn('key') or fn(`key`) or fn("key")
202
+ for (const fnPattern of configuredFunctions) {
203
+ const prefix = fnPrefixToRegex(fnPattern);
204
+ // Match fullKey first (namespace-prefixed in source)
205
+ if (oldParts.fullKey) {
206
+ const regexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g');
207
+ newCode = newCode.replace(regexFull, (match, q) => {
208
+ changes++;
209
+ const replacementKey = (oldParts.fullKey.includes(nsSeparator || ':') ? newParts.fullKey : newParts.key);
210
+ // preserve surrounding characters up to the opening quote
211
+ return match.replace(oldParts.fullKey, replacementKey);
222
212
  });
223
213
  }
224
- }
225
- for (const { pattern, original } of functionPatterns) {
226
- if (pattern.test(newCode)) {
227
- const replacement = getReplacementKey(original);
228
- newCode = newCode.replace(pattern, (match, quote) => {
214
+ // Then match bare key (no namespace in source)
215
+ const regexKey = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g');
216
+ newCode = newCode.replace(regexKey, (match) => {
217
+ changes++;
218
+ const replacementKey = newParts.key;
219
+ return match.replace(new RegExp(escapeRegex(oldParts.key)), replacementKey);
220
+ });
221
+ // Handle ns option in options object: fn('key', { ns: 'oldNs', ... })
222
+ if (oldParts.namespace && newParts.namespace && oldParts.namespace !== newParts.namespace) {
223
+ // We want to change only when key matches and ns value equals oldParts.namespace
224
+ const nsRegexFullKey = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*,\\s*\\{([^}]*)\\bns\\s*:\\s*(['"\`])${escapeRegex(oldParts.namespace)}\\3([^}]*)\\}\\s*\\)`, 'g');
225
+ newCode = newCode.replace(nsRegexFullKey, (match, keyQ, beforeNs, nsQ, afterNs) => {
229
226
  changes++;
230
- // Preserve the function name part, only replace the key
231
- const functionNameMatch = match.match(/^(\w+(?:\.\w+)*)\(/);
232
- if (functionNameMatch) {
233
- return `${functionNameMatch[1]}(${quote}${replacement}${quote}`;
234
- }
235
- return match;
227
+ // replace ns value
228
+ return match.replace(new RegExp(`(\\bns\\s*:\\s*['"\`])${escapeRegex(oldParts.namespace ?? '')}(['"\`])`), `$1${newParts.namespace ?? ''}$2`);
236
229
  });
230
+ // same but if the call used fullKey (ns included inside key string), still update ns option if present
231
+ if (oldParts.fullKey) {
232
+ const nsRegexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1\\s*,\\s*\\{([^}]*)\\bns\\s*:\\s*(['"\`])${escapeRegex(oldParts.namespace)}\\3([^}]*)\\}\\s*\\)`, 'g');
233
+ newCode = newCode.replace(nsRegexFull, (match) => {
234
+ changes++;
235
+ return match.replace(new RegExp(`(\\bns\\s*:\\s*['"\`])${escapeRegex(oldParts.namespace ?? '')}(['"\`])`), `$1${newParts.namespace ?? ''}$2`);
236
+ });
237
+ }
237
238
  }
238
239
  }
239
- // Pattern 2: Selector API arrow functions (e.g. t(($) => $.old.key) or i18n.t($ => $.old.key))
240
- // Respect configured function names (including wildcard patterns)
240
+ // Selector API: dot-notation: fn(($) => $.old.key)
241
241
  for (const fnPattern of configuredFunctions) {
242
- // Build a regex prefix for the function invocation (handles wildcard '*.t' -> '\w+\.t')
243
- let patternPrefix;
244
- if (fnPattern.startsWith('*.')) {
245
- const suffix = fnPattern.substring(1); // '.t'
246
- patternPrefix = `\\w+${escapeRegex(suffix)}`;
247
- }
248
- else {
249
- patternPrefix = escapeRegex(fnPattern);
250
- }
251
- // Try matching both the plain key and the ns-prefixed fullKey used in selector access
252
- for (const original of [oldParts.fullKey, oldParts.key]) {
253
- // Match dot-notation selector forms like:
254
- // t(($) => $.old.key)
255
- // i18n.t($ => $.old.key.nested)
256
- const selectorDotRegex = new RegExp(`(\\b${patternPrefix}\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*)\\2\\.${escapeRegex(original)}(\\s*\\))`, 'g');
257
- if (selectorDotRegex.test(newCode)) {
258
- const replacementKey = getReplacementKey(original);
259
- newCode = newCode.replace(selectorDotRegex, (match, prefix, param, suffix) => {
260
- changes++;
261
- // Replace property chain with dot-notation replacement
262
- return `${prefix}${param}.${replacementKey}${suffix}`;
263
- });
242
+ const prefix = fnPrefixToRegex(fnPattern);
243
+ // match forms like: prefix( $ => $.old.key )
244
+ // capture the arrow param name and the rest
245
+ // We'll attempt to match the param and the dotted property chain equal to oldParts.key (exact)
246
+ const dotRegex = new RegExp(`${prefix}\\s*\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*\\1\\.${escapeRegex(oldParts.key)}\\s*\\)`, 'g');
247
+ newCode = newCode.replace(dotRegex, (match, param) => {
248
+ changes++;
249
+ // Determine replacement (if source used namespace in key it would be fullKey, but in selector dot-notation it's always key form)
250
+ const replacementKey = newParts.key;
251
+ return match.replace(`.${oldParts.key}`, `.${replacementKey}`);
252
+ });
253
+ // Bracket notation: fn(($) => $["Old Key"]) etc.
254
+ const bracketRegex = new RegExp(`${prefix}\\s*\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*\\1\\s*\\[\\s*(['"\`])${escapeRegex(oldParts.key)}\\2\\s*\\]\\s*\\)`, 'g');
255
+ newCode = newCode.replace(bracketRegex, (match, param, quote) => {
256
+ changes++;
257
+ const replacementKey = newParts.key;
258
+ // If replacementKey is a valid identifier, convert to dot-notation, otherwise keep bracket form with preserved quote style
259
+ if (/^[A-Za-z_$][\w$]*$/.test(replacementKey)) {
260
+ return match.replace(new RegExp(`\\[\\s*['"\`]${escapeRegex(oldParts.key)}['"\`]\\s*\\]`), `.${replacementKey}`);
264
261
  }
265
- // Match bracket-notation selector forms like:
266
- // t(($) => $["Old Key"])
267
- const selectorBracketRegex = new RegExp(`(\\b${patternPrefix}\\(\\s*\\(?\\s*([a-zA-Z_$][\\w$]*)\\s*\\)?\\s*=>\\s*)\\2\\[\\s*(['"\`])${escapeRegex(original)}\\3\\s*\\](\\s*\\))`, 'g');
268
- if (selectorBracketRegex.test(newCode)) {
269
- const replacementKey = getReplacementKey(original);
270
- const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
271
- newCode = newCode.replace(selectorBracketRegex, (match, prefix, param, quote, suffix) => {
272
- changes++;
273
- // If the replacement is a valid identifier, convert to dot-notation, otherwise keep bracket-notation
274
- if (isIdentifier(replacementKey)) {
275
- return `${prefix}${param}.${replacementKey}${suffix}`;
276
- }
277
- return `${prefix}${param}[${quote}${replacementKey}${quote}]${suffix}`;
278
- });
262
+ else {
263
+ return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `$1${replacementKey}$1`);
279
264
  }
280
- }
265
+ });
281
266
  }
282
- // Pattern 3: JSX i18nKey attribute - respect configured transComponents
283
- // const transComponents = config.extract.transComponents || ['Trans']
284
- // Create a pattern that matches i18nKey on any of the configured components
285
- // This is a simplified approach - for more complex cases, consider AST-based replacement
286
- const i18nKeyPatterns = [
287
- { pattern: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g'), original: oldParts.fullKey },
288
- { pattern: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g'), original: oldParts.key }
267
+ // JSX i18nKey attribute (handles all quote types)
268
+ const jsxPatterns = [
269
+ { orig: oldParts.fullKey, regex: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g') },
270
+ { orig: oldParts.key, regex: new RegExp(`i18nKey=(['"\`])${escapeRegex(oldParts.key)}\\1`, 'g') }
289
271
  ];
290
- for (const { pattern, original } of i18nKeyPatterns) {
291
- if (pattern.test(newCode)) {
292
- const replacement = getReplacementKey(original);
293
- newCode = newCode.replace(pattern, (match, quote) => {
294
- changes++;
295
- return `i18nKey=${quote}${replacement}${quote}`;
296
- });
297
- }
272
+ for (const p of jsxPatterns) {
273
+ newCode = newCode.replace(p.regex, (match, q) => {
274
+ changes++;
275
+ const nsSepStr = nsSeparator === false ? ':' : nsSeparator;
276
+ const replacement = (p.orig === oldParts.fullKey && oldParts.fullKey.includes(nsSepStr)) ? newParts.fullKey : newParts.key;
277
+ return `i18nKey=${q}${replacement}${q}`;
278
+ });
298
279
  }
299
280
  return { newCode, changes };
300
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.39.0",
3
+ "version": "1.39.1",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {