i18next-cli 1.71.2 → 1.71.3

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/README.md CHANGED
@@ -921,6 +921,11 @@ export default defineConfig({
921
921
  // Namespaces to ignore during extraction, status, and sync operations.
922
922
  // Useful for monorepos where shared namespaces are managed elsewhere.
923
923
  // Keys using these namespaces will be excluded from processing.
924
+ // An ignored namespace can still act as a `fallbackNS` source: its
925
+ // translations are read (never written) for fallback accounting. If it
926
+ // lives in its own file outside a merged output (`mergeNamespaces: true`),
927
+ // use an `output` function that maps that namespace to its path, e.g.
928
+ // output: (lng, ns) => ns === 'shared' ? `locales/${lng}/${ns}.json` : `locales/${lng}.json`
924
929
  ignoreNamespaces: ['shared', 'common'], // Optional
925
930
 
926
931
  // When true (default), the extractor also scans code comments for t(...) / Trans examples and will extract keys found there.
package/dist/cjs/cli.js CHANGED
@@ -37,7 +37,7 @@ const program = new commander.Command();
37
37
  program
38
38
  .name('i18next-cli')
39
39
  .description('A unified, high-performance i18next CLI.')
40
- .version('1.71.2'); // This string is replaced with the actual version at build time by rollup
40
+ .version('1.71.3'); // This string is replaced with the actual version at build time by rollup
41
41
  // new: global config override option
42
42
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
43
43
  program
@@ -1017,8 +1017,9 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
1017
1017
  const pluralSeparator = config.extract.pluralSeparator ?? '_';
1018
1018
  // Same merged-output detection as the per-locale loop in getTranslations.
1019
1019
  const shouldMerge = config.extract.mergeNamespaces || (typeof config.extract.output === 'string' ? !config.extract.output.includes('{{namespace}}') : false);
1020
+ const mergedPrimaryPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage));
1020
1021
  const mergedPrimaryFile = shouldMerge
1021
- ? (await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage))) || {})
1022
+ ? (await fileUtils.loadTranslationFile(mergedPrimaryPath) || {})
1022
1023
  : null;
1023
1024
  const catalogCache = new Map();
1024
1025
  const loadPrimaryCatalog = async (ns, isFallback) => {
@@ -1026,9 +1027,19 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
1026
1027
  let catalog = catalogCache.get(cacheKey);
1027
1028
  if (!catalog) {
1028
1029
  if (mergedPrimaryFile) {
1030
+ catalog = mergedPrimaryFile[ns];
1031
+ if (catalog === undefined) {
1032
+ // The namespace may live in its own file outside the merged one
1033
+ // (split out and hidden via `ignoreNamespaces`, #287). Only
1034
+ // reachable when `output` resolves it to a separate path.
1035
+ const nsPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns));
1036
+ if (nsPath !== mergedPrimaryPath) {
1037
+ catalog = (await fileUtils.loadTranslationFile(nsPath)) ?? undefined;
1038
+ }
1039
+ }
1029
1040
  // In merged mode the fallback keys may live at the top level when the
1030
1041
  // file is not namespaced (same resolution as the status command).
1031
- catalog = mergedPrimaryFile[ns] ?? (isFallback ? mergedPrimaryFile : {});
1042
+ catalog ??= (isFallback ? mergedPrimaryFile : {});
1032
1043
  }
1033
1044
  else {
1034
1045
  catalog = await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns))) || {};
@@ -238,8 +238,16 @@ async function generateStatusReport(config) {
238
238
  }
239
239
  const contextVariantsByNs = new Map();
240
240
  const nestedReferenceKeysByNs = new Map();
241
+ // Absolute path of the merged translation file for a locale. A function
242
+ // `output` is called WITHOUT a namespace — the same way `extract` resolves
243
+ // the merged file it writes — so hybrid layouts (one merged file plus a few
244
+ // split-out namespace files) resolve correctly (#287). String templates keep
245
+ // the historical defaultNS substitution for the {{namespace}} placeholder.
246
+ const getMergedPath = (locale) => node_path.resolve(process.cwd(), typeof config.extract.output === 'function'
247
+ ? fileUtils.getOutputPath(config.extract.output, locale)
248
+ : fileUtils.getOutputPath(config.extract.output, locale, (defaultNS === false ? 'translation' : (defaultNS || 'translation'))));
241
249
  const primaryMergedForScan = mergeNamespaces
