i18next-cli 1.39.7 → 1.39.9

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.7'); // This string is replaced with the actual version at build time by rollup
31
+ .version('1.39.9'); // 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
@@ -56,13 +56,21 @@ async function runRenameKey(config, oldKey, newKey, options = {}, logger$1 = new
56
56
  // Check for conflicts in translation files
57
57
  const conflicts = await checkConflicts(newParts, config);
58
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
- };
59
+ // If the old key doesn't exist in any translation file, treat this as a
60
+ // no-op (allow the command to succeed). This mirrors previous behavior
61
+ // where renaming a missing key doesn't fail just because the target
62
+ // already exists (it avoids blocking repeated/no-op renames).
63
+ const oldExists = await checkOldKeyExists(parseKeyWithNamespace(oldKey, config), config);
64
+ if (oldExists) {
65
+ return {
66
+ success: false,
67
+ sourceFiles: [],
68
+ translationFiles: [],
69
+ conflicts,
70
+ error: 'Target key already exists in translation files'
71
+ };
72
+ }
73
+ // otherwise: old key not present -> continue (no-op)
66
74
  }
67
75
  // Build a quick map of which namespaces contain which keys (union across locales).
68
76
  // This allows us to decide, per-call, whether an explicit `{ ns: 'x' }` refers to
@@ -155,6 +163,25 @@ async function checkConflicts(newParts, config) {
155
163
  }
156
164
  return conflicts;
157
165
  }
166
+ async function checkOldKeyExists(oldParts, config) {
167
+ const keySeparator = config.extract.keySeparator ?? '.';
168
+ for (const locale of config.locales) {
169
+ const outputPath = fileUtils.getOutputPath(config.extract.output, locale, oldParts.namespace);
170
+ const fullPath = node_path.resolve(process.cwd(), outputPath);
171
+ try {
172
+ const translations = await fileUtils.loadTranslationFile(fullPath);
173
+ if (translations) {
174
+ const val = nestedObject.getNestedValue(translations, oldParts.key, keySeparator);
175
+ if (val !== undefined)
176
+ return true;
177
+ }
178
+ }
179
+ catch {
180
+ // file missing — continue to next locale
181
+ }
182
+ }
183
+ return false;
184
+ }
158
185
  async function buildNamespaceKeyMap(config) {
159
186
  // Map namespace -> set of flattened keys present in that namespace (union across locales)
160
187
  const map = new Map();
@@ -323,7 +350,34 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
323
350
  });
324
351
  }
325
352
  //
