i18next-cli 1.71.1 → 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 +5 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +59 -9
- package/dist/cjs/status.js +45 -12
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +59 -9
- package/dist/esm/status.js +45 -12
- package/package.json +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/status.d.ts.map +1 -1
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.
|
|
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
|
|
@@ -342,13 +342,31 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
342
342
|
: nsKeys;
|
|
343
343
|
// Prepare namespace pattern checking helpers
|
|
344
344
|
const rawPreserve = config.extract.preservePatterns || [];
|
|
345
|
+
// Fast equivalent of matching a `${objectKey}.*` glob per object key: instead of
|
|
346
|
+
// testing O(objectKeys) regexes per key, walk the key's '.' boundaries and look
|
|
347
|
+
// each ancestor prefix up in the Set — O(key depth). See issue #286.
|
|
348
|
+
const isUnderObjectKey = (key) => {
|
|
349
|
+
if (objectKeys.size === 0)
|
|
350
|
+
return false;
|
|
351
|
+
let i = key.indexOf('.');
|
|
352
|
+
while (i !== -1) {
|
|
353
|
+
if (objectKeys.has(key.slice(0, i)))
|
|
354
|
+
return true;
|
|
355
|
+
i = key.indexOf('.', i + 1);
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
};
|
|
345
359
|
// Helper to check if a key should be filtered out during extraction
|
|
346
360
|
const shouldFilterKey = (key) => {
|
|
347
|
-
// 1)
|
|
361
|
+
// 1) keys nested under a returnObjects / selector-API base key
|
|
362
|
+
if (isUnderObjectKey(key)) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
// 2) regex based patterns (existing behavior)
|
|
348
366
|
if (preservePatterns.some(re => re.test(key))) {
|
|
349
367
|
return true;
|
|
350
368
|
}
|
|
351
|
-
//
|
|
369
|
+
// 3) namespace:* style patterns (respect nsSeparator)
|
|
352
370
|
for (const rp of rawPreserve) {
|
|
353
371
|
if (typeof rp !== 'string')
|
|
354
372
|
continue;
|
|
@@ -365,11 +383,15 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
365
383
|
};
|
|
366
384
|
// Helper to check if an existing key should be preserved
|
|
367
385
|
const shouldPreserveExistingKey = (key) => {
|
|
368
|
-
// 1)
|
|
386
|
+
// 1) keys nested under a returnObjects / selector-API base key
|
|
387
|
+
if (isUnderObjectKey(key)) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
// 2) regex-style patterns
|
|
369
391
|
if (preservePatterns.some(re => re.test(key))) {
|
|
370
392
|
return true;
|
|
371
393
|
}
|
|
372
|
-
//
|
|
394
|
+
// 3) namespace:key patterns - check if pattern matches this namespace:key combination
|
|
373
395
|
for (const rp of rawPreserve) {
|
|
374
396
|
if (typeof rp !== 'string')
|
|
375
397
|
continue;
|
|
@@ -575,6 +597,19 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
575
597
|
}
|
|
576
598
|
}
|
|
577
599
|
}
|
|
600
|
+
// Precompute every proper ancestor prefix (up to each keySeparator boundary) of the
|
|
601
|
+
// extracted keys, so the per-key "is this a leaf?" check below is a single Set lookup
|
|
602
|
+
// instead of an O(keys) scan per key. See issue #286.
|
|
603
|
+
const parentPrefixesInNewKeys = new Set();
|
|
604
|
+
if (typeof keySeparator === 'string') {
|
|
605
|
+
for (const { key } of filteredKeys) {
|
|
606
|
+
let i = key.indexOf(keySeparator);
|
|
607
|
+
while (i !== -1 && i < key.length) {
|
|
608
|
+
parentPrefixesInNewKeys.add(key.slice(0, i));
|
|
609
|
+
i = key.indexOf(keySeparator, i + 1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
578
613
|
// 1. Build the object first, without any sorting.
|
|
579
614
|
for (const { key, defaultValue: defaultValue$1, explicitDefault, hasCount, isExpandedPlural, isOrdinal } of filteredKeys) {
|
|
580
615
|
// If this is a base plural key (hasCount true but not an already-expanded variant)
|
|
@@ -717,7 +752,7 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
717
752
|
// For flat keys there cannot be nested children, so treat them as leaves.
|
|
718
753
|
const isLeafInNewKeys = keySeparator === false
|
|
719
754
|
? true
|
|
720
|
-
: !
|
|
755
|
+
: !parentPrefixesInNewKeys.has(key);
|
|
721
756
|
const isDerivedDefault = isDerivedFromKey(key, defaultValue$1, explicitDefault);
|
|
722
757
|
// Determine if we should preserve an existing object
|
|
723
758
|
const shouldPreserveObject = typeof existingValue === 'object' && existingValue !== null && (objectKeys.has(key) || // Explicit returnObjects
|
|
@@ -982,8 +1017,9 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
|
|
|
982
1017
|
const pluralSeparator = config.extract.pluralSeparator ?? '_';
|
|
983
1018
|
// Same merged-output detection as the per-locale loop in getTranslations.
|
|
984
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));
|
|
985
1021
|
const mergedPrimaryFile = shouldMerge
|
|
986
|
-
? (await fileUtils.loadTranslationFile(
|
|
1022
|
+
? (await fileUtils.loadTranslationFile(mergedPrimaryPath) || {})
|
|
987
1023
|
: null;
|
|
988
1024
|
const catalogCache = new Map();
|
|
989
1025
|
const loadPrimaryCatalog = async (ns, isFallback) => {
|
|
@@ -991,9 +1027,19 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
|
|
|
991
1027
|
let catalog = catalogCache.get(cacheKey);
|
|
992
1028
|
if (!catalog) {
|
|
993
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
|
+
}
|
|
994
1040
|
// In merged mode the fallback keys may live at the top level when the
|
|
995
1041
|
// file is not namespaced (same resolution as the status command).
|
|
996
|
-
catalog
|
|
1042
|
+
catalog ??= (isFallback ? mergedPrimaryFile : {});
|
|
997
1043
|
}
|
|
998
1044
|
else {
|
|
999
1045
|
catalog = await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns))) || {};
|
|
@@ -1099,8 +1145,12 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
1099
1145
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])];
|
|
1100
1146
|
const indentation = config.extract.indentation ?? 2;
|
|
1101
1147
|
for (const key of objectKeys) {
|
|
1102
|
-
//
|
|
1103
|
-
|
|
1148
|
+
// Object keys are matched directly (and cheaply) against the objectKeys Set in
|
|
1149
|
+
// buildNewTranslationsForNs (see isUnderObjectKey). Only a key that itself
|
|
1150
|
+
// contains a wildcard still needs the glob-to-regex path.
|
|
1151
|
+
if (key.includes('*')) {
|
|
1152
|
+
patternsToPreserve.push(`${key}.*`);
|
|
1153
|
+
}
|
|
1104
1154
|
}
|
|
1105
1155
|
const preservePatterns = patternsToPreserve.map(globToRegex);
|
|
1106
1156
|
// Group keys by namespace. If the plugin recorded the namespace as implicit
|
package/dist/cjs/status.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -340,13 +340,31 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
340
340
|
: nsKeys;
|
|
341
341
|
// Prepare namespace pattern checking helpers
|
|
342
342
|
const rawPreserve = config.extract.preservePatterns || [];
|
|
343
|
+
// Fast equivalent of matching a `${objectKey}.*` glob per object key: instead of
|
|
344
|
+
// testing O(objectKeys) regexes per key, walk the key's '.' boundaries and look
|
|
345
|
+
// each ancestor prefix up in the Set — O(key depth). See issue #286.
|
|
346
|
+
const isUnderObjectKey = (key) => {
|
|
347
|
+
if (objectKeys.size === 0)
|
|
348
|
+
return false;
|
|
349
|
+
let i = key.indexOf('.');
|
|
350
|
+
while (i !== -1) {
|
|
351
|
+
if (objectKeys.has(key.slice(0, i)))
|
|
352
|
+
return true;
|
|
353
|
+
i = key.indexOf('.', i + 1);
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
};
|
|
343
357
|
// Helper to check if a key should be filtered out during extraction
|
|
344
358
|
const shouldFilterKey = (key) => {
|
|
345
|
-
// 1)
|
|
359
|
+
// 1) keys nested under a returnObjects / selector-API base key
|
|
360
|
+
if (isUnderObjectKey(key)) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
// 2) regex based patterns (existing behavior)
|
|
346
364
|
if (preservePatterns.some(re => re.test(key))) {
|
|
347
365
|
return true;
|
|
348
366
|
}
|
|
349
|
-
//
|
|
367
|
+
// 3) namespace:* style patterns (respect nsSeparator)
|
|
350
368
|
for (const rp of rawPreserve) {
|
|
351
369
|
if (typeof rp !== 'string')
|
|
352
370
|
continue;
|
|
@@ -363,11 +381,15 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
363
381
|
};
|
|
364
382
|
// Helper to check if an existing key should be preserved
|
|
365
383
|
const shouldPreserveExistingKey = (key) => {
|
|
366
|
-
// 1)
|
|
384
|
+
// 1) keys nested under a returnObjects / selector-API base key
|
|
385
|
+
if (isUnderObjectKey(key)) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
// 2) regex-style patterns
|
|
367
389
|
if (preservePatterns.some(re => re.test(key))) {
|
|
368
390
|
return true;
|
|
369
391
|
}
|
|
370
|
-
//
|
|
392
|
+
// 3) namespace:key patterns - check if pattern matches this namespace:key combination
|
|
371
393
|
for (const rp of rawPreserve) {
|
|
372
394
|
if (typeof rp !== 'string')
|
|
373
395
|
continue;
|
|
@@ -573,6 +595,19 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
573
595
|
}
|
|
574
596
|
}
|
|
575
597
|
}
|
|
598
|
+
// Precompute every proper ancestor prefix (up to each keySeparator boundary) of the
|
|
599
|
+
// extracted keys, so the per-key "is this a leaf?" check below is a single Set lookup
|
|
600
|
+
// instead of an O(keys) scan per key. See issue #286.
|
|
601
|
+
const parentPrefixesInNewKeys = new Set();
|
|
602
|
+
if (typeof keySeparator === 'string') {
|
|
603
|
+
for (const { key } of filteredKeys) {
|
|
604
|
+
let i = key.indexOf(keySeparator);
|
|
605
|
+
while (i !== -1 && i < key.length) {
|
|
606
|
+
parentPrefixesInNewKeys.add(key.slice(0, i));
|
|
607
|
+
i = key.indexOf(keySeparator, i + 1);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
576
611
|
// 1. Build the object first, without any sorting.
|
|
577
612
|
for (const { key, defaultValue, explicitDefault, hasCount, isExpandedPlural, isOrdinal } of filteredKeys) {
|
|
578
613
|
// If this is a base plural key (hasCount true but not an already-expanded variant)
|
|
@@ -715,7 +750,7 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
715
750
|
// For flat keys there cannot be nested children, so treat them as leaves.
|
|
716
751
|
const isLeafInNewKeys = keySeparator === false
|
|
717
752
|
? true
|
|
718
|
-
: !
|
|
753
|
+
: !parentPrefixesInNewKeys.has(key);
|
|
719
754
|
const isDerivedDefault = isDerivedFromKey(key, defaultValue, explicitDefault);
|
|
720
755
|
// Determine if we should preserve an existing object
|
|
721
756
|
const shouldPreserveObject = typeof existingValue === 'object' && existingValue !== null && (objectKeys.has(key) || // Explicit returnObjects
|
|
@@ -980,8 +1015,9 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
|
|
|
980
1015
|
const pluralSeparator = config.extract.pluralSeparator ?? '_';
|
|
981
1016
|
// Same merged-output detection as the per-locale loop in getTranslations.
|
|
982
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));
|
|
983
1019
|
const mergedPrimaryFile = shouldMerge
|
|
984
|
-
? (await loadTranslationFile(
|
|
1020
|
+
? (await loadTranslationFile(mergedPrimaryPath) || {})
|
|
985
1021
|
: null;
|
|
986
1022
|
const catalogCache = new Map();
|
|
987
1023
|
const loadPrimaryCatalog = async (ns, isFallback) => {
|
|
@@ -989,9 +1025,19 @@ async function reattributeFallbackNsKeys(keysByNS, noNsToken, config, primaryLan
|
|
|
989
1025
|
let catalog = catalogCache.get(cacheKey);
|
|
990
1026
|
if (!catalog) {
|
|
991
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
|
+
}
|
|
992
1038
|
// In merged mode the fallback keys may live at the top level when the
|
|
993
1039
|
// file is not namespaced (same resolution as the status command).
|
|
994
|
-
catalog
|
|
1040
|
+
catalog ??= (isFallback ? mergedPrimaryFile : {});
|
|
995
1041
|
}
|
|
996
1042
|
else {
|
|
997
1043
|
catalog = await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns))) || {};
|
|
@@ -1097,8 +1143,12 @@ async function getTranslations(keys, objectKeys, config, { syncPrimaryWithDefaul
|
|
|
1097
1143
|
const patternsToPreserve = [...(config.extract.preservePatterns || [])];
|
|
1098
1144
|
const indentation = config.extract.indentation ?? 2;
|
|
1099
1145
|
for (const key of objectKeys) {
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1146
|
+
// Object keys are matched directly (and cheaply) against the objectKeys Set in
|
|
1147
|
+
// buildNewTranslationsForNs (see isUnderObjectKey). Only a key that itself
|
|
1148
|
+
// contains a wildcard still needs the glob-to-regex path.
|
|
1149
|
+
if (key.includes('*')) {
|
|
1150
|
+
patternsToPreserve.push(`${key}.*`);
|
|
1151
|
+
}
|
|
1102
1152
|
}
|
|
1103
1153
|
const preservePatterns = patternsToPreserve.map(globToRegex);
|
|
1104
1154
|
// Group keys by namespace. If the plugin recorded the namespace as implicit
|
package/dist/esm/status.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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 +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;
|
|
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"}
|
package/types/status.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|