242
- ? ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
250
+ ? ((await fileUtils.loadTranslationFile(getMergedPath(primaryLanguage))) || {})
243
251
  : null;
244
252
  const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
245
253
  if (typeof value === 'string') {
@@ -322,28 +330,53 @@ async function generateStatusReport(config) {
322
330
  const namespaces = new Map();
323
331
  const mergedTranslations = mergeNamespaces
324
332
  // When merging namespaces we need to load the combined translation file.
325
- // The combined file lives under the regular output pattern and must include a namespace.
326
- // If defaultNS is explicitly false, fall back to the conventional "translation" file name.
327
- ? await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, locale, (defaultNS === false ? 'translation' : (defaultNS || 'translation'))))) || {}
333
+ ? await fileUtils.loadTranslationFile(getMergedPath(locale)) || {}
328
334
  : null;
335
+ // Load fallbackNS catalogs once per locale (looked up in order, like the
336
+ // i18next runtime). The per-namespace loop below skips a namespace's own
337
+ // entry.
338
+ const fallbackCatalogs = new Map();
339
+ for (const fallbackNs of fallbackNamespaces) {
340
+ if (mergeNamespaces) {
341
+ // In merged mode the fallback keys normally live in the merged file
342
+ // under the fallback namespace.
343
+ let catalog = mergedTranslations?.[fallbackNs];
344
+ if (catalog === undefined) {
345
+ // The fallback namespace may live in its own file outside the merged
346
+ // one (split out and hidden via `ignoreNamespaces`, #287). Only
347
+ // reachable when `output` resolves it to a separate path.
348
+ const nsPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, locale, fallbackNs));
349
+ if (nsPath !== getMergedPath(locale)) {
350
+ catalog = (await fileUtils.loadTranslationFile(nsPath)) ?? undefined;
351
+ }
352
+ }
353
+ if (catalog === undefined && isPrimary && ignoreNamespaces.has(fallbackNs)) {
354
+ console.warn(`⚠️ fallbackNS "${fallbackNs}" is listed in ignoreNamespaces, but no translations for it were found — keys resolving through it will be reported as absent. If the namespace lives in its own file, use an \`output\` function that maps it to that path.`);
355
+ }
356
+ // If it's still not found, fall back to the top level of the merged
357
+ // file (flat, non-namespaced merged files).
358
+ fallbackCatalogs.set(fallbackNs, catalog ?? mergedTranslations ?? {});
359
+ }
360
+ else {
361
+ const nsPath = node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, locale, fallbackNs));
362
+ const catalog = await fileUtils.loadTranslationFile(nsPath);
363
+ if (!catalog && isPrimary && ignoreNamespaces.has(fallbackNs)) {
364
+ console.warn(`⚠️ fallbackNS "${fallbackNs}" is listed in ignoreNamespaces, but no translations for it were found at "${nsPath}" — keys resolving through it will be reported as absent.`);
365
+ }
366
+ fallbackCatalogs.set(fallbackNs, catalog || {});
367
+ }
368
+ }
329
369
  for (const [ns, keysInNs] of keysByNs.entries()) {
330
370
  const translationsForNs = mergeNamespaces
331
371
  // If mergedTranslations is a flat object (no nested namespace) prefer the root object
332
372
  // when mergedTranslations[ns] is missing.
333
373
  ? (mergedTranslations?.[ns] ?? mergedTranslations ?? {})
334
374
  : await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, locale, ns))) || {};
335
- // Load fallbackNS translations if configured (looked up in order, like the i18next runtime)
336
375
  const fallbackTranslationsList = [];
337
376
  for (const fallbackNs of fallbackNamespaces) {
338
377
  if (ns === fallbackNs)
339
378
  continue;
340
- if (mergeNamespaces) {
341
- // In merged mode, fallbackNS keys are in mergedTranslations under the fallback namespace
342
- fallbackTranslationsList.push(mergedTranslations?.[fallbackNs] ?? mergedTranslations ?? {});
343
- }
344
- else {
345
- fallbackTranslationsList.push(await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, locale, fallbackNs))) || {});
346
- }
379
+ fallbackTranslationsList.push(fallbackCatalogs.get(fallbackNs));
347
380
  }
