i18next-cli 1.17.1 → 1.18.0
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/CHANGELOG.md +11 -1
- package/README.md +34 -3
- package/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/key-finder.js +1 -1
- package/dist/cjs/extractor/core/translation-manager.js +1 -1
- package/dist/cjs/extractor/parsers/call-expression-handler.js +1 -1
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -1
- package/dist/cjs/extractor/parsers/jsx-handler.js +1 -1
- package/dist/cjs/linter.js +1 -1
- package/dist/cjs/status.js +1 -1
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/key-finder.js +1 -1
- package/dist/esm/extractor/core/translation-manager.js +1 -1
- package/dist/esm/extractor/parsers/call-expression-handler.js +1 -1
- package/dist/esm/extractor/parsers/comment-parser.js +1 -1
- package/dist/esm/extractor/parsers/jsx-handler.js +1 -1
- package/dist/esm/linter.js +1 -1
- package/dist/esm/status.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -3
- package/src/extractor/core/key-finder.ts +11 -0
- package/src/extractor/core/translation-manager.ts +94 -7
- package/src/extractor/parsers/call-expression-handler.ts +110 -0
- package/src/extractor/parsers/comment-parser.ts +12 -0
- package/src/extractor/parsers/jsx-handler.ts +18 -0
- package/src/linter.ts +93 -34
- package/src/status.ts +18 -10
- package/src/types.ts +3 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -1
- package/types/extractor/core/translation-manager.d.ts.map +1 -1
- package/types/extractor/parsers/call-expression-handler.d.ts.map +1 -1
- package/types/extractor/parsers/jsx-handler.d.ts.map +1 -1
- package/types/linter.d.ts +46 -2
- package/types/linter.d.ts.map +1 -1
- package/types/types.d.ts +2 -0
- package/types/types.d.ts.map +1 -1
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { runTypesGenerator } from './types-generator'
|
|
|
12
12
|
import { runSyncer } from './syncer'
|
|
13
13
|
import { runMigrator } from './migrator'
|
|
14
14
|
import { runInit } from './init'
|
|
15
|
-
import {
|
|
15
|
+
import { runLinterCli } from './linter'
|
|
16
16
|
import { runStatus } from './status'
|
|
17
17
|
import { runLocizeSync, runLocizeDownload, runLocizeMigrate } from './locize'
|
|
18
18
|
import type { I18nextToolkitConfig } from './types'
|
|
@@ -22,7 +22,7 @@ const program = new Command()
|
|
|
22
22
|
program
|
|
23
23
|
.name('i18next-cli')
|
|
24
24
|
.description('A unified, high-performance i18next CLI.')
|
|
25
|
-
.version('1.
|
|
25
|
+
.version('1.18.0')
|
|
26
26
|
|
|
27
27
|
// new: global config override option
|
|
28
28
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)')
|
|
@@ -174,7 +174,7 @@ program
|
|
|
174
174
|
console.log(chalk.green('Project structure detected successfully!'))
|
|
175
175
|
config = detected as I18nextToolkitConfig
|
|
176
176
|
}
|
|
177
|
-
await
|
|
177
|
+
await runLinterCli(config)
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Run the linter once initially
|
|
@@ -100,6 +100,17 @@ export async function findKeys (
|
|
|
100
100
|
await plugin.onEnd?.(allKeys)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// Normalization: mark already-expanded plural keys
|
|
104
|
+
const pluralSeparator = otherConfig.extract?.pluralSeparator ?? '_'
|
|
105
|
+
const categories = ['zero', 'one', 'two', 'few', 'many', 'other']
|
|
106
|
+
for (const ek of allKeys.values()) {
|
|
107
|
+
const parts = String(ek.key).split(pluralSeparator)
|
|
108
|
+
const last = parts[parts.length - 1]
|
|
109
|
+
const isOrdinal = parts.length >= 3 && parts[parts.length - 2] === 'ordinal'
|
|
110
|
+
if (categories.includes(last) || (isOrdinal && categories.includes(last))) {
|
|
111
|
+
;(ek as any).isExpandedPlural = true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
103
114
|
return { allKeys, objectKeys: astVisitors.objectKeys }
|
|
104
115
|
}
|
|
105
116
|
|
|
@@ -123,18 +123,26 @@ function buildNewTranslationsForNs (
|
|
|
123
123
|
|
|
124
124
|
// Get the plural categories for the target language
|
|
125
125
|
const targetLanguagePluralCategories = new Set<string>()
|
|
126
|
+
// Track cardinal plural categories separately so we can special-case single-"other" languages
|
|
127
|
+
let cardinalCategories: string[] = []
|
|
128
|
+
let ordinalCategories: string[] = []
|
|
126
129
|
try {
|
|
127
130
|
const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' })
|
|
128
131
|
const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' })
|
|
129
132
|
|
|
130
|
-
cardinalRules.resolvedOptions().pluralCategories
|
|
133
|
+
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories
|
|
134
|
+
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories
|
|
135
|
+
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat))
|
|
131
136
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`))
|
|
132
137
|
} catch (e) {
|
|
133
|
-
// Fallback to English if locale is invalid
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
// Fallback to primaryLanguage (or English) if locale is invalid
|
|
139
|
+
const fallbackLang = primaryLanguage || 'en'
|
|
140
|
+
const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' })
|
|
141
|
+
const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' })
|
|
142
|
+
|
|
143
|
+
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories
|
|
144
|
+
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories
|
|
145
|
+
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat))
|
|
138
146
|
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`))
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -153,6 +161,22 @@ function buildNewTranslationsForNs (
|
|
|
153
161
|
// For plural keys, check if this specific plural form is needed for the target language
|
|
154
162
|
const keyParts = key.split(pluralSeparator)
|
|
155
163
|
|
|
164
|
+
// If this is a base plural key (no plural suffix), keep it so that the
|
|
165
|
+
// builder can expand it to the target locale's plural forms.
|
|
166
|
+
if (hasCount && keyParts.length === 1) {
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Special-case single-cardinal-"other" languages (ja/zh/ko etc.):
|
|
171
|
+
// when the target language's cardinal categories are exactly ['other'],
|
|
172
|
+
// the extractor may have emitted the base key (no "_other" suffix).
|
|
173
|
+
// Accept the base key in that situation, while still accepting explicit *_other variants.
|
|
174
|
+
if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
|
|
175
|
+
// If this is a plain/base key (no plural suffix), include it.
|
|
176
|
+
if (keyParts.length === 1) return true
|
|
177
|
+
// Otherwise fall through and check the explicit suffix as before.
|
|
178
|
+
}
|
|
179
|
+
|
|
156
180
|
if (isOrdinal && keyParts.includes('ordinal')) {
|
|
157
181
|
// For ordinal plurals: key_context_ordinal_category or key_ordinal_category
|
|
158
182
|
const lastPart = keyParts[keyParts.length - 1]
|
|
@@ -166,6 +190,24 @@ function buildNewTranslationsForNs (
|
|
|
166
190
|
return true
|
|
167
191
|
})
|
|
168
192
|
|
|
193
|
+
// NEW: detect bases that already have expanded plural variants extracted.
|
|
194
|
+
// If a base has explicit expanded variants (e.g. key_one, key_other or key_ordinal_one),
|
|
195
|
+
// we should avoid generating/expanding the base plural key for that base to prevent
|
|
196
|
+
// double-generation / duplicate counting.
|
|
197
|
+
const expandedBases = new Set<string>()
|
|
198
|
+
for (const ek of filteredKeys) {
|
|
199
|
+
if (ek.isExpandedPlural) {
|
|
200
|
+
const parts = String(ek.key).split(pluralSeparator)
|
|
201
|
+
// If ordinal form like "key_ordinal_one" -> base should strip "_ordinal_<cat>"
|
|
202
|
+
if (parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
|
|
203
|
+
expandedBases.add(parts.slice(0, -2).join(pluralSeparator))
|
|
204
|
+
} else {
|
|
205
|
+
// strip single trailing category
|
|
206
|
+
expandedBases.add(parts.slice(0, -1).join(pluralSeparator))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
169
211
|
// If `removeUnusedKeys` is true, start with an empty object. Otherwise, start with a clone of the existing translations.
|
|
170
212
|
let newTranslations: Record<string, any> = removeUnusedKeys
|
|
171
213
|
? {}
|
|
@@ -208,7 +250,52 @@ function buildNewTranslationsForNs (
|
|
|
208
250
|
}
|
|
209
251
|
|
|
210
252
|
// 1. Build the object first, without any sorting.
|
|
211
|
-
for (const { key, defaultValue, explicitDefault } of filteredKeys) {
|
|
253
|
+
for (const { key, defaultValue, explicitDefault, hasCount, isExpandedPlural, isOrdinal } of filteredKeys) {
|
|
254
|
+
// If this is a base plural key (hasCount true but not an already-expanded variant)
|
|
255
|
+
// and we detected explicit expanded variants for this base, skip expanding the base.
|
|
256
|
+
if (hasCount && !isExpandedPlural) {
|
|
257
|
+
const parts = String(key).split(pluralSeparator)
|
|
258
|
+
let base = key
|
|
259
|
+
if (parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
|
|
260
|
+
base = parts.slice(0, -2).join(pluralSeparator)
|
|
261
|
+
} else if (parts.length >= 2) {
|
|
262
|
+
base = parts.slice(0, -1).join(pluralSeparator)
|
|
263
|
+
}
|
|
264
|
+
if (expandedBases.has(base)) {
|
|
265
|
+
// Skip generating/expanding this base key because explicit expanded forms exist.
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If this is a base plural key (no explicit suffix) and the locale is NOT the primary,
|
|
271
|
+
// expand it into locale-specific plural variants (e.g. key_one, key_other).
|
|
272
|
+
// Use the extracted defaultValue (fallback to base) for variant values.
|
|
273
|
+
if (hasCount && !isExpandedPlural) {
|
|
274
|
+
const parts = String(key).split(pluralSeparator)
|
|
275
|
+
const isBaseKey = parts.length === 1
|
|
276
|
+
if (isBaseKey && locale !== primaryLanguage) {
|
|
277
|
+
// If explicit expanded variants exist, do not expand the base.
|
|
278
|
+
const base = key
|
|
279
|
+
if (expandedBases.has(base)) {
|
|
280
|
+
// Skip expansion when explicit variants were provided
|
|
281
|
+
} else {
|
|
282
|
+
// choose categories based on ordinal flag
|
|
283
|
+
const categories = isOrdinal ? ordinalCategories : cardinalCategories
|
|
284
|
+
for (const category of categories) {
|
|
285
|
+
const finalKey = isOrdinal
|
|
286
|
+
? `${base}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
287
|
+
: `${base}${pluralSeparator}${category}`
|
|
288
|
+
|
|
289
|
+
// For secondary locales, prefer the extracted defaultValue (fallback to base) as test expects.
|
|
290
|
+
const valueToSet = (typeof defaultValue === 'string' && defaultValue !== undefined) ? defaultValue : base
|
|
291
|
+
setNestedValue(newTranslations, finalKey, valueToSet, keySeparator ?? '.')
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// We've expanded variants for this base key; skip the normal single-key handling.
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
212
299
|
const existingValue = getNestedValue(existingTranslations, key, keySeparator ?? '.')
|
|
213
300
|
const isLeafInNewKeys = !filteredKeys.some(otherKey => otherKey.key.startsWith(`${key}${keySeparator}`) && otherKey.key !== key)
|
|
214
301
|
|
|
@@ -261,6 +261,67 @@ export class CallExpressionHandler {
|
|
|
261
261
|
return false
|
|
262
262
|
})()
|
|
263
263
|
if (hasCount || isOrdinalByKey) {
|
|
264
|
+
// QUICK PATH: If ALL target locales only have the "other" category,
|
|
265
|
+
// emit base/context keys directly (avoid generating *_other). This
|
|
266
|
+
// mirrors the special-case in handlePluralKeys but is placed here as a
|
|
267
|
+
// defensive guard to ensure keys are always emitted.
|
|
268
|
+
try {
|
|
269
|
+
const typeForCheck = isOrdinalByKey ? 'ordinal' : 'cardinal'
|
|
270
|
+
// Prefer the configured primaryLanguage as the deciding signal for
|
|
271
|
+
// "single-other" languages (ja/zh/ko). Fall back to union of locales.
|
|
272
|
+
const primaryLang = this.config.extract?.primaryLanguage || (Array.isArray(this.config.locales) ? this.config.locales[0] : undefined) || 'en'
|
|
273
|
+
let isSingleOther = false
|
|
274
|
+
try {
|
|
275
|
+
const primaryCategories = new Intl.PluralRules(primaryLang, { type: typeForCheck }).resolvedOptions().pluralCategories
|
|
276
|
+
if (primaryCategories.length === 1 && primaryCategories[0] === 'other') {
|
|
277
|
+
isSingleOther = true
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore and fall back to union-of-locales check below
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!isSingleOther) {
|
|
284
|
+
const allPluralCategoriesCheck = new Set<string>()
|
|
285
|
+
for (const locale of this.config.locales) {
|
|
286
|
+
try {
|
|
287
|
+
const rules = new Intl.PluralRules(locale, { type: typeForCheck })
|
|
288
|
+
rules.resolvedOptions().pluralCategories.forEach(c => allPluralCategoriesCheck.add(c))
|
|
289
|
+
} catch {
|
|
290
|
+
new Intl.PluralRules('en', { type: typeForCheck }).resolvedOptions().pluralCategories.forEach(c => allPluralCategoriesCheck.add(c))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const pluralCategoriesCheck = Array.from(allPluralCategoriesCheck).sort()
|
|
294
|
+
if (pluralCategoriesCheck.length === 1 && pluralCategoriesCheck[0] === 'other') {
|
|
295
|
+
isSingleOther = true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (isSingleOther) {
|
|
300
|
+
// Emit only base/context keys (no _other) and skip the heavy plural path.
|
|
301
|
+
if (keysWithContext.length > 0) {
|
|
302
|
+
for (const k of keysWithContext) {
|
|
303
|
+
this.pluginContext.addKey({
|
|
304
|
+
key: k.key,
|
|
305
|
+
ns: k.ns,
|
|
306
|
+
defaultValue: k.defaultValue,
|
|
307
|
+
hasCount: true,
|
|
308
|
+
isOrdinal: isOrdinalByKey
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
this.pluginContext.addKey({
|
|
313
|
+
key: finalKey,
|
|
314
|
+
ns,
|
|
315
|
+
defaultValue: dv,
|
|
316
|
+
hasCount: true,
|
|
317
|
+
isOrdinal: isOrdinalByKey
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
// Ignore Intl failures here and fall through to normal logic
|
|
324
|
+
}
|
|
264
325
|
// Check if plurals are disabled
|
|
265
326
|
if (this.config.extract.disablePlurals) {
|
|
266
327
|
// When plurals are disabled, treat count as a regular option (for interpolation only)
|
|
@@ -480,6 +541,55 @@ export class CallExpressionHandler {
|
|
|
480
541
|
keysToGenerate.push({ key })
|
|
481
542
|
}
|
|
482
543
|
|
|
544
|
+
// If the only plural category across configured locales is "other",
|
|
545
|
+
// prefer the base key (no "_other" suffix) as it's more natural for languages
|
|
546
|
+
// with no grammatical plural forms (ja/zh/ko).
|
|
547
|
+
// Prefer the configured primaryLanguage as signal for single-"other" languages.
|
|
548
|
+
// If primaryLanguage indicates single-"other", treat as that case; otherwise
|
|
549
|
+
// fall back to earlier union-of-locales check that produced `pluralCategories`.
|
|
550
|
+
const primaryLang = this.config.extract?.primaryLanguage || (Array.isArray(this.config.locales) ? this.config.locales[0] : undefined) || 'en'
|
|
551
|
+
let primaryIsSingleOther = false
|
|
552
|
+
try {
|
|
553
|
+
const primaryCats = new Intl.PluralRules(primaryLang, { type }).resolvedOptions().pluralCategories
|
|
554
|
+
if (primaryCats.length === 1 && primaryCats[0] === 'other') primaryIsSingleOther = true
|
|
555
|
+
} catch {
|
|
556
|
+
primaryIsSingleOther = false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (primaryIsSingleOther || (pluralCategories.length === 1 && pluralCategories[0] === 'other')) {
|
|
560
|
+
for (const { key: baseKey, context } of keysToGenerate) {
|
|
561
|
+
const specificOther = getObjectPropValue(options, `defaultValue${pluralSeparator}other`)
|
|
562
|
+
// Final default resolution:
|
|
563
|
+
// 1) plural-specific defaultValue_other
|
|
564
|
+
// 2) general defaultValue (from options)
|
|
565
|
+
// 3) defaultValueFromCall (string arg)
|
|
566
|
+
// 4) fallback to key (or context-key for context variants)
|
|
567
|
+
let finalDefaultValue: string | undefined
|
|
568
|
+
if (typeof specificOther === 'string') {
|
|
569
|
+
finalDefaultValue = specificOther
|
|
570
|
+
} else if (typeof defaultValue === 'string') {
|
|
571
|
+
finalDefaultValue = defaultValue
|
|
572
|
+
} else if (typeof defaultValueFromCall === 'string') {
|
|
573
|
+
finalDefaultValue = defaultValueFromCall
|
|
574
|
+
} else {
|
|
575
|
+
finalDefaultValue = context ? `${baseKey}_${context}` : baseKey
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const ctxSep = this.config.extract.contextSeparator ?? '_'
|
|
579
|
+
const finalKey = context ? `${baseKey}${ctxSep}${context}` : baseKey
|
|
580
|
+
|
|
581
|
+
this.pluginContext.addKey({
|
|
582
|
+
key: finalKey,
|
|
583
|
+
ns,
|
|
584
|
+
defaultValue: finalDefaultValue,
|
|
585
|
+
hasCount: true,
|
|
586
|
+
isOrdinal,
|
|
587
|
+
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificOther === 'string')
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
483
593
|
// Generate plural forms for each key variant
|
|
484
594
|
for (const { key: baseKey, context } of keysToGenerate) {
|
|
485
595
|
for (const category of pluralCategories) {
|
|
@@ -185,6 +185,18 @@ function generatePluralKeys (
|
|
|
185
185
|
const pluralCategories = Array.from(allPluralCategories).sort()
|
|
186
186
|
const pluralSeparator = config.extract.pluralSeparator ?? '_'
|
|
187
187
|
|
|
188
|
+
// If the only plural category is "other", prefer emitting the base key instead of "key_other"
|
|
189
|
+
if (pluralCategories.length === 1 && pluralCategories[0] === 'other') {
|
|
190
|
+
// Emit base key only
|
|
191
|
+
pluginContext.addKey({
|
|
192
|
+
key,
|
|
193
|
+
ns,
|
|
194
|
+
defaultValue,
|
|
195
|
+
hasCount: true
|
|
196
|
+
})
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
// Generate keys for each plural category
|
|
189
201
|
for (const category of pluralCategories) {
|
|
190
202
|
const finalKey = isOrdinal
|
|
@@ -286,6 +286,24 @@ export class JSXHandler {
|
|
|
286
286
|
ordinalOtherDefault = getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}ordinal${pluralSeparator}other`) as string | undefined
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// Special-case single-"other" languages: generate base key (or context variant) instead of key_other
|
|
290
|
+
if (pluralCategories.length === 1 && pluralCategories[0] === 'other') {
|
|
291
|
+
// Determine final default for the base/other form
|
|
292
|
+
const specificDefault = optionsNode ? getObjectPropValue(optionsNode, `defaultValue${pluralSeparator}other`) as string | undefined : undefined
|
|
293
|
+
const finalDefault = typeof specificDefault === 'string' ? specificDefault : (typeof defaultValue === 'string' ? defaultValue : key)
|
|
294
|
+
|
|
295
|
+
// add base key (no suffix)
|
|
296
|
+
this.pluginContext.addKey({
|
|
297
|
+
key,
|
|
298
|
+
ns,
|
|
299
|
+
defaultValue: finalDefault,
|
|
300
|
+
hasCount: true,
|
|
301
|
+
isOrdinal,
|
|
302
|
+
explicitDefault: Boolean(explicitDefaultFromSource || typeof specificDefault === 'string' || typeof otherDefault === 'string')
|
|
303
|
+
})
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
289
307
|
for (const category of pluralCategories) {
|
|
290
308
|
// Look for the most specific default value (e.g., defaultValue_ordinal_one)
|
|
291
309
|
const specificDefaultKey = isOrdinal ? `defaultValue${pluralSeparator}ordinal${pluralSeparator}${category}` : `defaultValue${pluralSeparator}${category}`
|
package/src/linter.ts
CHANGED
|
@@ -1,10 +1,87 @@
|
|
|
1
1
|
import { glob } from 'glob'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { parse } from '@swc/core'
|
|
4
|
+
import { EventEmitter } from 'node:events'
|
|
4
5
|
import chalk from 'chalk'
|
|
5
6
|
import ora from 'ora'
|
|
6
7
|
import type { I18nextToolkitConfig } from './types'
|
|
7
8
|
|
|
9
|
+
type LinterEventMap = {
|
|
10
|
+
progress: [{
|
|
11
|
+
message: string;
|
|
12
|
+
}];
|
|
13
|
+
done: [{
|
|
14
|
+
success: boolean;
|
|
15
|
+
message: string;
|
|
16
|
+
files: Record<string, HardcodedString[]>;
|
|
17
|
+
}];
|
|
18
|
+
error: [error: Error];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Linter extends EventEmitter<LinterEventMap> {
|
|
22
|
+
private config: I18nextToolkitConfig
|
|
23
|
+
|
|
24
|
+
constructor (config: I18nextToolkitConfig) {
|
|
25
|
+
super({ captureRejections: true })
|
|
26
|
+
this.config = config
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
wrapError (error: unknown) {
|
|
30
|
+
const prefix = 'Linter failed to run: '
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
if (error.message.startsWith(prefix)) {
|
|
33
|
+
return error
|
|
34
|
+
}
|
|
35
|
+
const wrappedError = new Error(`${prefix}${error.message}`)
|
|
36
|
+
wrappedError.stack = error.stack
|
|
37
|
+
return wrappedError
|
|
38
|
+
}
|
|
39
|
+
return new Error(`${prefix}${String(error)}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async run () {
|
|
43
|
+
const { config } = this
|
|
44
|
+
try {
|
|
45
|
+
this.emit('progress', { message: 'Finding source files to analyze...' })
|
|
46
|
+
const defaultIgnore = ['node_modules/**']
|
|
47
|
+
const userIgnore = Array.isArray(config.extract.ignore)
|
|
48
|
+
? config.extract.ignore
|
|
49
|
+
: config.extract.ignore ? [config.extract.ignore] : []
|
|
50
|
+
|
|
51
|
+
const sourceFiles = await glob(config.extract.input, {
|
|
52
|
+
ignore: [...defaultIgnore, ...userIgnore]
|
|
53
|
+
})
|
|
54
|
+
this.emit('progress', { message: `Analyzing ${sourceFiles.length} source files...` })
|
|
55
|
+
let totalIssues = 0
|
|
56
|
+
const issuesByFile = new Map<string, HardcodedString[]>()
|
|
57
|
+
|
|
58
|
+
for (const file of sourceFiles) {
|
|
59
|
+
const code = await readFile(file, 'utf-8')
|
|
60
|
+
const ast = await parse(code, {
|
|
61
|
+
syntax: 'typescript',
|
|
62
|
+
tsx: true,
|
|
63
|
+
decorators: true
|
|
64
|
+
})
|
|
65
|
+
const hardcodedStrings = findHardcodedStrings(ast, code, config)
|
|
66
|
+
|
|
67
|
+
if (hardcodedStrings.length > 0) {
|
|
68
|
+
totalIssues += hardcodedStrings.length
|
|
69
|
+
issuesByFile.set(file, hardcodedStrings)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const files = Object.fromEntries(issuesByFile.entries())
|
|
74
|
+
const data = { success: totalIssues === 0, message: totalIssues > 0 ? `Linter found ${totalIssues} potential issues.` : 'No issues found.', files }
|
|
75
|
+
this.emit('done', data)
|
|
76
|
+
return data
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const wrappedError = this.wrapError(error)
|
|
79
|
+
this.emit('error', wrappedError)
|
|
80
|
+
throw wrappedError
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
8
85
|
/**
|
|
9
86
|
* Runs the i18next linter to detect hardcoded strings and other potential issues.
|
|
10
87
|
*
|
|
@@ -32,44 +109,25 @@ import type { I18nextToolkitConfig } from './types'
|
|
|
32
109
|
*
|
|
33
110
|
* await runLinter(config)
|
|
34
111
|
* // Outputs issues found or success message
|
|
35
|
-
* // Exits with code 1 if issues found, 0 if clean
|
|
36
112
|
* ```
|
|
37
113
|
*/
|
|
38
114
|
export async function runLinter (config: I18nextToolkitConfig) {
|
|
39
|
-
|
|
115
|
+
return new Linter(config).run()
|
|
116
|
+
}
|
|
40
117
|
|
|
118
|
+
export async function runLinterCli (config: I18nextToolkitConfig) {
|
|
119
|
+
const linter = new Linter(config)
|
|
120
|
+
const spinner = ora().start()
|
|
121
|
+
linter.on('progress', (event) => {
|
|
122
|
+
spinner.text = event.message
|
|
123
|
+
})
|
|
41
124
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: config.extract.ignore ? [config.extract.ignore] : []
|
|
46
|
-
|
|
47
|
-
const sourceFiles = await glob(config.extract.input, {
|
|
48
|
-
ignore: [...defaultIgnore, ...userIgnore]
|
|
49
|
-
})
|
|
50
|
-
let totalIssues = 0
|
|
51
|
-
const issuesByFile = new Map<string, HardcodedString[]>()
|
|
52
|
-
|
|
53
|
-
for (const file of sourceFiles) {
|
|
54
|
-
const code = await readFile(file, 'utf-8')
|
|
55
|
-
const ast = await parse(code, {
|
|
56
|
-
syntax: 'typescript',
|
|
57
|
-
tsx: true,
|
|
58
|
-
decorators: true
|
|
59
|
-
})
|
|
60
|
-
const hardcodedStrings = findHardcodedStrings(ast, code, config)
|
|
61
|
-
|
|
62
|
-
if (hardcodedStrings.length > 0) {
|
|
63
|
-
totalIssues += hardcodedStrings.length
|
|
64
|
-
issuesByFile.set(file, hardcodedStrings)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (totalIssues > 0) {
|
|
69
|
-
spinner.fail(chalk.red.bold(`Linter found ${totalIssues} potential issues.`))
|
|
125
|
+
const { success, message, files } = await linter.run()
|
|
126
|
+
if (!success) {
|
|
127
|
+
spinner.fail(chalk.red.bold(message))
|
|
70
128
|
|
|
71
129
|
// Print detailed report after spinner fails
|
|
72
|
-
for (const [file, issues] of
|
|
130
|
+
for (const [file, issues] of Object.entries(files)) {
|
|
73
131
|
console.log(chalk.yellow(`\n${file}`))
|
|
74
132
|
issues.forEach(({ text, line }) => {
|
|
75
133
|
console.log(` ${chalk.gray(`${line}:`)} ${chalk.red('Error:')} Found hardcoded string: "${text}"`)
|
|
@@ -77,11 +135,12 @@ export async function runLinter (config: I18nextToolkitConfig) {
|
|
|
77
135
|
}
|
|
78
136
|
process.exit(1)
|
|
79
137
|
} else {
|
|
80
|
-
spinner.succeed(chalk.green.bold(
|
|
138
|
+
spinner.succeed(chalk.green.bold(message))
|
|
81
139
|
}
|
|
82
140
|
} catch (error) {
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
const wrappedError = linter.wrapError(error)
|
|
142
|
+
spinner.fail(wrappedError.message)
|
|
143
|
+
console.error(wrappedError)
|
|
85
144
|
process.exit(1)
|
|
86
145
|
}
|
|
87
146
|
}
|
package/src/status.ts
CHANGED
|
@@ -124,20 +124,28 @@ async function generateStatusReport (config: I18nextToolkitConfig): Promise<Stat
|
|
|
124
124
|
const keyDetails: Array<{ key: string; isTranslated: boolean }> = []
|
|
125
125
|
|
|
126
126
|
// This is the new, language-aware logic loop
|
|
127
|
-
for (const { key: baseKey, hasCount, isOrdinal } of keysInNs) {
|
|
127
|
+
for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of keysInNs) {
|
|
128
128
|
if (hasCount) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const pluralCategories = new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories
|
|
132
|
-
for (const category of pluralCategories) {
|
|
129
|
+
// Rely only on the extractor-provided flag; extractor must set isExpandedPlural
|
|
130
|
+
if (isExpandedPlural) {
|
|
133
131
|
totalInNs++
|
|
134
|
-
const
|
|
135
|
-
? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
136
|
-
: `${baseKey}${pluralSeparator}${category}`
|
|
137
|
-
const value = getNestedValue(translationsForNs, pluralKey, keySeparator ?? '.')
|
|
132
|
+
const value = getNestedValue(translationsForNs, baseKey, keySeparator ?? '.')
|
|
138
133
|
const isTranslated = !!value
|
|
139
134
|
if (isTranslated) translatedInNs++
|
|
140
|
-
keyDetails.push({ key:
|
|
135
|
+
keyDetails.push({ key: baseKey, isTranslated })
|
|
136
|
+
} else {
|
|
137
|
+
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
138
|
+
const pluralCategories = new Intl.PluralRules(locale, { type }).resolvedOptions().pluralCategories
|
|
139
|
+
for (const category of pluralCategories) {
|
|
140
|
+
totalInNs++
|
|
141
|
+
const pluralKey = isOrdinal
|
|
142
|
+
? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
143
|
+
: `${baseKey}${pluralSeparator}${category}`
|
|
144
|
+
const value = getNestedValue(translationsForNs, pluralKey, keySeparator ?? '.')
|
|
145
|
+
const isTranslated = !!value
|
|
146
|
+
if (isTranslated) translatedInNs++
|
|
147
|
+
keyDetails.push({ key: pluralKey, isTranslated })
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
} else {
|
|
143
151
|
// It's a simple key
|
package/src/types.ts
CHANGED
|
@@ -319,6 +319,9 @@ export interface ExtractedKey {
|
|
|
319
319
|
|
|
320
320
|
/** Whether the defaultValue was explicitly provided in source code (vs derived from children/key) */
|
|
321
321
|
explicitDefault?: boolean;
|
|
322
|
+
|
|
323
|
+
/** True when the extractor returned an already-expanded plural form (e.g. "key_one") */
|
|
324
|
+
isExpandedPlural?: boolean
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-finder.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/key-finder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,oBAAoB,EAAmB,MAAM,aAAa,CAAA;AAM9F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"key-finder.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/key-finder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,oBAAoB,EAAmB,MAAM,aAAa,CAAA;AAM9F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CA2E1E"}
|
|
@@ -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;AAkanF;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,EAAE,uBAA+B,EAAE,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6F9B"}
|
|
@@ -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;AAG1D,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;gBAGnC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB;IAQxC;;;;;;;;;;;;;OAaG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;
|
|
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;AAG1D,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;gBAGnC,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,kBAAkB,EAAE,kBAAkB;IAQxC;;;;;;;;;;;;;OAaG;IACH,oBAAoB,CAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,GAAG,IAAI;IA2UxG;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,gBAAgB;IA2LxB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CA2BxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAgB,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAI1D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAoB;gBAG5C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB;IAOxC;;;;;;;;OAQG;IACH,gBAAgB,CAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IA0OjI;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;
|
|
1
|
+
{"version":3,"file":"jsx-handler.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/jsx-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,WAAW,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAgB,MAAM,aAAa,CAAA;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAI1D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAAoB;gBAG5C,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,kBAAkB,EAAE,kBAAkB;IAOxC;;;;;;;;OAQG;IACH,gBAAgB,CAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAG,IAAI;IA0OjI;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IAkFlC;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;CAevB"}
|
package/types/linter.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
import type { I18nextToolkitConfig } from './types';
|
|
3
|
+
type LinterEventMap = {
|
|
4
|
+
progress: [
|
|
5
|
+
{
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
];
|
|
9
|
+
done: [
|
|
10
|
+
{
|
|
11
|
+
success: boolean;
|
|
12
|
+
message: string;
|
|
13
|
+
files: Record<string, HardcodedString[]>;
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
error: [error: Error];
|
|
17
|
+
};
|
|
18
|
+
export declare class Linter extends EventEmitter<LinterEventMap> {
|
|
19
|
+
private config;
|
|
20
|
+
constructor(config: I18nextToolkitConfig);
|
|
21
|
+
wrapError(error: unknown): Error;
|
|
22
|
+
run(): Promise<{
|
|
23
|
+
success: boolean;
|
|
24
|
+
message: string;
|
|
25
|
+
files: {
|
|
26
|
+
[k: string]: HardcodedString[];
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* Runs the i18next linter to detect hardcoded strings and other potential issues.
|
|
4
32
|
*
|
|
@@ -26,8 +54,24 @@ import type { I18nextToolkitConfig } from './types';
|
|
|
26
54
|
*
|
|
27
55
|
* await runLinter(config)
|
|
28
56
|
* // Outputs issues found or success message
|
|
29
|
-
* // Exits with code 1 if issues found, 0 if clean
|
|
30
57
|
* ```
|
|
31
58
|
*/
|
|
32
|
-
export declare function runLinter(config: I18nextToolkitConfig): Promise<
|
|
59
|
+
export declare function runLinter(config: I18nextToolkitConfig): Promise<{
|
|
60
|
+
success: boolean;
|
|
61
|
+
message: string;
|
|
62
|
+
files: {
|
|
63
|
+
[k: string]: HardcodedString[];
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
export declare function runLinterCli(config: I18nextToolkitConfig): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Represents a found hardcoded string with its location information.
|
|
69
|
+
*/
|
|
70
|
+
interface HardcodedString {
|
|
71
|
+
/** The hardcoded text content */
|
|
72
|
+
text: string;
|
|
73
|
+
/** Line number where the string was found */
|
|
74
|
+
line: number;
|
|
75
|
+
}
|
|
76
|
+
export {};
|
|
33
77
|
//# sourceMappingURL=linter.d.ts.map
|
package/types/linter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE;QAAC;YACT,OAAO,EAAE,MAAM,CAAC;SACjB;KAAC,CAAC;IACH,IAAI,EAAE;QAAC;YACL,OAAO,EAAE,OAAO,CAAC;YACjB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;SAC1C;KAAC,CAAC;IACH,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CACvB,CAAA;AAED,qBAAa,MAAO,SAAQ,YAAY,CAAC,cAAc,CAAC;IACtD,OAAO,CAAC,MAAM,CAAsB;gBAEvB,MAAM,EAAE,oBAAoB;IAKzC,SAAS,CAAE,KAAK,EAAE,OAAO;IAanB,GAAG;;;;;;;CAyCV;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB;;;;;;GAE5D;AAED,wBAAsB,YAAY,CAAE,MAAM,EAAE,oBAAoB,iBA4B/D;AAED;;GAEG;AACH,UAAU,eAAe;IACvB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/types/types.d.ts
CHANGED
|
@@ -262,6 +262,8 @@ export interface ExtractedKey {
|
|
|
262
262
|
contextExpression?: Expression;
|
|
263
263
|
/** Whether the defaultValue was explicitly provided in source code (vs derived from children/key) */
|
|
264
264
|
explicitDefault?: boolean;
|
|
265
|
+
/** True when the extractor returned an already-expanded plural form (e.g. "key_one") */
|
|
266
|
+
isExpandedPlural?: boolean;
|
|
265
267
|
}
|
|
266
268
|
/**
|
|
267
269
|
* Result of processing translation files for a specific locale and namespace.
|