326
- // 3) fullKey (explicitly namespaced string in call): only when user supplied a namespaced target
353
+ // 3a) Replace occurrences where the call uses an explicitly namespaced string
354
+ // literal like t('ns:key') while the CLI rename was invoked with the
355
+ // key without namespace (oldKey='key'). Example: defaultNS = 'ns1',
356
+ // source contains t('ns1:key'), and user called runRenameKey('key', 'key2').
357
+ // We should update 'ns1:key' -> 'ns1:key2' or to a new namespace if the
358
+ // target includes an explicit namespace.
359
+ //
360
+ // Only update explicit "ns:oldKey" string literals when the key name itself
361
+ // is changing. If only the namespace is changing but the key name stays
362
+ // identical (e.g. `key` -> `ns2:key`) we should NOT rewrite explicit
363
+ // `t('ns1:key')` occurrences — keep their explicit namespace intact.
364
+ if (oldParts.namespace && newParts.key !== oldParts.key) {
365
+ // ensure ns separator is a string for regex building (default ':')
366
+ const nsSepStr = nsSeparator === false ? ':' : String(nsSeparator);
367
+ const prefixed = `${escapeRegex(String(oldParts.namespace))}${escapeRegex(nsSepStr)}${escapeRegex(oldParts.key)}`;
368
+ const regexPrefixed = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${prefixed}\\1`, 'g');
369
+ newCode = newCode.replace(regexPrefixed, (match) => {
370
+ changes++;
371
+ // determine replacement: if newParts is explicitly namespaced, use fullKey;
372
+ // otherwise keep the original namespace but swap the key.
373
+ const replacement = newParts.explicitNamespace
374
+ ? newParts.fullKey
375
+ : `${oldParts.namespace}${nsSepStr}${newParts.key}`;
376
+ return match.replace(`${oldParts.namespace}${nsSepStr}${oldParts.key}`, replacement);
377
+ });
378
+ }
379
+ //
380
+ // 3b) fullKey (explicitly namespaced string in call): only when user supplied a namespaced target
327
381
  //
328
382
  if (oldParts.fullKey && oldParts.explicitNamespace) {
329
383
  const regexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g');
@@ -371,14 +425,22 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
371
425
  }
372
426
  //
373
427
  // 6) Bare calls without options: fn('key') -> fn('newKey')
428
+ // Apply this replacement only when the old key's namespace is the
429
+ // *effective* default namespace (config.extract.defaultNS ?? 'translation').
430
+ // This preserves previous behaviour: default-namespace bare-calls are
431
+ // considered "key form" and should be rewritten even when the translation
432
+ // file exists but the specific key isn't present.
374
433
  //
375
434
  {
376
- const regexKeyNoOptions = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*\\)`, 'g');
377
- newCode = newCode.replace(regexKeyNoOptions, (match, q) => {
378
- changes++;
379
- const replacementKey = newParts.key;
380
- return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `${q}${replacementKey}${q}`);
381
- });
435
+ const effectiveDefaultNS = config.extract.defaultNS ?? 'translation';
436
+ if (oldParts.namespace === effectiveDefaultNS) {
437
+ const regexKeyNoOptions = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*\\)`, 'g');
438
+ newCode = newCode.replace(regexKeyNoOptions, (match, q) => {
439
+ changes++;
440
+ const replacementKey = newParts.key;
441
+ return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `${q}${replacementKey}${q}`);
442
+ });
443
+ }
382
444
  }
383
445
  //
384
446
  // 7) JSX i18nKey attribute (handles both fullKey and key)
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.7'); // This string is replaced with the actual version at build time by rollup
29
+ .version('1.39.9'); // 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
@@ -54,13 +54,21 @@ async function runRenameKey(config, oldKey, newKey, options = {}, logger = new C
54
54
  // Check for conflicts in translation files
55
55
  const conflicts = await checkConflicts(newParts, config);
56
56
  if (conflicts.length > 0) {
57
- return {
58
- success: false,
59
- sourceFiles: [],
60
- translationFiles: [],
61
- conflicts,
62
- error: 'Target key already exists in translation files'
63
- };
57
+ // If the old key doesn't exist in any translation file, treat this as a
58
+ // no-op (allow the command to succeed). This mirrors previous behavior
59
+ // where renaming a missing key doesn't fail just because the target
60
+ // already exists (it avoids blocking repeated/no-op renames).
61
+ const oldExists = await checkOldKeyExists(parseKeyWithNamespace(oldKey, config), config);
62
+ if (oldExists) {
63
+ return {
64
+ success: false,
65
+ sourceFiles: [],
66
+ translationFiles: [],
67
+ conflicts,
68
+ error: 'Target key already exists in translation files'
69
+ };
70
+ }
71
+ // otherwise: old key not present -> continue (no-op)
64
72
  }
65
73
  // Build a quick map of which namespaces contain which keys (union across locales).
66
74
  // This allows us to decide, per-call, whether an explicit `{ ns: 'x' }` refers to
@@ -153,6 +161,25 @@ async function checkConflicts(newParts, config) {
153
161
  }
154
162
  return conflicts;
155
163
  }
164
+ async function checkOldKeyExists(oldParts, config) {
165
+ const keySeparator = config.extract.keySeparator ?? '.';
166
+ for (const locale of config.locales) {
167
+ const outputPath = getOutputPath(config.extract.output, locale, oldParts.namespace);
168
+ const fullPath = resolve(process.cwd(), outputPath);
169
+ try {
170
+ const translations = await loadTranslationFile(fullPath);
171
+ if (translations) {
172
+ const val = getNestedValue(translations, oldParts.key, keySeparator);
173
+ if (val !== undefined)
174
+ return true;
175
+ }
176
+ }
177
+ catch {
178
+ // file missing — continue to next locale
179
+ }
180
+ }
181
+ return false;
182
+ }
156
183
  async function buildNamespaceKeyMap(config) {
157
184
  // Map namespace -> set of flattened keys present in that namespace (union across locales)
158
185
  const map = new Map();
@@ -321,7 +348,34 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
321
348
  });
322
349
  }
323
350
  //
324
- // 3) fullKey (explicitly namespaced string in call): only when user supplied a namespaced target
351
+ // 3a) Replace occurrences where the call uses an explicitly namespaced string
352
+ // literal like t('ns:key') while the CLI rename was invoked with the
353
+ // key without namespace (oldKey='key'). Example: defaultNS = 'ns1',
354
+ // source contains t('ns1:key'), and user called runRenameKey('key', 'key2').
355
+ // We should update 'ns1:key' -> 'ns1:key2' or to a new namespace if the
356
+ // target includes an explicit namespace.
357
+ //
358
+ // Only update explicit "ns:oldKey" string literals when the key name itself
359
+ // is changing. If only the namespace is changing but the key name stays
360
+ // identical (e.g. `key` -> `ns2:key`) we should NOT rewrite explicit
361
+ // `t('ns1:key')` occurrences — keep their explicit namespace intact.
362
+ if (oldParts.namespace && newParts.key !== oldParts.key) {
363
+ // ensure ns separator is a string for regex building (default ':')
364
+ const nsSepStr = nsSeparator === false ? ':' : String(nsSeparator);
365
+ const prefixed = `${escapeRegex(String(oldParts.namespace))}${escapeRegex(nsSepStr)}${escapeRegex(oldParts.key)}`;
366
+ const regexPrefixed = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${prefixed}\\1`, 'g');
367
+ newCode = newCode.replace(regexPrefixed, (match) => {
368
+ changes++;
369
+ // determine replacement: if newParts is explicitly namespaced, use fullKey;
370
+ // otherwise keep the original namespace but swap the key.
371
+ const replacement = newParts.explicitNamespace
372
+ ? newParts.fullKey
373
+ : `${oldParts.namespace}${nsSepStr}${newParts.key}`;
374
+ return match.replace(`${oldParts.namespace}${nsSepStr}${oldParts.key}`, replacement);
375
+ });
376
+ }
377
+ //
378
+ // 3b) fullKey (explicitly namespaced string in call): only when user supplied a namespaced target
325
379
  //