348
381
  let translatedInNs = 0;
349
382
  let emptyInNs = 0;
package/dist/esm/cli.js CHANGED
@@ -31,7 +31,7 @@ const program = new Command();
31
31
  program
32
32
  .name('i18next-cli')
33
33
  .description('A unified, high-performance i18next CLI.')
34
- .version('1.71.2'); // This string is replaced with the actual version at build time by rollup
34
+ .version('1.71.3'); // This string is replaced with the actual version at build time by rollup
35
35
  // new: global config override option
36
36
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
37
37
  program
@@ -1015,8 +1015,9 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
1015
1015
  const pluralSeparator = config.extract.pluralSeparator ?? '_';
1016
1016
  // Same merged-output detection as the per-locale loop in getTranslations.
1017
1017
  const shouldMerge = config.extract.mergeNamespaces || (typeof config.extract.output === 'string' ? !config.extract.output.includes('{{namespace}}') : false);
1018
+ const mergedPrimaryPath = resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage));
1018
1019
  const mergedPrimaryFile = shouldMerge
1019
- ? (await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage))) || {})
1020
+ ? (await loadTranslationFile(mergedPrimaryPath) || {})
1020
1021
  : null;
1021
1022
  const catalogCache = new Map();
1022
1023
  const loadPrimaryCatalog = async (ns, isFallback) => {
@@ -1024,9 +1025,19 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
1024
1025
  let catalog = catalogCache.get(cacheKey);
1025
1026
  if (!catalog) {
1026
1027
  if (mergedPrimaryFile) {
1028
+ catalog = mergedPrimaryFile[ns];
1029
+ if (catalog === undefined) {
1030
+ // The namespace may live in its own file outside the merged one
1031
+ // (split out and hidden via `ignoreNamespaces`, #287). Only
1032
+ // reachable when `output` resolves it to a separate path.
1033
+ const nsPath = resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns));
1034
+ if (nsPath !== mergedPrimaryPath) {
1035
+ catalog = (await loadTranslationFile(nsPath)) ?? undefined;
1036
+ }
1037
+ }
1027
1038
  // In merged mode the fallback keys may live at the top level when the
1028
1039
  // file is not namespaced (same resolution as the status command).
1029
- catalog = mergedPrimaryFile[ns] ?? (isFallback ? mergedPrimaryFile : {});
1040
+ catalog ??= (isFallback ? mergedPrimaryFile : {});
1030
1041
  }
1031
1042
  else {
1032
1043
  catalog = await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns))) || {};
@@ -232,8 +232,16 @@ async function generateStatusReport(config) {
232
232
  }
233
233
  const contextVariantsByNs = new Map();
234
234
  const nestedReferenceKeysByNs = new Map();
235
+ // Absolute path of the merged translation file for a locale. A function
236
+ // `output` is called WITHOUT a namespace — the same way `extract` resolves
237
+ // the merged file it writes — so hybrid layouts (one merged file plus a few
238
+ // split-out namespace files) resolve correctly (#287). String templates keep
239
+ // the historical defaultNS substitution for the {{namespace}} placeholder.
240
+ const getMergedPath = (locale) => resolve(process.cwd(), typeof config.extract.output === 'function'
241
+ ? getOutputPath(config.extract.output, locale)
242
+ : getOutputPath(config.extract.output, locale, (defaultNS === false ? 'translation' : (defaultNS || 'translation'))));
235
243
  const primaryMergedForScan = mergeNamespaces
236
- ? ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
244
+ ? ((await loadTranslationFile(getMergedPath(primaryLanguage))) || {})
237
245
  : null;
238
246
  const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
239
247
  if (typeof value === 'string') {
@@ -316,28 +324,53 @@ async function generateStatusReport(config) {
316
324
  const namespaces = new Map();
317
325
  const mergedTranslations = mergeNamespaces
318
326
  // When merging namespaces we need to load the combined translation file.
319
- // The combined file lives under the regular output pattern and must include a namespace.
320
- // If defaultNS is explicitly false, fall back to the conventional "translation" file name.
321
- ? await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, (defaultNS === false ? 'translation' : (defaultNS || 'translation'))))) || {}
327
+ ? await loadTranslationFile(getMergedPath(locale)) || {}
322
328
  : null;
