i18next-cli 1.56.0 → 1.56.2
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 +1 -1
- package/dist/cjs/linter.js +27 -4
- package/dist/cjs/status.js +85 -16
- package/dist/esm/cli.js +1 -1
- package/dist/esm/linter.js +27 -4
- package/dist/esm/status.js +85 -16
- package/package.json +1 -1
- package/types/status.d.ts.map +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ const program = new commander.Command();
|
|
|
32
32
|
program
|
|
33
33
|
.name('i18next-cli')
|
|
34
34
|
.description('A unified, high-performance i18next CLI.')
|
|
35
|
-
.version('1.56.
|
|
35
|
+
.version('1.56.2'); // This string is replaced with the actual version at build time by rollup
|
|
36
36
|
// new: global config override option
|
|
37
37
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
38
38
|
program
|
package/dist/cjs/linter.js
CHANGED
|
@@ -755,14 +755,22 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
755
755
|
}
|
|
756
756
|
if (node.type === 'StringLiteral') {
|
|
757
757
|
const parent = currentAncestors[currentAncestors.length - 2];
|
|
758
|
+
const grandparent = currentAncestors[currentAncestors.length - 3];
|
|
758
759
|
// Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
|
|
759
760
|
const insideIgnored = isWithinIgnoredElement(currentAncestors);
|
|
760
|
-
|
|
761
|
-
|
|
761
|
+
// A StringLiteral can be an attribute value in two forms:
|
|
762
|
+
// <tag attr="value" /> → parent is JSXAttribute
|
|
763
|
+
// <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
|
|
764
|
+
const attrNode = parent?.type === 'JSXAttribute'
|
|
765
|
+
? parent
|
|
766
|
+
: (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
|
|
767
|
+
if (attrNode && !insideIgnored) {
|
|
768
|
+
const rawAttrName = extractAttrName(attrNode.name);
|
|
762
769
|
const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
|
|
763
770
|
// Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
|
|
764
|
-
// when the nearest enclosing element is accepted.
|
|
765
|
-
const
|
|
771
|
+
// when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
|
|
772
|
+
const attrNodeIdx = currentAncestors.indexOf(attrNode);
|
|
773
|
+
const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
|
|
766
774
|
if (acceptedTagsSet && parentElement) {
|
|
767
775
|
const parentName = extractJSXName(parentElement);
|
|
768
776
|
if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
|
|
@@ -786,6 +794,21 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
786
794
|
}
|
|
787
795
|
}
|
|
788
796
|
}
|
|
797
|
+
// Hardcoded string inside a JSX expression container used as element child:
|
|
798
|
+
// <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
|
|
799
|
+
// Apply the same filters used for JSXText so this behaves like raw text.
|
|
800
|
+
const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
|
|
801
|
+
(grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
|
|
802
|
+
if (isJsxChildExpression && !insideIgnored) {
|
|
803
|
+
// Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
|
|
804
|
+
// only attribute strings are linted — skip JSX child text.
|
|
805
|
+
if (!(acceptedAttributesSet && !acceptedTagsSet)) {
|
|
806
|
+
const text = node.value.trim();
|
|
807
|
+
if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
|
|
808
|
+
nodesToLint.push(node);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
789
812
|
}
|
|
790
813
|
// Recurse into children
|
|
791
814
|
for (const key of Object.keys(node)) {
|
package/dist/cjs/status.js
CHANGED
|
@@ -10,6 +10,7 @@ require('glob');
|
|
|
10
10
|
var nestedObject = require('./utils/nested-object.js');
|
|
11
11
|
var fileUtils = require('./utils/file-utils.js');
|
|
12
12
|
var pluralRules = require('./utils/plural-rules.js');
|
|
13
|
+
var nesting = require('./utils/nesting.js');
|
|
13
14
|
var contextVariants = require('./utils/context-variants.js');
|
|
14
15
|
var funnelMsgTracker = require('./utils/funnel-msg-tracker.js');
|
|
15
16
|
require('./extractor/parsers/jsx-parser.js');
|
|
@@ -107,13 +108,23 @@ async function generateStatusReport(config) {
|
|
|
107
108
|
keysByNs,
|
|
108
109
|
locales: new Map(),
|
|
109
110
|
};
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
111
|
+
// Build per-namespace "virtual" key lists for translation entries that the
|
|
112
|
+
// AST-based extractor cannot see on its own. Both inputs come from the
|
|
113
|
+
// primary translation file:
|
|
114
|
+
//
|
|
115
|
+
// 1. Context variants of an accepting-context key (see issue #243).
|
|
116
|
+
// `t('exportType', { context: dynamic })` only registers the base key;
|
|
117
|
+
// the concrete `exportType_gas` / `exportType_water` variants live in
|
|
118
|
+
// the primary file.
|
|
119
|
+
//
|
|
120
|
+
// 2. Keys reachable only via `$t(...)` nested references from inside an
|
|
121
|
+
// existing translation value (see follow-up to issue #241).
|
|
122
|
+
// `"girlsAndBoys": "... $t(boys, {\"count\": x}) ..."` doesn't appear
|
|
123
|
+
// in source code, yet the referenced keys (`boys`, plus per-locale
|
|
124
|
+
// plural forms) must be checked in every secondary locale.
|
|
125
|
+
//
|
|
126
|
+
// Both scans need the primary translation file per namespace, so the load
|
|
127
|
+
// is shared.
|
|
117
128
|
const keysAcceptingContext = new Set();
|
|
118
129
|
for (const keys of keysByNs.values()) {
|
|
119
130
|
for (const k of keys) {
|
|
@@ -122,14 +133,61 @@ async function generateStatusReport(config) {
|
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
const contextVariantsByNs = new Map();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
const nestedReferenceKeysByNs = new Map();
|
|
137
|
+
const primaryMergedForScan = mergeNamespaces
|
|
138
|
+
? ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
|
|
139
|
+
: null;
|
|
140
|
+
const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
if (seen.has(value))
|
|
143
|
+
return;
|
|
144
|
+
seen.add(value);
|
|
145
|
+
const refs = nesting.parseNestedReferences(value, {
|
|
146
|
+
nestingPrefix: config.extract.nestingPrefix,
|
|
147
|
+
nestingSuffix: config.extract.nestingSuffix,
|
|
148
|
+
nestingOptionsSeparator: config.extract.nestingOptionsSeparator,
|
|
149
|
+
nsSeparator: config.extract.nsSeparator,
|
|
150
|
+
defaultNS: config.extract.defaultNS
|
|
151
|
+
});
|
|
152
|
+
for (const ref of refs) {
|
|
153
|
+
// References with an explicit namespace that differs from the current
|
|
154
|
+
// bucket are ignored — they belong to another namespace's scan.
|
|
155
|
+
const normalizedRefNs = ref.ns === undefined || ref.ns === null
|
|
156
|
+
? (config.extract.defaultNS ?? 'translation')
|
|
157
|
+
: ref.ns;
|
|
158
|
+
if (normalizedRefNs !== refNs)
|
|
159
|
+
continue;
|
|
160
|
+
if (ref.context !== undefined) {
|
|
161
|
+
const ctxKey = `${ref.key}${contextSeparator}${ref.context}`;
|
|
162
|
+
if (ref.hasCount) {
|
|
163
|
+
// Treat `key_ctx` as a base plural key; the per-locale loop
|
|
164
|
+
// expands it into the correct CLDR forms for each target locale.
|
|
165
|
+
bucket.push({ key: ctxKey, hasCount: true });
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
bucket.push({ key: ref.key });
|
|
169
|
+
bucket.push({ key: ctxKey });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (ref.hasCount) {
|
|
173
|
+
bucket.push({ key: ref.key, hasCount: true });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
bucket.push({ key: ref.key });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
181
|
+
for (const v of Object.values(value)) {
|
|
182
|
+
collectNestedRefsFromValue(v, refNs, bucket, seen);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
for (const ns of keysByNs.keys()) {
|
|
187
|
+
const primaryNsTranslations = mergeNamespaces
|
|
188
|
+
? (primaryMergedForScan?.[ns] ?? primaryMergedForScan ?? {})
|
|
189
|
+
: ((await fileUtils.loadTranslationFile(node_path.resolve(process.cwd(), fileUtils.getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
|
|
190
|
+
if (keysAcceptingContext.size > 0) {
|
|
133
191
|
const primaryKeys = nestedObject.getNestedKeys(primaryNsTranslations, keySeparator ?? '.');
|
|
134
192
|
const variants = [];
|
|
135
193
|
for (const primaryKey of primaryKeys) {
|
|
@@ -140,6 +198,10 @@ async function generateStatusReport(config) {
|
|
|
140
198
|
if (variants.length > 0)
|
|
141
199
|
contextVariantsByNs.set(ns, variants);
|
|
142
200
|
}
|
|
201
|
+
const nestedRefKeys = [];
|
|
202
|
+
collectNestedRefsFromValue(primaryNsTranslations, ns, nestedRefKeys, new Set());
|
|
203
|
+
if (nestedRefKeys.length > 0)
|
|
204
|
+
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
143
205
|
}
|
|
144
206
|
for (const locale of secondaryLanguages) {
|
|
145
207
|
let totalTranslatedForLocale = 0;
|
|
@@ -209,7 +271,14 @@ async function generateStatusReport(config) {
|
|
|
209
271
|
return primaryState;
|
|
210
272
|
};
|
|
211
273
|
const processedKeys = new Set();
|
|
212
|
-
|
|
274
|
+
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
275
|
+
// the primary translation file (see follow-up on issue #241). Both go
|
|
276
|
+
// through the same plural-expansion logic; processedKeys dedupes.
|
|
277
|
+
const nestedRefKeys = nestedReferenceKeysByNs.get(ns) || [];
|
|
278
|
+
const combinedKeysInNs = nestedRefKeys.length > 0
|
|
279
|
+
? [...keysInNs, ...nestedRefKeys]
|
|
280
|
+
: keysInNs;
|
|
281
|
+
for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of combinedKeysInNs) {
|
|
213
282
|
if (hasCount) {
|
|
214
283
|
if (isExpandedPlural) {
|
|
215
284
|
// This is an already-expanded plural variant key (e.g., key_one, key_other)
|
package/dist/esm/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ const program = new Command();
|
|
|
30
30
|
program
|
|
31
31
|
.name('i18next-cli')
|
|
32
32
|
.description('A unified, high-performance i18next CLI.')
|
|
33
|
-
.version('1.56.
|
|
33
|
+
.version('1.56.2'); // This string is replaced with the actual version at build time by rollup
|
|
34
34
|
// new: global config override option
|
|
35
35
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
36
36
|
program
|
package/dist/esm/linter.js
CHANGED
|
@@ -753,14 +753,22 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
753
753
|
}
|
|
754
754
|
if (node.type === 'StringLiteral') {
|
|
755
755
|
const parent = currentAncestors[currentAncestors.length - 2];
|
|
756
|
+
const grandparent = currentAncestors[currentAncestors.length - 3];
|
|
756
757
|
// Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
|
|
757
758
|
const insideIgnored = isWithinIgnoredElement(currentAncestors);
|
|
758
|
-
|
|
759
|
-
|
|
759
|
+
// A StringLiteral can be an attribute value in two forms:
|
|
760
|
+
// <tag attr="value" /> → parent is JSXAttribute
|
|
761
|
+
// <tag attr={"value"} /> → parent is JSXExpressionContainer, grandparent is JSXAttribute
|
|
762
|
+
const attrNode = parent?.type === 'JSXAttribute'
|
|
763
|
+
? parent
|
|
764
|
+
: (parent?.type === 'JSXExpressionContainer' && grandparent?.type === 'JSXAttribute' ? grandparent : null);
|
|
765
|
+
if (attrNode && !insideIgnored) {
|
|
766
|
+
const rawAttrName = extractAttrName(attrNode.name);
|
|
760
767
|
const attrNameLower = rawAttrName ? String(rawAttrName).toLowerCase() : null;
|
|
761
768
|
// Check tag-level acceptance if acceptedTagsSet provided: attributes should only be considered
|
|
762
|
-
// when the nearest enclosing element is accepted.
|
|
763
|
-
const
|
|
769
|
+
// when the nearest enclosing element is accepted. Use the ancestors above the attrNode.
|
|
770
|
+
const attrNodeIdx = currentAncestors.indexOf(attrNode);
|
|
771
|
+
const parentElement = currentAncestors.slice(0, attrNodeIdx).reverse().find(a => a && typeof a === 'object' && (a.type === 'JSXElement' || a.type === 'JSXOpeningElement' || a.type === 'JSXSelfClosingElement'));
|
|
764
772
|
if (acceptedTagsSet && parentElement) {
|
|
765
773
|
const parentName = extractJSXName(parentElement);
|
|
766
774
|
if (!parentName || !acceptedTagsSet.has(String(parentName).toLowerCase())) {
|
|
@@ -784,6 +792,21 @@ function findHardcodedStrings(ast, code, config) {
|
|
|
784
792
|
}
|
|
785
793
|
}
|
|
786
794
|
}
|
|
795
|
+
// Hardcoded string inside a JSX expression container used as element child:
|
|
796
|
+
// <tag>{"hello"}</tag> → parent is JSXExpressionContainer, grandparent is JSXElement/JSXFragment
|
|
797
|
+
// Apply the same filters used for JSXText so this behaves like raw text.
|
|
798
|
+
const isJsxChildExpression = parent?.type === 'JSXExpressionContainer' &&
|
|
799
|
+
(grandparent?.type === 'JSXElement' || grandparent?.type === 'JSXFragment');
|
|
800
|
+
if (isJsxChildExpression && !insideIgnored) {
|
|
801
|
+
// Respect attribute-only mode: when acceptedAttributes is set without acceptedTags,
|
|
802
|
+
// only attribute strings are linted — skip JSX child text.
|
|
803
|
+
if (!(acceptedAttributesSet && !acceptedTagsSet)) {
|
|
804
|
+
const text = node.value.trim();
|
|
805
|
+
if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
|
|
806
|
+
nodesToLint.push(node);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
787
810
|
}
|
|
788
811
|
// Recurse into children
|
|
789
812
|
for (const key of Object.keys(node)) {
|
package/dist/esm/status.js
CHANGED
|
@@ -8,6 +8,7 @@ import 'glob';
|
|
|
8
8
|
import { getNestedKeys, getNestedValue } from './utils/nested-object.js';
|
|
9
9
|
import { loadTranslationFile, getOutputPath } from './utils/file-utils.js';
|
|
10
10
|
import { safePluralRules } from './utils/plural-rules.js';
|
|
11
|
+
import { parseNestedReferences } from './utils/nesting.js';
|
|
11
12
|
import { isContextVariantOfAcceptingKey } from './utils/context-variants.js';
|
|
12
13
|
import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker.js';
|
|
13
14
|
import './extractor/parsers/jsx-parser.js';
|
|
@@ -105,13 +106,23 @@ async function generateStatusReport(config) {
|
|
|
105
106
|
keysByNs,
|
|
106
107
|
locales: new Map(),
|
|
107
108
|
};
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
109
|
+
// Build per-namespace "virtual" key lists for translation entries that the
|
|
110
|
+
// AST-based extractor cannot see on its own. Both inputs come from the
|
|
111
|
+
// primary translation file:
|
|
112
|
+
//
|
|
113
|
+
// 1. Context variants of an accepting-context key (see issue #243).
|
|
114
|
+
// `t('exportType', { context: dynamic })` only registers the base key;
|
|
115
|
+
// the concrete `exportType_gas` / `exportType_water` variants live in
|
|
116
|
+
// the primary file.
|
|
117
|
+
//
|
|
118
|
+
// 2. Keys reachable only via `$t(...)` nested references from inside an
|
|
119
|
+
// existing translation value (see follow-up to issue #241).
|
|
120
|
+
// `"girlsAndBoys": "... $t(boys, {\"count\": x}) ..."` doesn't appear
|
|
121
|
+
// in source code, yet the referenced keys (`boys`, plus per-locale
|
|
122
|
+
// plural forms) must be checked in every secondary locale.
|
|
123
|
+
//
|
|
124
|
+
// Both scans need the primary translation file per namespace, so the load
|
|
125
|
+
// is shared.
|
|
115
126
|
const keysAcceptingContext = new Set();
|
|
116
127
|
for (const keys of keysByNs.values()) {
|
|
117
128
|
for (const k of keys) {
|
|
@@ -120,14 +131,61 @@ async function generateStatusReport(config) {
|
|
|
120
131
|
}
|
|
121
132
|
}
|
|
122
133
|
const contextVariantsByNs = new Map();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
const nestedReferenceKeysByNs = new Map();
|
|
135
|
+
const primaryMergedForScan = mergeNamespaces
|
|
136
|
+
? ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, (defaultNS === false ? 'translation' : (defaultNS || 'translation')))))) || {})
|
|
137
|
+
: null;
|
|
138
|
+
const collectNestedRefsFromValue = (value, refNs, bucket, seen) => {
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
if (seen.has(value))
|
|
141
|
+
return;
|
|
142
|
+
seen.add(value);
|
|
143
|
+
const refs = parseNestedReferences(value, {
|
|
144
|
+
nestingPrefix: config.extract.nestingPrefix,
|
|
145
|
+
nestingSuffix: config.extract.nestingSuffix,
|
|
146
|
+
nestingOptionsSeparator: config.extract.nestingOptionsSeparator,
|
|
147
|
+
nsSeparator: config.extract.nsSeparator,
|
|
148
|
+
defaultNS: config.extract.defaultNS
|
|
149
|
+
});
|
|
150
|
+
for (const ref of refs) {
|
|
151
|
+
// References with an explicit namespace that differs from the current
|
|
152
|
+
// bucket are ignored — they belong to another namespace's scan.
|
|
153
|
+
const normalizedRefNs = ref.ns === undefined || ref.ns === null
|
|
154
|
+
? (config.extract.defaultNS ?? 'translation')
|
|
155
|
+
: ref.ns;
|
|
156
|
+
if (normalizedRefNs !== refNs)
|
|
157
|
+
continue;
|
|
158
|
+
if (ref.context !== undefined) {
|
|
159
|
+
const ctxKey = `${ref.key}${contextSeparator}${ref.context}`;
|
|
160
|
+
if (ref.hasCount) {
|
|
161
|
+
// Treat `key_ctx` as a base plural key; the per-locale loop
|
|
162
|
+
// expands it into the correct CLDR forms for each target locale.
|
|
163
|
+
bucket.push({ key: ctxKey, hasCount: true });
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
bucket.push({ key: ref.key });
|
|
167
|
+
bucket.push({ key: ctxKey });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (ref.hasCount) {
|
|
171
|
+
bucket.push({ key: ref.key, hasCount: true });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
bucket.push({ key: ref.key });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
179
|
+
for (const v of Object.values(value)) {
|
|
180
|
+
collectNestedRefsFromValue(v, refNs, bucket, seen);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
for (const ns of keysByNs.keys()) {
|
|
185
|
+
const primaryNsTranslations = mergeNamespaces
|
|
186
|
+
? (primaryMergedForScan?.[ns] ?? primaryMergedForScan ?? {})
|
|
187
|
+
: ((await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, primaryLanguage, ns)))) || {});
|
|
188
|
+
if (keysAcceptingContext.size > 0) {
|
|
131
189
|
const primaryKeys = getNestedKeys(primaryNsTranslations, keySeparator ?? '.');
|
|
132
190
|
const variants = [];
|
|
133
191
|
for (const primaryKey of primaryKeys) {
|
|
@@ -138,6 +196,10 @@ async function generateStatusReport(config) {
|
|
|
138
196
|
if (variants.length > 0)
|
|
139
197
|
contextVariantsByNs.set(ns, variants);
|
|
140
198
|
}
|
|
199
|
+
const nestedRefKeys = [];
|
|
200
|
+
collectNestedRefsFromValue(primaryNsTranslations, ns, nestedRefKeys, new Set());
|
|
201
|
+
if (nestedRefKeys.length > 0)
|
|
202
|
+
nestedReferenceKeysByNs.set(ns, nestedRefKeys);
|
|
141
203
|
}
|
|
142
204
|
for (const locale of secondaryLanguages) {
|
|
143
205
|
let totalTranslatedForLocale = 0;
|
|
@@ -207,7 +269,14 @@ async function generateStatusReport(config) {
|
|
|
207
269
|
return primaryState;
|
|
208
270
|
};
|
|
209
271
|
const processedKeys = new Set();
|
|
210
|
-
|
|
272
|
+
// Combine AST-extracted keys with nested-reference keys discovered in
|
|
273
|
+
// the primary translation file (see follow-up on issue #241). Both go
|
|
274
|
+
// through the same plural-expansion logic; processedKeys dedupes.
|
|
275
|
+
const nestedRefKeys = nestedReferenceKeysByNs.get(ns) || [];
|
|
276
|
+
const combinedKeysInNs = nestedRefKeys.length > 0
|
|
277
|
+
? [...keysInNs, ...nestedRefKeys]
|
|
278
|
+
: keysInNs;
|
|
279
|
+
for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of combinedKeysInNs) {
|
|
211
280
|
if (hasCount) {
|
|
212
281
|
if (isExpandedPlural) {
|
|
213
282
|
// This is an already-expanded plural variant key (e.g., key_one, key_other)
|
package/package.json
CHANGED
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,EAAgB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;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;AAqDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuBzF"}
|