326
380
  if (oldParts.fullKey && oldParts.explicitNamespace) {
327
381
  const regexFull = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.fullKey)}\\1`, 'g');
@@ -369,14 +423,22 @@ function replaceKeyWithRegex(code, oldParts, newParts, config, namespaceKeyMap)
369
423
  }
370
424
  //
371
425
  // 6) Bare calls without options: fn('key') -> fn('newKey')
426
+ // Apply this replacement only when the old key's namespace is the
427
+ // *effective* default namespace (config.extract.defaultNS ?? 'translation').
428
+ // This preserves previous behaviour: default-namespace bare-calls are
429
+ // considered "key form" and should be rewritten even when the translation
430
+ // file exists but the specific key isn't present.
372
431
  //
373
432
  {
374
- const regexKeyNoOptions = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*\\)`, 'g');
375
- newCode = newCode.replace(regexKeyNoOptions, (match, q) => {
376
- changes++;
377
- const replacementKey = newParts.key;
378
- return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `${q}${replacementKey}${q}`);
379
- });
433
+ const effectiveDefaultNS = config.extract.defaultNS ?? 'translation';
434
+ if (oldParts.namespace === effectiveDefaultNS) {
435
+ const regexKeyNoOptions = new RegExp(`${prefix}\\s*\\(\\s*(['"\`])${escapeRegex(oldParts.key)}\\1\\s*\\)`, 'g');
436
+ newCode = newCode.replace(regexKeyNoOptions, (match, q) => {
437
+ changes++;
438
+ const replacementKey = newParts.key;
439
+ return match.replace(new RegExp(`(['"\`])${escapeRegex(oldParts.key)}\\1`), `${q}${replacementKey}${q}`);
440
+ });
441
+ }
380
442
  }
381
443
  //
382
444
  // 7) JSX i18nKey attribute (handles both fullKey and key)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.39.7",
3
+ "version": "1.39.9",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"rename-key.d.ts","sourceRoot":"","sources":["../src/rename-key.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAO5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,OAAO,CAAA;CACZ,EACN,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,eAAe,CAAC,CA6D1B"}
1
+ {"version":3,"file":"rename-key.d.ts","sourceRoot":"","sources":["../src/rename-key.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAO5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,OAAO,CAAA;CACZ,EACN,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,eAAe,CAAC,CAqE1B"}