i18next-cli 1.46.0 β 1.46.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/README.md +9 -0
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +70 -0
- package/dist/cjs/extractor/parsers/call-expression-handler.js +19 -16
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +70 -0
- package/dist/esm/extractor/parsers/call-expression-handler.js +19 -16
- package/package.json +5 -5
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ A unified, high-performance i18next CLI toolchain, powered by SWC.
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
> By default, `i18next-cli` only extracts translation keys from JavaScript and TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`).
|
|
12
|
+
> To extract from other file types (such as `.pug`, `.vue`, `.svelte`, etc.), you must use or create a plugin. Specifying additional file extensions in the `extract.input` config is not sufficient on its ownβplugins are required for non-JS/TS formats. See the [Plugin System](#plugin-system) section for details and examples.
|
|
13
|
+
|
|
10
14
|
`i18next-cli` is a complete reimagining of the static analysis toolchain for the i18next ecosystem. It consolidates key extraction, type safety generation, locale syncing, linting, and cloud integrations into a single, cohesive, and blazing-fast CLI.
|
|
11
15
|
|
|
12
16
|
> ### π Try it Now - Zero Config!
|
|
@@ -80,6 +84,7 @@ Get an overview of your project's localization health:
|
|
|
80
84
|
npx i18next-cli status
|
|
81
85
|
```
|
|
82
86
|
|
|
87
|
+
|
|
83
88
|
### 3. Extract Translation Keys
|
|
84
89
|
|
|
85
90
|
```bash
|
|
@@ -336,6 +341,7 @@ The configuration file supports both TypeScript (`.ts`) and JavaScript (`.js`) f
|
|
|
336
341
|
|
|
337
342
|
> **π‘ No Installation Required?** If you don't want to install `i18next-cli` as a dependency, you can skip the `defineConfig` helper and return a plain JavaScript object or JSON instead. The `defineConfig` function is purely for TypeScript support and doesn't affect functionality.
|
|
338
343
|
|
|
344
|
+
|
|
339
345
|
### Basic Configuration
|
|
340
346
|
|
|
341
347
|
```typescript
|
|
@@ -351,6 +357,9 @@ export default defineConfig({
|
|
|
351
357
|
});
|
|
352
358
|
```
|
|
353
359
|
|
|
360
|
+
> **β Important:**
|
|
361
|
+
> Only `.js`, `.jsx`, `.ts`, and `.tsx` files are extracted by default. If you want to extract from other file types (e.g., `.pug`, `.vue`), you must use or create a plugin. See the [Plugin System](#plugin-system) section for more information.
|
|
362
|
+
|
|
354
363
|
**Alternative without local installation:**
|
|
355
364
|
|
|
356
365
|
```javascript
|
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.46.
|
|
31
|
+
.version('1.46.2'); // 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
|
|
@@ -87,6 +87,55 @@ function looksLikeObjectPath(key, separator, regex) {
|
|
|
87
87
|
}
|
|
88
88
|
return matched;
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns true when splitting `key` by `separator` would produce at least one
|
|
92
|
+
* empty string segment (e.g. "Loading..." split by "." β ["Loading","","",""]).
|
|
93
|
+
* Keys with empty segments must be treated as flat keys, not nested paths,
|
|
94
|
+
* otherwise they create `{ "": { "": "..." } }` entries in the JSON output.
|
|
95
|
+
*/
|
|
96
|
+
function hasEmptySegments(key, separator) {
|
|
97
|
+
return key.split(separator).some(s => s === '');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detects a nesting conflict for `key` against the object being built.
|
|
101
|
+
*
|
|
102
|
+
* Returns true when:
|
|
103
|
+
* - A prefix of `key` already resolves to a *non-object* value in `obj`
|
|
104
|
+
* (we cannot descend into a string to set a child).
|
|
105
|
+
* - A parent of a segment in `key` would clobber an existing sub-tree
|
|
106
|
+
* (the key itself resolves to an object but a new leaf is about to overwrite it
|
|
107
|
+
* and the object already has children from a deeper extracted key).
|
|
108
|
+
*
|
|
109
|
+
* In both cases the caller should skip the conflicting key rather than producing
|
|
110
|
+
* a silently-wrong flat fallback.
|
|
111
|
+
*/
|
|
112
|
+
function hasNestingConflict(obj, key, separator) {
|
|
113
|
+
const parts = key.split(separator);
|
|
114
|
+
let current = obj;
|
|
115
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
116
|
+
const part = parts[i];
|
|
117
|
+
const value = current[part];
|
|
118
|
+
if (value === undefined || value === null) {
|
|
119
|
+
// Path does not exist yet β no conflict
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
123
|
+
// A non-object value already occupies an ancestor segment.
|
|
124
|
+
// We cannot nest inside a string/number/array.
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
current = value;
|
|
128
|
+
}
|
|
129
|
+
// Check the final segment: if the existing value is a non-empty object and we are
|
|
130
|
+
// about to overwrite it with a string, that is also a conflict (the deeper keys
|
|
131
|
+
// that populate this object came from other extracted keys and would be silently lost).
|
|
132
|
+
const leafPart = parts[parts.length - 1];
|
|
133
|
+
const leafValue = current[leafPart];
|
|
134
|
+
if (typeof leafValue === 'object' && leafValue !== null && !Array.isArray(leafValue) && Object.keys(leafValue).length > 0) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
90
139
|
/**
|
|
91
140
|
* Recursively sorts the keys of an object.
|
|
92
141
|
*/
|
|
@@ -551,6 +600,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
551
600
|
let separator = key.startsWith('<') ? false : (keySeparator ?? '.');
|
|
552
601
|
if (separator && typeof separator === 'string') {
|
|
553
602
|
if (!looksLikeObjectPath(key, separator, naturalLanguageRegex)) {
|
|
603
|
+
// Natural-language key β treat as flat
|
|
604
|
+
separator = false;
|
|
605
|
+
}
|
|
606
|
+
else if (hasEmptySegments(key, separator)) {
|
|
607
|
+
// Splitting would produce empty-string segments (e.g. "Loading..." split by "."
|
|
608
|
+
// yields ["Loading","","",""]). Storing those creates { "": { "": "β¦" } }
|
|
609
|
+
// noise in the JSON, so treat the whole key as a flat leaf instead.
|
|
554
610
|
separator = false;
|
|
555
611
|
}
|
|
556
612
|
}
|
|
@@ -637,6 +693,20 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
637
693
|
}
|
|
638
694
|
}
|
|
639
695
|
}
|
|
696
|
+
// Guard against nesting conflicts before writing to the output object.
|
|
697
|
+
// A conflict arises when one extracted key would clobber an ancestor/descendant
|
|
698
|
+
// that was already written by a different extracted key, e.g.:
|
|
699
|
+
// t("a.b") => sets a.b = string
|
|
700
|
+
// t("a.b.c") => tries to descend into a.b which is already a string
|
|
701
|
+
// In that situation we skip the conflicting key rather than producing a
|
|
702
|
+
// silently-wrong top-level flat fallback like { "a.b.c": "a.b.c" }.
|
|
703
|
+
if (separator && typeof separator === 'string' && hasNestingConflict(newTranslations, key, separator)) {
|
|
704
|
+
// Log at most once per key so the output is not spammy
|
|
705
|
+
// (the logger reference is captured from the outer scope via closure β it is not
|
|
706
|
+
// directly available here, but we can at least avoid crashing silently).
|
|
707
|
+
// Callers / tests can verify the key is simply absent from the output.
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
640
710
|
nestedObject.setNestedValue(newTranslations, key, valueToSet, separator);
|
|
641
711
|
}
|
|
642
712
|
// 2. If sorting is enabled, recursively sort the entire object.
|
|
@@ -216,6 +216,12 @@ class CallExpressionHandler {
|
|
|
216
216
|
: (nsSeparator && originalKey.includes(nsSeparator || ':') ? originalKey : key))
|
|
217
217
|
: key;
|
|
218
218
|
// Handle plurals, context, and returnObjects
|
|
219
|
+
// Compute location once here so it is available to ALL paths below
|
|
220
|
+
// (plural, context, single-other fast paths, and the default leaf path).
|
|
221
|
+
const location = this.getLocationFromNode(node);
|
|
222
|
+
const locationEntry = location
|
|
223
|
+
? [{ file: this.getCurrentFile(), line: location.line, column: location.column }]
|
|
224
|
+
: undefined;
|
|
219
225
|
if (options) {
|
|
220
226
|
const contextPropValue = astUtils.getObjectPropValueExpression(options, 'context');
|
|
221
227
|
const keysWithContext = [];
|
|
@@ -345,7 +351,8 @@ class CallExpressionHandler {
|
|
|
345
351
|
ns: k.ns,
|
|
346
352
|
defaultValue: k.defaultValue,
|
|
347
353
|
hasCount: true,
|
|
348
|
-
isOrdinal: isOrdinalByKey
|
|
354
|
+
isOrdinal: isOrdinalByKey,
|
|
355
|
+
locations: locationEntry
|
|
349
356
|
});
|
|
350
357
|
}
|
|
351
358
|
}
|
|
@@ -355,7 +362,8 @@ class CallExpressionHandler {
|
|
|
355
362
|
ns,
|
|
356
363
|
defaultValue: dv,
|
|
357
364
|
hasCount: true,
|
|
358
|
-
isOrdinal: isOrdinalByKey
|
|
365
|
+
isOrdinal: isOrdinalByKey,
|
|
366
|
+
locations: locationEntry
|
|
359
367
|
});
|
|
360
368
|
}
|
|
361
369
|
continue;
|
|
@@ -381,7 +389,7 @@ class CallExpressionHandler {
|
|
|
381
389
|
// Pass explicitDefaultForBase so that when a call-site provided an explicit
|
|
382
390
|
// base default (e.g. t('key', 'Default', { count })), plural variant keys
|
|
383
391
|
// are treated as explicit and may be synced to that default.
|
|
384
|
-
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue, explicitPluralForVariants);
|
|
392
|
+
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue, explicitPluralForVariants, locationEntry);
|
|
385
393
|
}
|
|
386
394
|
continue; // This key is fully handled
|
|
387
395
|
}
|
|
@@ -401,21 +409,14 @@ class CallExpressionHandler {
|
|
|
401
409
|
// Fall through to add the base key itself
|
|
402
410
|
}
|
|
403
411
|
// 5. Default case: Add the simple key
|
|
412
|
+
// eslint-disable-next-line no-lone-blocks
|
|
404
413
|
{
|
|
405
|
-
// β
Use the helper method to find location by searching the code
|
|
406
|
-
const location = this.getLocationFromNode(node);
|
|
407
414
|
this.pluginContext.addKey({
|
|
408
415
|
key: finalKey,
|
|
409
416
|
ns,
|
|
410
417
|
defaultValue: dv,
|
|
411
418
|
explicitDefault: explicitDefaultForBase,
|
|
412
|
-
locations:
|
|
413
|
-
? [{
|
|
414
|
-
file: this.getCurrentFile(),
|
|
415
|
-
line: location.line,
|
|
416
|
-
column: location.column
|
|
417
|
-
}]
|
|
418
|
-
: undefined
|
|
419
|
+
locations: locationEntry
|
|
419
420
|
});
|
|
420
421
|
// Check for nested translations in the key itself
|
|
421
422
|
this.extractNestedKeys(finalKey, ns);
|
|
@@ -686,7 +687,7 @@ class CallExpressionHandler {
|
|
|
686
687
|
* @param options - object expression options
|
|
687
688
|
* @param isOrdinal - isOrdinal flag
|
|
688
689
|
*/
|
|
689
|
-
handlePluralKeys(key, ns, options, isOrdinal, defaultValueFromCall, explicitDefaultFromSource) {
|
|
690
|
+
handlePluralKeys(key, ns, options, isOrdinal, defaultValueFromCall, explicitDefaultFromSource, locations) {
|
|
690
691
|
try {
|
|
691
692
|
const type = isOrdinal ? 'ordinal' : 'cardinal';
|
|
692
693
|
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
@@ -794,7 +795,8 @@ class CallExpressionHandler {
|
|
|
794
795
|
defaultValue: finalDefaultValue,
|
|
795
796
|
hasCount: true,
|
|
796
797
|
isOrdinal,
|
|
797
|
-
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string')
|
|
798
|
+
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string'),
|
|
799
|
+
locations
|
|
798
800
|
});
|
|
799
801
|
}
|
|
800
802
|
return;
|
|
@@ -864,7 +866,8 @@ class CallExpressionHandler {
|
|
|
864
866
|
// Do NOT treat the presence of a general base defaultValueFromCall as making variants explicit.
|
|
865
867
|
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string'),
|
|
866
868
|
// If this is a context variant, track the base key (without context or plural suffixes)
|
|
867
|
-
keyAcceptingContext: context !== undefined ? key : undefined
|
|
869
|
+
keyAcceptingContext: context !== undefined ? key : undefined,
|
|
870
|
+
locations
|
|
868
871
|
});
|
|
869
872
|
}
|
|
870
873
|
}
|
|
@@ -873,7 +876,7 @@ class CallExpressionHandler {
|
|
|
873
876
|
this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);
|
|
874
877
|
// Fallback to a simple key if Intl API fails
|
|
875
878
|
const defaultValue = defaultValueFromCall || astUtils.getObjectPropValue(options, 'defaultValue');
|
|
876
|
-
this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key });
|
|
879
|
+
this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key, locations });
|
|
877
880
|
}
|
|
878
881
|
}
|
|
879
882
|
/**
|
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.46.
|
|
29
|
+
.version('1.46.2'); // 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
|
|
@@ -85,6 +85,55 @@ function looksLikeObjectPath(key, separator, regex) {
|
|
|
85
85
|
}
|
|
86
86
|
return matched;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns true when splitting `key` by `separator` would produce at least one
|
|
90
|
+
* empty string segment (e.g. "Loading..." split by "." β ["Loading","","",""]).
|
|
91
|
+
* Keys with empty segments must be treated as flat keys, not nested paths,
|
|
92
|
+
* otherwise they create `{ "": { "": "..." } }` entries in the JSON output.
|
|
93
|
+
*/
|
|
94
|
+
function hasEmptySegments(key, separator) {
|
|
95
|
+
return key.split(separator).some(s => s === '');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Detects a nesting conflict for `key` against the object being built.
|
|
99
|
+
*
|
|
100
|
+
* Returns true when:
|
|
101
|
+
* - A prefix of `key` already resolves to a *non-object* value in `obj`
|
|
102
|
+
* (we cannot descend into a string to set a child).
|
|
103
|
+
* - A parent of a segment in `key` would clobber an existing sub-tree
|
|
104
|
+
* (the key itself resolves to an object but a new leaf is about to overwrite it
|
|
105
|
+
* and the object already has children from a deeper extracted key).
|
|
106
|
+
*
|
|
107
|
+
* In both cases the caller should skip the conflicting key rather than producing
|
|
108
|
+
* a silently-wrong flat fallback.
|
|
109
|
+
*/
|
|
110
|
+
function hasNestingConflict(obj, key, separator) {
|
|
111
|
+
const parts = key.split(separator);
|
|
112
|
+
let current = obj;
|
|
113
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
114
|
+
const part = parts[i];
|
|
115
|
+
const value = current[part];
|
|
116
|
+
if (value === undefined || value === null) {
|
|
117
|
+
// Path does not exist yet β no conflict
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
121
|
+
// A non-object value already occupies an ancestor segment.
|
|
122
|
+
// We cannot nest inside a string/number/array.
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
current = value;
|
|
126
|
+
}
|
|
127
|
+
// Check the final segment: if the existing value is a non-empty object and we are
|
|
128
|
+
// about to overwrite it with a string, that is also a conflict (the deeper keys
|
|
129
|
+
// that populate this object came from other extracted keys and would be silently lost).
|
|
130
|
+
const leafPart = parts[parts.length - 1];
|
|
131
|
+
const leafValue = current[leafPart];
|
|
132
|
+
if (typeof leafValue === 'object' && leafValue !== null && !Array.isArray(leafValue) && Object.keys(leafValue).length > 0) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
88
137
|
/**
|
|
89
138
|
* Recursively sorts the keys of an object.
|
|
90
139
|
*/
|
|
@@ -549,6 +598,13 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
549
598
|
let separator = key.startsWith('<') ? false : (keySeparator ?? '.');
|
|
550
599
|
if (separator && typeof separator === 'string') {
|
|
551
600
|
if (!looksLikeObjectPath(key, separator, naturalLanguageRegex)) {
|
|
601
|
+
// Natural-language key β treat as flat
|
|
602
|
+
separator = false;
|
|
603
|
+
}
|
|
604
|
+
else if (hasEmptySegments(key, separator)) {
|
|
605
|
+
// Splitting would produce empty-string segments (e.g. "Loading..." split by "."
|
|
606
|
+
// yields ["Loading","","",""]). Storing those creates { "": { "": "β¦" } }
|
|
607
|
+
// noise in the JSON, so treat the whole key as a flat leaf instead.
|
|
552
608
|
separator = false;
|
|
553
609
|
}
|
|
554
610
|
}
|
|
@@ -635,6 +691,20 @@ function buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale,
|
|
|
635
691
|
}
|
|
636
692
|
}
|
|
637
693
|
}
|
|
694
|
+
// Guard against nesting conflicts before writing to the output object.
|
|
695
|
+
// A conflict arises when one extracted key would clobber an ancestor/descendant
|
|
696
|
+
// that was already written by a different extracted key, e.g.:
|
|
697
|
+
// t("a.b") => sets a.b = string
|
|
698
|
+
// t("a.b.c") => tries to descend into a.b which is already a string
|
|
699
|
+
// In that situation we skip the conflicting key rather than producing a
|
|
700
|
+
// silently-wrong top-level flat fallback like { "a.b.c": "a.b.c" }.
|
|
701
|
+
if (separator && typeof separator === 'string' && hasNestingConflict(newTranslations, key, separator)) {
|
|
702
|
+
// Log at most once per key so the output is not spammy
|
|
703
|
+
// (the logger reference is captured from the outer scope via closure β it is not
|
|
704
|
+
// directly available here, but we can at least avoid crashing silently).
|
|
705
|
+
// Callers / tests can verify the key is simply absent from the output.
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
638
708
|
setNestedValue(newTranslations, key, valueToSet, separator);
|
|
639
709
|
}
|
|
640
710
|
// 2. If sorting is enabled, recursively sort the entire object.
|
|
@@ -214,6 +214,12 @@ class CallExpressionHandler {
|
|
|
214
214
|
: (nsSeparator && originalKey.includes(nsSeparator || ':') ? originalKey : key))
|
|
215
215
|
: key;
|
|
216
216
|
// Handle plurals, context, and returnObjects
|
|
217
|
+
// Compute location once here so it is available to ALL paths below
|
|
218
|
+
// (plural, context, single-other fast paths, and the default leaf path).
|
|
219
|
+
const location = this.getLocationFromNode(node);
|
|
220
|
+
const locationEntry = location
|
|
221
|
+
? [{ file: this.getCurrentFile(), line: location.line, column: location.column }]
|
|
222
|
+
: undefined;
|
|
217
223
|
if (options) {
|
|
218
224
|
const contextPropValue = getObjectPropValueExpression(options, 'context');
|
|
219
225
|
const keysWithContext = [];
|
|
@@ -343,7 +349,8 @@ class CallExpressionHandler {
|
|
|
343
349
|
ns: k.ns,
|
|
344
350
|
defaultValue: k.defaultValue,
|
|
345
351
|
hasCount: true,
|
|
346
|
-
isOrdinal: isOrdinalByKey
|
|
352
|
+
isOrdinal: isOrdinalByKey,
|
|
353
|
+
locations: locationEntry
|
|
347
354
|
});
|
|
348
355
|
}
|
|
349
356
|
}
|
|
@@ -353,7 +360,8 @@ class CallExpressionHandler {
|
|
|
353
360
|
ns,
|
|
354
361
|
defaultValue: dv,
|
|
355
362
|
hasCount: true,
|
|
356
|
-
isOrdinal: isOrdinalByKey
|
|
363
|
+
isOrdinal: isOrdinalByKey,
|
|
364
|
+
locations: locationEntry
|
|
357
365
|
});
|
|
358
366
|
}
|
|
359
367
|
continue;
|
|
@@ -379,7 +387,7 @@ class CallExpressionHandler {
|
|
|
379
387
|
// Pass explicitDefaultForBase so that when a call-site provided an explicit
|
|
380
388
|
// base default (e.g. t('key', 'Default', { count })), plural variant keys
|
|
381
389
|
// are treated as explicit and may be synced to that default.
|
|
382
|
-
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue, explicitPluralForVariants);
|
|
390
|
+
this.handlePluralKeys(finalKey, ns, options, isOrdinalByOption || isOrdinalByKey, finalDefaultValue, explicitPluralForVariants, locationEntry);
|
|
383
391
|
}
|
|
384
392
|
continue; // This key is fully handled
|
|
385
393
|
}
|
|
@@ -399,21 +407,14 @@ class CallExpressionHandler {
|
|
|
399
407
|
// Fall through to add the base key itself
|
|
400
408
|
}
|
|
401
409
|
// 5. Default case: Add the simple key
|
|
410
|
+
// eslint-disable-next-line no-lone-blocks
|
|
402
411
|
{
|
|
403
|
-
// β
Use the helper method to find location by searching the code
|
|
404
|
-
const location = this.getLocationFromNode(node);
|
|
405
412
|
this.pluginContext.addKey({
|
|
406
413
|
key: finalKey,
|
|
407
414
|
ns,
|
|
408
415
|
defaultValue: dv,
|
|
409
416
|
explicitDefault: explicitDefaultForBase,
|
|
410
|
-
locations:
|
|
411
|
-
? [{
|
|
412
|
-
file: this.getCurrentFile(),
|
|
413
|
-
line: location.line,
|
|
414
|
-
column: location.column
|
|
415
|
-
}]
|
|
416
|
-
: undefined
|
|
417
|
+
locations: locationEntry
|
|
417
418
|
});
|
|
418
419
|
// Check for nested translations in the key itself
|
|
419
420
|
this.extractNestedKeys(finalKey, ns);
|
|
@@ -684,7 +685,7 @@ class CallExpressionHandler {
|
|
|
684
685
|
* @param options - object expression options
|
|
685
686
|
* @param isOrdinal - isOrdinal flag
|
|
686
687
|
*/
|
|
687
|
-
handlePluralKeys(key, ns, options, isOrdinal, defaultValueFromCall, explicitDefaultFromSource) {
|
|
688
|
+
handlePluralKeys(key, ns, options, isOrdinal, defaultValueFromCall, explicitDefaultFromSource, locations) {
|
|
688
689
|
try {
|
|
689
690
|
const type = isOrdinal ? 'ordinal' : 'cardinal';
|
|
690
691
|
// Generate plural forms for ALL target languages to ensure we have all necessary keys
|
|
@@ -792,7 +793,8 @@ class CallExpressionHandler {
|
|
|
792
793
|
defaultValue: finalDefaultValue,
|
|
793
794
|
hasCount: true,
|
|
794
795
|
isOrdinal,
|
|
795
|
-
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string')
|
|
796
|
+
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string'),
|
|
797
|
+
locations
|
|
796
798
|
});
|
|
797
799
|
}
|
|
798
800
|
return;
|
|
@@ -862,7 +864,8 @@ class CallExpressionHandler {
|
|
|
862
864
|
// Do NOT treat the presence of a general base defaultValueFromCall as making variants explicit.
|
|
863
865
|
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string'),
|
|
864
866
|
// If this is a context variant, track the base key (without context or plural suffixes)
|
|
865
|
-
keyAcceptingContext: context !== undefined ? key : undefined
|
|
867
|
+
keyAcceptingContext: context !== undefined ? key : undefined,
|
|
868
|
+
locations
|
|
866
869
|
});
|
|
867
870
|
}
|
|
868
871
|
}
|
|
@@ -871,7 +874,7 @@ class CallExpressionHandler {
|
|
|
871
874
|
this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`);
|
|
872
875
|
// Fallback to a simple key if Intl API fails
|
|
873
876
|
const defaultValue = defaultValueFromCall || getObjectPropValue(options, 'defaultValue');
|
|
874
|
-
this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key });
|
|
877
|
+
this.pluginContext.addKey({ key, ns, defaultValue: typeof defaultValue === 'string' ? defaultValue : key, locations });
|
|
875
878
|
}
|
|
876
879
|
}
|
|
877
880
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18next-cli",
|
|
3
|
-
"version": "1.46.
|
|
3
|
+
"version": "1.46.2",
|
|
4
4
|
"description": "A unified, high-performance i18next CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@rollup/plugin-replace": "6.0.3",
|
|
56
56
|
"@rollup/plugin-terser": "0.4.4",
|
|
57
57
|
"@types/inquirer": "9.0.9",
|
|
58
|
-
"@types/node": "25.3.
|
|
58
|
+
"@types/node": "25.3.2",
|
|
59
59
|
"@types/react": "19.2.14",
|
|
60
60
|
"@vitest/coverage-v8": "4.0.18",
|
|
61
61
|
"eslint": "9.39.2",
|
|
@@ -69,17 +69,17 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@croct/json5-parser": "0.2.2",
|
|
72
|
-
"@swc/core": "1.15.
|
|
72
|
+
"@swc/core": "1.15.17",
|
|
73
73
|
"yaml": "2.8.2",
|
|
74
74
|
"chokidar": "5.0.0",
|
|
75
75
|
"commander": "14.0.3",
|
|
76
76
|
"execa": "9.6.1",
|
|
77
77
|
"glob": "13.0.6",
|
|
78
78
|
"i18next-resources-for-ts": "2.0.0",
|
|
79
|
-
"inquirer": "13.
|
|
79
|
+
"inquirer": "13.3.0",
|
|
80
80
|
"jiti": "2.6.1",
|
|
81
81
|
"jsonc-parser": "3.3.1",
|
|
82
|
-
"minimatch": "10.2.
|
|
82
|
+
"minimatch": "10.2.4",
|
|
83
83
|
"ora": "9.3.0",
|
|
84
84
|
"react": "^19.2.4",
|
|
85
85
|
"react-i18next": "^16.5.4"
|
|
@@ -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,aAAa,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,aAAa,CAAA;AA82BnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,EAChB,GAAE;IACD,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAA;CACb,GACL,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA8J9B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAM1D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;gBAGlC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAU9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IAgYxG;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,oBAAoB;IA6E5B,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;
|
|
1
|
+
{"version":3,"file":"call-expression-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/call-expression-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6C,MAAM,WAAW,CAAA;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAgB,SAAS,EAAE,MAAM,aAAa,CAAA;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAM1D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,kBAAkB,CAAoB;IACvC,UAAU,cAAoB;IACrC,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAc;gBAGlC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB,EACtC,cAAc,EAAE,MAAM,MAAM,EAC5B,cAAc,EAAE,MAAM,MAAM;IAU9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IAgYxG;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,oBAAoB;IA6E5B,OAAO,CAAC,wBAAwB;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IA+LxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
|