i18next-cli 1.24.13 → 1.24.14
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/esm/cli.js +1 -1
- package/package.json +6 -6
- package/types/cli.d.ts +3 -1
- package/types/cli.d.ts.map +1 -1
- package/CHANGELOG.md +0 -599
- package/src/cli.ts +0 -283
- package/src/config.ts +0 -215
- package/src/extractor/core/ast-visitors.ts +0 -259
- package/src/extractor/core/extractor.ts +0 -250
- package/src/extractor/core/key-finder.ts +0 -142
- package/src/extractor/core/translation-manager.ts +0 -750
- package/src/extractor/index.ts +0 -7
- package/src/extractor/parsers/ast-utils.ts +0 -87
- package/src/extractor/parsers/call-expression-handler.ts +0 -793
- package/src/extractor/parsers/comment-parser.ts +0 -424
- package/src/extractor/parsers/expression-resolver.ts +0 -391
- package/src/extractor/parsers/jsx-handler.ts +0 -488
- package/src/extractor/parsers/jsx-parser.ts +0 -1463
- package/src/extractor/parsers/scope-manager.ts +0 -445
- package/src/extractor/plugin-manager.ts +0 -116
- package/src/extractor.ts +0 -15
- package/src/heuristic-config.ts +0 -92
- package/src/index.ts +0 -22
- package/src/init.ts +0 -175
- package/src/linter.ts +0 -345
- package/src/locize.ts +0 -263
- package/src/migrator.ts +0 -208
- package/src/rename-key.ts +0 -398
- package/src/status.ts +0 -380
- package/src/syncer.ts +0 -133
- package/src/types-generator.ts +0 -139
- package/src/types.ts +0 -577
- package/src/utils/default-value.ts +0 -45
- package/src/utils/file-utils.ts +0 -167
- package/src/utils/funnel-msg-tracker.ts +0 -84
- package/src/utils/logger.ts +0 -36
- package/src/utils/nested-object.ts +0 -135
- package/src/utils/validation.ts +0 -72
|
@@ -1,750 +0,0 @@
|
|
|
1
|
-
import { TranslationResult, ExtractedKey, I18nextToolkitConfig } from '../../types'
|
|
2
|
-
import { resolve, basename, extname } from 'node:path'
|
|
3
|
-
import { glob } from 'glob'
|
|
4
|
-
import { getNestedValue, setNestedValue, getNestedKeys } from '../../utils/nested-object'
|
|
5
|
-
import { getOutputPath, loadTranslationFile } from '../../utils/file-utils'
|
|
6
|
-
import { resolveDefaultValue } from '../../utils/default-value'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Converts a glob pattern to a regular expression for matching keys
|
|
10
|
-
* @param glob - The glob pattern to convert
|
|
11
|
-
* @returns A RegExp object that matches the glob pattern
|
|
12
|
-
*/
|
|
13
|
-
function globToRegex (glob: string): RegExp {
|
|
14
|
-
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
15
|
-
const regexString = `^${escaped.replace(/\*/g, '.*')}$`
|
|
16
|
-
return new RegExp(regexString)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Checks if an existing key is a context variant of a base key that accepts context.
|
|
21
|
-
* This function handles complex cases where:
|
|
22
|
-
* - The key might have plural suffixes (_one, _other, etc.)
|
|
23
|
-
* - The context value itself might contain the separator (e.g., mc_laren)
|
|
24
|
-
*
|
|
25
|
-
* @param existingKey - The key from the translation file to check
|
|
26
|
-
* @param keysAcceptingContext - Set of base keys that were used with context in source code
|
|
27
|
-
* @param pluralSeparator - The separator used for plural forms (default: '_')
|
|
28
|
-
* @param contextSeparator - The separator used for context variants (default: '_')
|
|
29
|
-
* @returns true if the existing key is a context variant of a key accepting context
|
|
30
|
-
*/
|
|
31
|
-
function isContextVariantOfAcceptingKey (
|
|
32
|
-
existingKey: string,
|
|
33
|
-
keysAcceptingContext: ReadonlySet<string>,
|
|
34
|
-
pluralSeparator: string,
|
|
35
|
-
contextSeparator: string
|
|
36
|
-
): boolean {
|
|
37
|
-
if (keysAcceptingContext.size === 0) {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Try to extract the base key from this existing key by removing context and/or plural suffixes
|
|
42
|
-
let potentialBaseKey = existingKey
|
|
43
|
-
|
|
44
|
-
// First, try removing plural suffixes if present
|
|
45
|
-
const pluralForms = ['zero', 'one', 'two', 'few', 'many', 'other']
|
|
46
|
-
for (const form of pluralForms) {
|
|
47
|
-
if (potentialBaseKey.endsWith(`${pluralSeparator}${form}`)) {
|
|
48
|
-
potentialBaseKey = potentialBaseKey.slice(0, -(pluralSeparator.length + form.length))
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
if (potentialBaseKey.endsWith(`${pluralSeparator}ordinal${pluralSeparator}${form}`)) {
|
|
52
|
-
potentialBaseKey = potentialBaseKey.slice(0, -(pluralSeparator.length + 'ordinal'.length + pluralSeparator.length + form.length))
|
|
53
|
-
break
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Then, try removing the context suffix to get the base key
|
|
58
|
-
// We need to check all possible base keys since the context value itself might contain separators
|
|
59
|
-
// For example: 'formula_one_mc_laren' could be:
|
|
60
|
-
// - base: 'formula_one_mc', context: 'laren'
|
|
61
|
-
// - base: 'formula_one', context: 'mc_laren' ← correct
|
|
62
|
-
// - base: 'formula', context: 'one_mc_laren'
|
|
63
|
-
const parts = potentialBaseKey.split(contextSeparator)
|
|
64
|
-
if (parts.length > 1) {
|
|
65
|
-
// Try removing 1, 2, 3... parts from the end to find a matching base key
|
|
66
|
-
for (let i = 1; i < parts.length; i++) {
|
|
67
|
-
const baseWithoutContext = parts.slice(0, -i).join(contextSeparator)
|
|
68
|
-
if (keysAcceptingContext.has(baseWithoutContext)) {
|
|
69
|
-
return true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Also check if the key itself (after removing plural suffix) accepts context
|
|
75
|
-
// This handles cases like 'friend_other' where 'friend' accepts context
|
|
76
|
-
if (keysAcceptingContext.has(potentialBaseKey)) {
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Recursively sorts the keys of an object.
|
|
85
|
-
*/
|
|
86
|
-
function sortObject (obj: any, config?: I18nextToolkitConfig, customSort?: (a: string, b: string) => number): any {
|
|
87
|
-
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
88
|
-
return obj
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const sortedObj: Record<string, any> = {}
|
|
92
|
-
const pluralSeparator = config?.extract?.pluralSeparator ?? '_'
|
|
93
|
-
|
|
94
|
-
// Define the canonical order for plural forms
|
|
95
|
-
const pluralOrder = ['zero', 'one', 'two', 'few', 'many', 'other']
|
|
96
|
-
const ordinalPluralOrder = pluralOrder.map(form => `ordinal${pluralSeparator}${form}`)
|
|
97
|
-
|
|
98
|
-
const keys = Object.keys(obj).sort((a, b) => {
|
|
99
|
-
// Helper function to extract base key and form info
|
|
100
|
-
const getKeyInfo = (key: string) => {
|
|
101
|
-
// Handle ordinal plurals: key_ordinal_form or key_context_ordinal_form
|
|
102
|
-
for (const form of ordinalPluralOrder) {
|
|
103
|
-
if (key.endsWith(`${pluralSeparator}${form}`)) {
|
|
104
|
-
const base = key.slice(0, -(pluralSeparator.length + form.length))
|
|
105
|
-
return { base, form, isOrdinal: true, isPlural: true, fullKey: key }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Handle cardinal plurals: key_form or key_context_form
|
|
109
|
-
for (const form of pluralOrder) {
|
|
110
|
-
if (key.endsWith(`${pluralSeparator}${form}`)) {
|
|
111
|
-
const base = key.slice(0, -(pluralSeparator.length + form.length))
|
|
112
|
-
return { base, form, isOrdinal: false, isPlural: true, fullKey: key }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return { base: key, form: '', isOrdinal: false, isPlural: false, fullKey: key }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const aInfo = getKeyInfo(a)
|
|
119
|
-
const bInfo = getKeyInfo(b)
|
|
120
|
-
|
|
121
|
-
// If both are plural forms
|
|
122
|
-
if (aInfo.isPlural && bInfo.isPlural) {
|
|
123
|
-
// First compare by base key
|
|
124
|
-
const baseComparison = customSort
|
|
125
|
-
? customSort(aInfo.base, bInfo.base)
|
|
126
|
-
: aInfo.base.localeCompare(bInfo.base, undefined, { sensitivity: 'base' })
|
|
127
|
-
if (baseComparison !== 0) {
|
|
128
|
-
return baseComparison
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Same base key - now sort by plural form order
|
|
132
|
-
// Ordinal forms come after cardinal forms
|
|
133
|
-
if (aInfo.isOrdinal !== bInfo.isOrdinal) {
|
|
134
|
-
return aInfo.isOrdinal ? 1 : -1
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Both same type (cardinal or ordinal), sort by canonical order
|
|
138
|
-
const orderArray = aInfo.isOrdinal ? ordinalPluralOrder : pluralOrder
|
|
139
|
-
const aIndex = orderArray.indexOf(aInfo.form)
|
|
140
|
-
const bIndex = orderArray.indexOf(bInfo.form)
|
|
141
|
-
|
|
142
|
-
if (aIndex !== -1 && bIndex !== -1) {
|
|
143
|
-
return aIndex - bIndex
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Fallback to alphabetical if forms not found in order array
|
|
147
|
-
return aInfo.form.localeCompare(bInfo.form)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Use custom sort if provided, otherwise default sorting
|
|
151
|
-
if (customSort) {
|
|
152
|
-
return customSort(a, b)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Default: case-insensitive, then by case
|
|
156
|
-
const caseInsensitiveComparison = a.localeCompare(b, undefined, { sensitivity: 'base' })
|
|
157
|
-
if (caseInsensitiveComparison === 0) {
|
|
158
|
-
return a.localeCompare(b, undefined, { sensitivity: 'case' })
|
|
159
|
-
}
|
|
160
|
-
return caseInsensitiveComparison
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
for (const key of keys) {
|
|
164
|
-
sortedObj[key] = sortObject(obj[key], config, customSort)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return sortedObj
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* A helper function to build a new translation object for a single namespace.
|
|
172
|
-
* This centralizes the core logic of merging keys.
|
|
173
|
-
*/
|
|
174
|
-
function buildNewTranslationsForNs (
|
|
175
|
-
nsKeys: ExtractedKey[],
|
|
176
|
-
existingTranslations: Record<string, any>,
|
|
177
|
-
config: I18nextToolkitConfig,
|
|
178
|
-
locale: string,
|
|
179
|
-
namespace?: string,
|
|
180
|
-
preservePatterns: RegExp[] = [],
|
|
181
|
-
objectKeys: Set<string> = new Set(),
|
|
182
|
-
syncPrimaryWithDefaults: boolean = false
|
|
183
|
-
): Record<string, any> {
|
|
184
|
-
const {
|
|
185
|
-
keySeparator = '.',
|
|
186
|
-
sort = true,
|
|
187
|
-
removeUnusedKeys = true,
|
|
188
|
-
primaryLanguage,
|
|
189
|
-
defaultValue: emptyDefaultValue = '',
|
|
190
|
-
pluralSeparator = '_',
|
|
191
|
-
contextSeparator = '_',
|
|
192
|
-
preserveContextVariants = false,
|
|
193
|
-
} = config.extract
|
|
194
|
-
|
|
195
|
-
// Build a set of base keys that accept context (only if preserveContextVariants is enabled)
|
|
196
|
-
// These are keys that were called with a context parameter in the source code
|
|
197
|
-
const keysAcceptingContext = new Set<string>()
|
|
198
|
-
if (preserveContextVariants) {
|
|
199
|
-
for (const { keyAcceptingContext } of nsKeys) {
|
|
200
|
-
if (keyAcceptingContext) {
|
|
201
|
-
keysAcceptingContext.add(keyAcceptingContext)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Get the plural categories for the target language
|
|
207
|
-
const targetLanguagePluralCategories = new Set<string>()
|
|
208
|
-
// Track cardinal plural categories separately so we can special-case single-"other" languages
|
|
209
|
-
let cardinalCategories: string[] = []
|
|
210
|
-
let ordinalCategories: string[] = []
|
|
211
|
-
try {
|
|
212
|
-
const cardinalRules = new Intl.PluralRules(locale, { type: 'cardinal' })
|
|
213
|
-
const ordinalRules = new Intl.PluralRules(locale, { type: 'ordinal' })
|
|
214
|
-
|
|
215
|
-
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories
|
|
216
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories
|
|
217
|
-
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat))
|
|
218
|
-
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`))
|
|
219
|
-
} catch (e) {
|
|
220
|
-
// Fallback to primaryLanguage (or English) if locale is invalid
|
|
221
|
-
const fallbackLang = primaryLanguage || 'en'
|
|
222
|
-
const cardinalRules = new Intl.PluralRules(fallbackLang, { type: 'cardinal' })
|
|
223
|
-
const ordinalRules = new Intl.PluralRules(fallbackLang, { type: 'ordinal' })
|
|
224
|
-
|
|
225
|
-
cardinalCategories = cardinalRules.resolvedOptions().pluralCategories
|
|
226
|
-
ordinalCategories = ordinalRules.resolvedOptions().pluralCategories
|
|
227
|
-
cardinalCategories.forEach(cat => targetLanguagePluralCategories.add(cat))
|
|
228
|
-
ordinalRules.resolvedOptions().pluralCategories.forEach(cat => targetLanguagePluralCategories.add(`ordinal_${cat}`))
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Prepare namespace pattern checking helpers
|
|
232
|
-
const rawPreserve = config.extract.preservePatterns || []
|
|
233
|
-
const nsSep = typeof config.extract.nsSeparator === 'string' ? config.extract.nsSeparator : ':'
|
|
234
|
-
|
|
235
|
-
// Helper to check if a key should be filtered out during extraction
|
|
236
|
-
const shouldFilterKey = (key: string): boolean => {
|
|
237
|
-
// 1) regex based patterns (existing behavior)
|
|
238
|
-
if (preservePatterns.some(re => re.test(key))) {
|
|
239
|
-
return true
|
|
240
|
-
}
|
|
241
|
-
// 2) namespace:* style patterns (respect nsSeparator)
|
|
242
|
-
for (const rp of rawPreserve) {
|
|
243
|
-
if (typeof rp !== 'string') continue
|
|
244
|
-
if (rp.endsWith(`${nsSep}*`)) {
|
|
245
|
-
const nsPrefix = rp.slice(0, -(nsSep.length + 1))
|
|
246
|
-
// If namespace is provided to this builder, and pattern targets this namespace, skip keys from this ns
|
|
247
|
-
// Support wildcard namespace '*' to match any namespace
|
|
248
|
-
if (nsPrefix === '*' || (namespace && nsPrefix === namespace)) {
|
|
249
|
-
return true
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return false
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Helper to check if an existing key should be preserved
|
|
257
|
-
const shouldPreserveExistingKey = (key: string): boolean => {
|
|
258
|
-
// 1) regex-style patterns
|
|
259
|
-
if (preservePatterns.some(re => re.test(key))) {
|
|
260
|
-
return true
|
|
261
|
-
}
|
|
262
|
-
// 2) namespace:key patterns - check if pattern matches this namespace:key combination
|
|
263
|
-
for (const rp of rawPreserve) {
|
|
264
|
-
if (typeof rp !== 'string') continue
|
|
265
|
-
|
|
266
|
-
// Handle namespace:* patterns
|
|
267
|
-
if (rp.endsWith(`${nsSep}*`)) {
|
|
268
|
-
const nsPrefix = rp.slice(0, -(nsSep.length + 1))
|
|
269
|
-
if (nsPrefix === '*' || (namespace && nsPrefix === namespace)) {
|
|
270
|
-
return true
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Handle namespace:specificKey patterns (e.g., 'other:okey', 'other:second*')
|
|
275
|
-
if (rp.includes(nsSep) && namespace) {
|
|
276
|
-
const [patternNs, patternKey] = rp.split(nsSep)
|
|
277
|
-
if (patternNs === namespace) {
|
|
278
|
-
// Convert the key part to regex (handle wildcards)
|
|
279
|
-
const keyRegex = globToRegex(patternKey)
|
|
280
|
-
if (keyRegex.test(key)) {
|
|
281
|
-
return true
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return false
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Filter nsKeys to only include keys relevant to this language
|
|
290
|
-
const filteredKeys = nsKeys.filter(({ key, hasCount, isOrdinal }) => {
|
|
291
|
-
// FIRST: Check if key matches preservePatterns and should be excluded
|
|
292
|
-
if (shouldFilterKey(key)) {
|
|
293
|
-
return false
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (!hasCount) {
|
|
297
|
-
// Non-plural keys are always included
|
|
298
|
-
return true
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// For plural keys, check if this specific plural form is needed for the target language
|
|
302
|
-
const keyParts = key.split(pluralSeparator)
|
|
303
|
-
|
|
304
|
-
// If this is a base plural key (no plural suffix), keep it so that the
|
|
305
|
-
// builder can expand it to the target locale's plural forms.
|
|
306
|
-
if (hasCount && keyParts.length === 1) {
|
|
307
|
-
return true
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Special-case single-cardinal-"other" languages (ja/zh/ko etc.):
|
|
311
|
-
// when the target language's cardinal categories are exactly ['other'],
|
|
312
|
-
// the extractor may have emitted the base key (no "_other" suffix).
|
|
313
|
-
// Accept the base key in that situation, while still accepting explicit *_other variants.
|
|
314
|
-
if (cardinalCategories.length === 1 && cardinalCategories[0] === 'other') {
|
|
315
|
-
// If this is a plain/base key (no plural suffix), include it.
|
|
316
|
-
if (keyParts.length === 1) return true
|
|
317
|
-
// Otherwise fall through and check the explicit suffix as before.
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (isOrdinal && keyParts.includes('ordinal')) {
|
|
321
|
-
// For ordinal plurals: key_context_ordinal_category or key_ordinal_category
|
|
322
|
-
const lastPart = keyParts[keyParts.length - 1]
|
|
323
|
-
return targetLanguagePluralCategories.has(`ordinal_${lastPart}`)
|
|
324
|
-
} else if (hasCount) {
|
|
325
|
-
// For cardinal plurals: key_context_category or key_category
|
|
326
|
-
const lastPart = keyParts[keyParts.length - 1]
|
|
327
|
-
return targetLanguagePluralCategories.has(lastPart)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return true
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
// NEW: detect bases that already have expanded plural variants extracted.
|
|
334
|
-
// If a base has explicit expanded variants (e.g. key_one, key_other or key_ordinal_one),
|
|
335
|
-
// we should avoid generating/expanding the base plural key for that base to prevent
|
|
336
|
-
// double-generation / duplicate counting.
|
|
337
|
-
const expandedBases = new Set<string>()
|
|
338
|
-
for (const ek of filteredKeys) {
|
|
339
|
-
if (ek.isExpandedPlural) {
|
|
340
|
-
const parts = String(ek.key).split(pluralSeparator)
|
|
341
|
-
// If ordinal form like "key_ordinal_one" -> base should strip "_ordinal_<cat>"
|
|
342
|
-
if (parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
|
|
343
|
-
expandedBases.add(parts.slice(0, -2).join(pluralSeparator))
|
|
344
|
-
} else {
|
|
345
|
-
// strip single trailing category
|
|
346
|
-
expandedBases.add(parts.slice(0, -1).join(pluralSeparator))
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// If `removeUnusedKeys` is true, start with an empty object. Otherwise, start with a clone of the existing translations.
|
|
352
|
-
let newTranslations: Record<string, any> = removeUnusedKeys
|
|
353
|
-
? {}
|
|
354
|
-
: JSON.parse(JSON.stringify(existingTranslations))
|
|
355
|
-
|
|
356
|
-
// Preserve keys that match the configured patterns OR are context variants of keys accepting context
|
|
357
|
-
const existingKeys = getNestedKeys(existingTranslations, keySeparator ?? '.')
|
|
358
|
-
for (const existingKey of existingKeys) {
|
|
359
|
-
const shouldPreserve = shouldPreserveExistingKey(existingKey)
|
|
360
|
-
const isContextVariant = !shouldPreserve && isContextVariantOfAcceptingKey(
|
|
361
|
-
existingKey,
|
|
362
|
-
keysAcceptingContext,
|
|
363
|
-
pluralSeparator,
|
|
364
|
-
contextSeparator
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
if (shouldPreserve || (preserveContextVariants && isContextVariant)) {
|
|
368
|
-
const value = getNestedValue(existingTranslations, existingKey, keySeparator ?? '.')
|
|
369
|
-
setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.')
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// SPECIAL HANDLING: Preserve existing _zero forms even if not in extracted keys
|
|
374
|
-
// This ensures that optional _zero forms are not removed when they exist
|
|
375
|
-
if (removeUnusedKeys) {
|
|
376
|
-
const existingKeys = getNestedKeys(existingTranslations, keySeparator ?? '.')
|
|
377
|
-
for (const existingKey of existingKeys) {
|
|
378
|
-
// Check if this is a _zero form that should be preserved
|
|
379
|
-
const keyParts = existingKey.split(pluralSeparator)
|
|
380
|
-
const lastPart = keyParts[keyParts.length - 1]
|
|
381
|
-
|
|
382
|
-
if (lastPart === 'zero') {
|
|
383
|
-
// Check if the base plural key exists in our extracted keys
|
|
384
|
-
const baseKey = keyParts.slice(0, -1).join(pluralSeparator)
|
|
385
|
-
const hasBaseInExtracted = filteredKeys.some(({ key }) => {
|
|
386
|
-
const extractedParts = key.split(pluralSeparator)
|
|
387
|
-
const extractedBase = extractedParts.slice(0, -1).join(pluralSeparator)
|
|
388
|
-
return extractedBase === baseKey
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
if (hasBaseInExtracted) {
|
|
392
|
-
// Preserve the existing _zero form
|
|
393
|
-
const value = getNestedValue(existingTranslations, existingKey, keySeparator ?? '.')
|
|
394
|
-
setNestedValue(newTranslations, existingKey, value, keySeparator ?? '.')
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 1. Build the object first, without any sorting.
|
|
401
|
-
for (const { key, defaultValue, explicitDefault, hasCount, isExpandedPlural, isOrdinal } of filteredKeys) {
|
|
402
|
-
// If this is a base plural key (hasCount true but not an already-expanded variant)
|
|
403
|
-
// and we detected explicit expanded variants for this base, skip expanding the base.
|
|
404
|
-
if (hasCount && !isExpandedPlural) {
|
|
405
|
-
const parts = String(key).split(pluralSeparator)
|
|
406
|
-
let base = key
|
|
407
|
-
if (parts.length >= 3 && parts[parts.length - 2] === 'ordinal') {
|
|
408
|
-
base = parts.slice(0, -2).join(pluralSeparator)
|
|
409
|
-
} else if (parts.length >= 2) {
|
|
410
|
-
base = parts.slice(0, -1).join(pluralSeparator)
|
|
411
|
-
}
|
|
412
|
-
if (expandedBases.has(base)) {
|
|
413
|
-
// Skip generating/expanding this base key because explicit expanded forms exist.
|
|
414
|
-
continue
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// If this is a base plural key (no explicit suffix) and the locale is NOT the primary,
|
|
419
|
-
// expand it into locale-specific plural variants (e.g. key_one, key_other).
|
|
420
|
-
// Use the extracted defaultValue (fallback to base) for variant values.
|
|
421
|
-
if (hasCount && !isExpandedPlural) {
|
|
422
|
-
const parts = String(key).split(pluralSeparator)
|
|
423
|
-
const isBaseKey = parts.length === 1
|
|
424
|
-
if (isBaseKey && locale !== primaryLanguage) {
|
|
425
|
-
// If explicit expanded variants exist, do not expand the base.
|
|
426
|
-
const base = key
|
|
427
|
-
if (expandedBases.has(base)) {
|
|
428
|
-
// Skip expansion when explicit variants were provided
|
|
429
|
-
} else {
|
|
430
|
-
// choose categories based on ordinal flag
|
|
431
|
-
const categories = isOrdinal ? ordinalCategories : cardinalCategories
|
|
432
|
-
for (const category of categories) {
|
|
433
|
-
const finalKey = isOrdinal
|
|
434
|
-
? `${base}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
435
|
-
: `${base}${pluralSeparator}${category}`
|
|
436
|
-
|
|
437
|
-
// Preserve existing translation if present; otherwise set a sensible default
|
|
438
|
-
const existingVariantValue = getNestedValue(existingTranslations, finalKey, keySeparator ?? '.')
|
|
439
|
-
if (existingVariantValue === undefined) {
|
|
440
|
-
// Prefer explicit defaultValue extracted for this key; fall back to configured defaultValue
|
|
441
|
-
// (resolved via resolveDefaultValue which handles functions or strings and accepts the full parameter set).
|
|
442
|
-
let resolvedValue: string
|
|
443
|
-
if (typeof defaultValue === 'string') {
|
|
444
|
-
resolvedValue = defaultValue
|
|
445
|
-
} else {
|
|
446
|
-
// Use resolveDefaultValue to compute a sensible default, providing namespace and locale context.
|
|
447
|
-
resolvedValue = resolveDefaultValue(emptyDefaultValue, String(base), namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue)
|
|
448
|
-
}
|
|
449
|
-
setNestedValue(newTranslations, finalKey, resolvedValue, keySeparator ?? '.')
|
|
450
|
-
} else {
|
|
451
|
-
// Keep existing translation
|
|
452
|
-
setNestedValue(newTranslations, finalKey, existingVariantValue, keySeparator ?? '.')
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// We've expanded variants for this base key; skip the normal single-key handling.
|
|
457
|
-
continue
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const existingValue = getNestedValue(existingTranslations, key, keySeparator ?? '.')
|
|
462
|
-
// When keySeparator === false we are working with flat keys (no nesting).
|
|
463
|
-
// Avoid concatenating false into strings (``${key}${false}`` => "keyfalse") which breaks the startsWith check.
|
|
464
|
-
// For flat keys there cannot be nested children, so treat them as leaves.
|
|
465
|
-
const isLeafInNewKeys = keySeparator === false
|
|
466
|
-
? true
|
|
467
|
-
: !filteredKeys.some(otherKey => otherKey.key !== key && otherKey.key.startsWith(`${key}${keySeparator}`))
|
|
468
|
-
|
|
469
|
-
// Determine if we should preserve an existing object
|
|
470
|
-
const shouldPreserveObject = typeof existingValue === 'object' && existingValue !== null && (
|
|
471
|
-
objectKeys.has(key) || // Explicit returnObjects
|
|
472
|
-
!defaultValue || defaultValue === key // No explicit default or default equals key
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
const isStaleObject = typeof existingValue === 'object' && existingValue !== null && isLeafInNewKeys && !objectKeys.has(key) && !shouldPreserveObject
|
|
476
|
-
|
|
477
|
-
// Special handling for existing objects that should be preserved
|
|
478
|
-
if (shouldPreserveObject) {
|
|
479
|
-
setNestedValue(newTranslations, key, existingValue, keySeparator ?? '.')
|
|
480
|
-
continue
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
let valueToSet: string
|
|
484
|
-
|
|
485
|
-
if (existingValue === undefined || isStaleObject) {
|
|
486
|
-
// New key or stale object - determine what value to use
|
|
487
|
-
if (locale === primaryLanguage) {
|
|
488
|
-
if (syncPrimaryWithDefaults) {
|
|
489
|
-
// When syncPrimaryWithDefaults is true:
|
|
490
|
-
// - Use defaultValue if it exists and is meaningful (not derived from key pattern)
|
|
491
|
-
// - Otherwise use empty string for new keys
|
|
492
|
-
const isDerivedDefault = defaultValue && (
|
|
493
|
-
defaultValue === key || // Exact match with the key itself
|
|
494
|
-
// Check if defaultValue matches the namespaced key format (namespace:key)
|
|
495
|
-
(nsSep && namespace && defaultValue === `${namespace}${nsSep}${key}`) ||
|
|
496
|
-
// For variant keys (plural/context), check if defaultValue is the base
|
|
497
|
-
(key !== defaultValue &&
|
|
498
|
-
(key.startsWith(defaultValue + pluralSeparator) ||
|
|
499
|
-
key.startsWith(defaultValue + contextSeparator)))
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
valueToSet = (defaultValue && !isDerivedDefault) ? defaultValue : resolveDefaultValue(emptyDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue)
|
|
503
|
-
} else {
|
|
504
|
-
// syncPrimaryWithDefaults is false - use original behavior
|
|
505
|
-
valueToSet = defaultValue || key
|
|
506
|
-
}
|
|
507
|
-
} else {
|
|
508
|
-
// For secondary languages, always use empty string
|
|
509
|
-
valueToSet = resolveDefaultValue(emptyDefaultValue, key, namespace || config?.extract?.defaultNS || 'translation', locale, defaultValue)
|
|
510
|
-
}
|
|
511
|
-
} else {
|
|
512
|
-
// Existing value exists - decide whether to preserve or sync
|
|
513
|
-
if (locale === primaryLanguage && syncPrimaryWithDefaults) {
|
|
514
|
-
// Only update when we have a meaningful defaultValue that's not derived from the key pattern.
|
|
515
|
-
const isDerivedDefault = defaultValue && (
|
|
516
|
-
defaultValue === key || // Exact match with the key itself
|
|
517
|
-
// Check if defaultValue matches the namespaced key format (namespace:key)
|
|
518
|
-
(nsSep && namespace && defaultValue === `${namespace}${nsSep}${key}`) ||
|
|
519
|
-
// For variant keys (plural/context), check if defaultValue is the base
|
|
520
|
-
(key !== defaultValue &&
|
|
521
|
-
(key.startsWith(defaultValue + pluralSeparator) ||
|
|
522
|
-
key.startsWith(defaultValue + contextSeparator)))
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
// If this key looks like a plural/context variant and the default
|
|
526
|
-
// wasn't explicitly provided in source code, preserve the existing value.
|
|
527
|
-
const isVariantKey = key.includes(pluralSeparator) || key.includes(contextSeparator)
|
|
528
|
-
if (isVariantKey && !explicitDefault) {
|
|
529
|
-
valueToSet = existingValue
|
|
530
|
-
} else if (defaultValue && !isDerivedDefault) {
|
|
531
|
-
// Otherwise, if we have a meaningful (non-derived) default, apply it.
|
|
532
|
-
valueToSet = defaultValue
|
|
533
|
-
} else {
|
|
534
|
-
// Fallback: preserve existing translation.
|
|
535
|
-
valueToSet = existingValue
|
|
536
|
-
}
|
|
537
|
-
} else {
|
|
538
|
-
// Not primary language or not syncing - always preserve existing
|
|
539
|
-
valueToSet = existingValue
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
setNestedValue(newTranslations, key, valueToSet, keySeparator ?? '.')
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// 2. If sorting is enabled, recursively sort the entire object.
|
|
547
|
-
// This correctly handles both top-level and nested keys.
|
|
548
|
-
if (sort === true) {
|
|
549
|
-
return sortObject(newTranslations, config)
|
|
550
|
-
}
|
|
551
|
-
// Custom sort function logic remains as a future enhancement if needed,
|
|
552
|
-
// but for now, this robustly handles the most common `sort: true` case.
|
|
553
|
-
if (typeof sort === 'function') {
|
|
554
|
-
const sortedObject: Record<string, any> = {}
|
|
555
|
-
const topLevelKeys = Object.keys(newTranslations)
|
|
556
|
-
|
|
557
|
-
// Create a map from key string to ExtractedKey for lookup
|
|
558
|
-
const keyMap = new Map<string, ExtractedKey>()
|
|
559
|
-
for (const extractedKey of nsKeys) {
|
|
560
|
-
// Store the full key path
|
|
561
|
-
keyMap.set(String(extractedKey.key), extractedKey)
|
|
562
|
-
|
|
563
|
-
// For nested keys, also store the top-level part
|
|
564
|
-
if (keySeparator) {
|
|
565
|
-
const topLevelKey = String(extractedKey.key).split(keySeparator)[0]
|
|
566
|
-
if (!keyMap.has(topLevelKey)) {
|
|
567
|
-
keyMap.set(topLevelKey, extractedKey)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Create a string comparator that applies the same logic as the custom sort function
|
|
573
|
-
// by extracting the actual comparison behavior
|
|
574
|
-
const stringSort = (a: string, b: string) => {
|
|
575
|
-
// Try to find ExtractedKey objects to use the custom comparator
|
|
576
|
-
const keyA = keyMap.get(a)
|
|
577
|
-
const keyB = keyMap.get(b)
|
|
578
|
-
|
|
579
|
-
if (keyA && keyB) {
|
|
580
|
-
return sort(keyA, keyB)
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// If we don't have ExtractedKey objects, we need to apply the same sorting logic
|
|
584
|
-
// Create mock ExtractedKey objects with just the key property
|
|
585
|
-
const mockKeyA = { key: a } as ExtractedKey
|
|
586
|
-
const mockKeyB = { key: b } as ExtractedKey
|
|
587
|
-
|
|
588
|
-
return sort(mockKeyA, mockKeyB)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Sort top-level keys
|
|
592
|
-
topLevelKeys.sort(stringSort)
|
|
593
|
-
|
|
594
|
-
// Pass the same string comparator to sortObject for nested keys
|
|
595
|
-
for (const key of topLevelKeys) {
|
|
596
|
-
sortedObject[key] = sortObject(newTranslations[key], config, stringSort)
|
|
597
|
-
}
|
|
598
|
-
newTranslations = sortedObject
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return newTranslations
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Processes extracted translation keys and generates translation files for all configured locales.
|
|
606
|
-
*
|
|
607
|
-
* This function:
|
|
608
|
-
* 1. Groups keys by namespace
|
|
609
|
-
* 2. For each locale and namespace combination:
|
|
610
|
-
* - Reads existing translation files
|
|
611
|
-
* - Preserves keys matching `preservePatterns` and those from `objectKeys`
|
|
612
|
-
* - Merges in newly extracted keys
|
|
613
|
-
* - Uses primary language defaults or empty strings for secondary languages
|
|
614
|
-
* - Maintains key sorting based on configuration
|
|
615
|
-
* 3. Determines if files need updating by comparing content
|
|
616
|
-
*
|
|
617
|
-
* @param keys - Map of extracted translation keys with metadata.
|
|
618
|
-
* @param objectKeys - A set of base keys that were called with the `returnObjects: true` option.
|
|
619
|
-
* @param config - The i18next toolkit configuration object.
|
|
620
|
-
* @returns Promise resolving to array of translation results with update status.
|
|
621
|
-
*
|
|
622
|
-
* @example
|
|
623
|
-
* ```typescript
|
|
624
|
-
* const keys = new Map([
|
|
625
|
-
* ['translation:welcome', { key: 'welcome', defaultValue: 'Welcome!', ns: 'translation' }],
|
|
626
|
-
* ]);
|
|
627
|
-
* const objectKeys = new Set(['countries']);
|
|
628
|
-
*
|
|
629
|
-
* const results = await getTranslations(keys, objectKeys, config);
|
|
630
|
-
* // Results contain update status and new/existing translations for each locale.
|
|
631
|
-
* ```
|
|
632
|
-
*/
|
|
633
|
-
export async function getTranslations (
|
|
634
|
-
keys: Map<string, ExtractedKey>,
|
|
635
|
-
objectKeys: Set<string>,
|
|
636
|
-
config: I18nextToolkitConfig,
|
|
637
|
-
{ syncPrimaryWithDefaults = false }: { syncPrimaryWithDefaults?: boolean } = {}
|
|
638
|
-
): Promise<TranslationResult[]> {
|
|
639
|
-
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
640
|
-
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
641
|
-
const patternsToPreserve = [...(config.extract.preservePatterns || [])]
|
|
642
|
-
const indentation = config.extract.indentation ?? 2
|
|
643
|
-
|
|
644
|
-
for (const key of objectKeys) {
|
|
645
|
-
// Convert the object key to a glob pattern to preserve all its children
|
|
646
|
-
patternsToPreserve.push(`${key}.*`)
|
|
647
|
-
}
|
|
648
|
-
const preservePatterns = patternsToPreserve.map(globToRegex)
|
|
649
|
-
|
|
650
|
-
// Group keys by namespace. If the plugin recorded the namespace as implicit
|
|
651
|
-
// (nsIsImplicit) AND the user set defaultNS === false we treat those keys
|
|
652
|
-
// as "no namespace" (will be merged at top-level). Otherwise use the stored
|
|
653
|
-
// namespace (internally we keep implicit keys as 'translation').
|
|
654
|
-
const NO_NS_TOKEN = '__no_namespace__'
|
|
655
|
-
const keysByNS = new Map<string, ExtractedKey[]>()
|
|
656
|
-
for (const k of keys.values()) {
|
|
657
|
-
const nsKey = (k.nsIsImplicit && config.extract.defaultNS === false)
|
|
658
|
-
? NO_NS_TOKEN
|
|
659
|
-
: String(k.ns ?? (config.extract.defaultNS ?? 'translation'))
|
|
660
|
-
if (!keysByNS.has(nsKey)) keysByNS.set(nsKey, [])
|
|
661
|
-
keysByNS.get(nsKey)!.push(k)
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const results: TranslationResult[] = []
|
|
665
|
-
const userIgnore = Array.isArray(config.extract.ignore)
|
|
666
|
-
? config.extract.ignore
|
|
667
|
-
: config.extract.ignore ? [config.extract.ignore] : []
|
|
668
|
-
|
|
669
|
-
// Process each locale one by one
|
|
670
|
-
for (const locale of config.locales) {
|
|
671
|
-
// If output is a string we can detect the presence of the namespace placeholder.
|
|
672
|
-
// If it's a function we cannot reliably detect that here — default to not merged
|
|
673
|
-
// unless mergeNamespaces is explicitly true.
|
|
674
|
-
const shouldMerge = config.extract.mergeNamespaces || (typeof config.extract.output === 'string' ? !config.extract.output.includes('{{namespace}}') : false)
|
|
675
|
-
|
|
676
|
-
// LOGIC PATH 1: Merged Namespaces
|
|
677
|
-
if (shouldMerge) {
|
|
678
|
-
const newMergedTranslations: Record<string, any> = {}
|
|
679
|
-
const outputPath = getOutputPath(config.extract.output, locale)
|
|
680
|
-
const fullPath = resolve(process.cwd(), outputPath)
|
|
681
|
-
const existingMergedFile = await loadTranslationFile(fullPath) || {}
|
|
682
|
-
|
|
683
|
-
// Determine whether the existing merged file already uses namespace objects
|
|
684
|
-
// or is a flat mapping of translation keys -> values.
|
|
685
|
-
// If it's flat (values are primitives), we must NOT treat each translation key as a namespace.
|
|
686
|
-
const existingKeys = Object.keys(existingMergedFile)
|
|
687
|
-
// Treat the file as namespaced only when the user is using namespaces.
|
|
688
|
-
// If defaultNS === false the project stores translations at the top-level
|
|
689
|
-
// (possibly as nested objects when keySeparator is '.'), which should NOT
|
|
690
|
-
// be interpreted as "namespaced files". This avoids splitting a single
|
|
691
|
-
// merged translations file into artificial namespace buckets on re-extract.
|
|
692
|
-
const existingIsNamespaced = (config.extract.defaultNS !== false) && existingKeys.some(k => {
|
|
693
|
-
const v = (existingMergedFile as any)[k]
|
|
694
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
// The namespaces to process:
|
|
698
|
-
// - If existing file is namespaced, combine keysByNS with existingMergedFile namespaces.
|
|
699
|
-
// - If existing file is flat (top-level translations), ensure NO_NS_TOKEN is processed.
|
|
700
|
-
const namespacesToProcess = existingIsNamespaced
|
|
701
|
-
? new Set<string>([...keysByNS.keys(), ...existingKeys])
|
|
702
|
-
: new Set<string>([...keysByNS.keys(), NO_NS_TOKEN])
|
|
703
|
-
|
|
704
|
-
for (const nsKey of namespacesToProcess) {
|
|
705
|
-
const nsKeys = keysByNS.get(nsKey) || []
|
|
706
|
-
if (nsKey === NO_NS_TOKEN) {
|
|
707
|
-
// keys without namespace -> merged into top-level of the merged file
|
|
708
|
-
const built = buildNewTranslationsForNs(nsKeys, existingMergedFile, config, locale, undefined, preservePatterns, objectKeys, syncPrimaryWithDefaults)
|
|
709
|
-
Object.assign(newMergedTranslations, built)
|
|
710
|
-
} else {
|
|
711
|
-
const existingTranslations = existingMergedFile[nsKey] || {}
|
|
712
|
-
newMergedTranslations[nsKey] = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, nsKey, preservePatterns, objectKeys, syncPrimaryWithDefaults)
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const oldContent = JSON.stringify(existingMergedFile, null, indentation)
|
|
717
|
-
const newContent = JSON.stringify(newMergedTranslations, null, indentation)
|
|
718
|
-
// Push a single result for the merged file
|
|
719
|
-
results.push({ path: fullPath, updated: newContent !== oldContent, newTranslations: newMergedTranslations, existingTranslations: existingMergedFile })
|
|
720
|
-
|
|
721
|
-
// LOGIC PATH 2: Separate Namespace Files
|
|
722
|
-
} else {
|
|
723
|
-
// Find all namespaces that exist on disk for this locale
|
|
724
|
-
const namespacesToProcess = new Set(keysByNS.keys())
|
|
725
|
-
const existingNsPattern = getOutputPath(config.extract.output, locale, '*')
|
|
726
|
-
// Ensure glob receives POSIX-style separators so pattern matching works cross-platform (Windows -> backslashes)
|
|
727
|
-
const existingNsGlobPattern = existingNsPattern.replace(/\\/g, '/')
|
|
728
|
-
const existingNsFiles = await glob(existingNsGlobPattern, { ignore: userIgnore })
|
|
729
|
-
for (const file of existingNsFiles) {
|
|
730
|
-
namespacesToProcess.add(basename(file, extname(file)))
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Process each namespace individually and create a result for each one
|
|
734
|
-
for (const ns of namespacesToProcess) {
|
|
735
|
-
const nsKeys = keysByNS.get(ns) || []
|
|
736
|
-
const outputPath = getOutputPath(config.extract.output, locale, ns)
|
|
737
|
-
const fullPath = resolve(process.cwd(), outputPath)
|
|
738
|
-
const existingTranslations = await loadTranslationFile(fullPath) || {}
|
|
739
|
-
const newTranslations = buildNewTranslationsForNs(nsKeys, existingTranslations, config, locale, ns, preservePatterns, objectKeys, syncPrimaryWithDefaults)
|
|
740
|
-
|
|
741
|
-
const oldContent = JSON.stringify(existingTranslations, null, indentation)
|
|
742
|
-
const newContent = JSON.stringify(newTranslations, null, indentation)
|
|
743
|
-
// Push one result per namespace file
|
|
744
|
-
results.push({ path: fullPath, updated: newContent !== oldContent, newTranslations, existingTranslations })
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
return results
|
|
750
|
-
}
|