329
+ // Load fallbackNS catalogs once per locale (looked up in order, like the
330
+ // i18next runtime). The per-namespace loop below skips a namespace's own
331
+ // entry.
332
+ const fallbackCatalogs = new Map();
333
+ for (const fallbackNs of fallbackNamespaces) {
334
+ if (mergeNamespaces) {
335
+ // In merged mode the fallback keys normally live in the merged file
336
+ // under the fallback namespace.
337
+ let catalog = mergedTranslations?.[fallbackNs];
338
+ if (catalog === undefined) {
339
+ // The fallback namespace may live in its own file outside the merged
340
+ // one (split out and hidden via `ignoreNamespaces`, #287). Only
341
+ // reachable when `output` resolves it to a separate path.
342
+ const nsPath = resolve(process.cwd(), getOutputPath(config.extract.output, locale, fallbackNs));
343
+ if (nsPath !== getMergedPath(locale)) {
344
+ catalog = (await loadTranslationFile(nsPath)) ?? undefined;
345
+ }
346
+ }
347
+ if (catalog === undefined && isPrimary && ignoreNamespaces.has(fallbackNs)) {
348
+ console.warn(`⚠️ fallbackNS "${fallbackNs}" is listed in ignoreNamespaces, but no translations for it were found — keys resolving through it will be reported as absent. If the namespace lives in its own file, use an \`output\` function that maps it to that path.`);
349
+ }
350
+ // If it's still not found, fall back to the top level of the merged
351
+ // file (flat, non-namespaced merged files).
352
+ fallbackCatalogs.set(fallbackNs, catalog ?? mergedTranslations ?? {});
353
+ }
354
+ else {
355
+ const nsPath = resolve(process.cwd(), getOutputPath(config.extract.output, locale, fallbackNs));
356
+ const catalog = await loadTranslationFile(nsPath);
357
+ if (!catalog && isPrimary && ignoreNamespaces.has(fallbackNs)) {
358
+ console.warn(`⚠️ fallbackNS "${fallbackNs}" is listed in ignoreNamespaces, but no translations for it were found at "${nsPath}" — keys resolving through it will be reported as absent.`);
359
+ }
360
+ fallbackCatalogs.set(fallbackNs, catalog || {});
361
+ }
362
+ }
323
363
  for (const [ns, keysInNs] of keysByNs.entries()) {
324
364
  const translationsForNs = mergeNamespaces
325
365
  // If mergedTranslations is a flat object (no nested namespace) prefer the root object
326
366
  // when mergedTranslations[ns] is missing.
327
367
  ? (mergedTranslations?.[ns] ?? mergedTranslations ?? {})
328
368
  : await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, ns))) || {};
329
- // Load fallbackNS translations if configured (looked up in order, like the i18next runtime)
330
369
  const fallbackTranslationsList = [];
331
370
  for (const fallbackNs of fallbackNamespaces) {
332
371
  if (ns === fallbackNs)
333
372
  continue;
334
- if (mergeNamespaces) {
335
- // In merged mode, fallbackNS keys are in mergedTranslations under the fallback namespace
336
- fallbackTranslationsList.push(mergedTranslations?.[fallbackNs] ?? mergedTranslations ?? {});
337
- }
338
- else {
339
- fallbackTranslationsList.push(await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, fallbackNs))) || {});
340
- }
373
+ fallbackTranslationsList.push(fallbackCatalogs.get(fallbackNs));
341
374
  }
342
375
  let translatedInNs = 0;
343
376
  let emptyInNs = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.71.2",
3
+ "version": "1.71.3",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AA8tC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EACf,oBAA4B,EAC5B,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6K9B"}
1
+ {"version":3,"file":"translation-manager.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/translation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAyuC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,EACvB,MAAM,EAAE,oBAAoB,EAC5B,EACE,uBAA+B,EAC/B,OAAe,EACf,oBAA4B,EAC5B,MAA4B,EAC7B,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;CACX,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6K9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAmC,MAAM,YAAY,CAAA;AAYvF;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyHD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAoDzF;AAsoBD;;GAEG;AACH,UAAU,aAAa;IACrB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuE/F"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAmC,MAAM,YAAY,CAAA;AAYvF;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyHD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAoDzF;AA4pBD;;GAEG;AACH,UAAU,aAAa;IACrB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuE/F"}