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
package/src/status.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import ora from 'ora'
|
|
3
|
-
import { resolve } from 'node:path'
|
|
4
|
-
import { findKeys } from './extractor/core/key-finder'
|
|
5
|
-
import { getNestedValue } from './utils/nested-object'
|
|
6
|
-
import type { I18nextToolkitConfig, ExtractedKey } from './types'
|
|
7
|
-
import { getOutputPath, loadTranslationFile } from './utils/file-utils'
|
|
8
|
-
import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Options for configuring the status report display.
|
|
12
|
-
*/
|
|
13
|
-
interface StatusOptions {
|
|
14
|
-
/** Locale code to display detailed information for a specific language */
|
|
15
|
-
detail?: string;
|
|
16
|
-
/** Namespace to filter the report by */
|
|
17
|
-
namespace?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Structured report containing all translation status data.
|
|
22
|
-
*/
|
|
23
|
-
interface StatusReport {
|
|
24
|
-
/** Total number of extracted keys across all namespaces */
|
|
25
|
-
totalBaseKeys: number;
|
|
26
|
-
/** Map of namespace names to their extracted keys */
|
|
27
|
-
keysByNs: Map<string, ExtractedKey[]>;
|
|
28
|
-
/** Map of locale codes to their translation status data */
|
|
29
|
-
locales: Map<string, {
|
|
30
|
-
/** Total number of extracted keys per locale */
|
|
31
|
-
totalKeys: number;
|
|
32
|
-
/** Total number of translated keys for this locale */
|
|
33
|
-
totalTranslated: number;
|
|
34
|
-
/** Map of namespace names to their translation details for this locale */
|
|
35
|
-
namespaces: Map<string, {
|
|
36
|
-
/** Total number of keys in this namespace */
|
|
37
|
-
totalKeys: number;
|
|
38
|
-
/** Number of translated keys in this namespace */
|
|
39
|
-
translatedKeys: number;
|
|
40
|
-
/** Detailed status for each key in this namespace */
|
|
41
|
-
keyDetails: Array<{ key: string; isTranslated: boolean }>;
|
|
42
|
-
}>;
|
|
43
|
-
}>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Runs a health check on the project's i18next translations and displays a status report.
|
|
48
|
-
*
|
|
49
|
-
* This command provides a high-level overview of the localization status by:
|
|
50
|
-
* 1. Extracting all keys from the source code using the core extractor.
|
|
51
|
-
* 2. Reading all existing translation files for each locale.
|
|
52
|
-
* 3. Calculating the translation completeness for each secondary language against the primary.
|
|
53
|
-
* 4. Displaying a formatted report with key counts, locales, and progress bars.
|
|
54
|
-
* 5. Serving as a value-driven funnel to introduce the locize commercial service.
|
|
55
|
-
*
|
|
56
|
-
* @param config - The i18next toolkit configuration object.
|
|
57
|
-
* @param options - Options object, may contain a `detail` property with a locale string.
|
|
58
|
-
* @throws {Error} When unable to extract keys or read translation files
|
|
59
|
-
*/
|
|
60
|
-
export async function runStatus (config: I18nextToolkitConfig, options: StatusOptions = {}) {
|
|
61
|
-
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
62
|
-
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
63
|
-
const spinner = ora('Analyzing project localization status...\n').start()
|
|
64
|
-
try {
|
|
65
|
-
const report = await generateStatusReport(config)
|
|
66
|
-
spinner.succeed('Analysis complete.')
|
|
67
|
-
await displayStatusReport(report, config, options)
|
|
68
|
-
} catch (error) {
|
|
69
|
-
spinner.fail('Failed to generate status report.')
|
|
70
|
-
console.error(error)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Gathers all translation data and compiles it into a structured report.
|
|
76
|
-
*
|
|
77
|
-
* This function:
|
|
78
|
-
* - Extracts all keys from source code using the configured extractor
|
|
79
|
-
* - Groups keys by namespace
|
|
80
|
-
* - Reads translation files for each secondary language
|
|
81
|
-
* - Compares extracted keys against existing translations
|
|
82
|
-
* - Compiles translation statistics for each locale and namespace
|
|
83
|
-
*
|
|
84
|
-
* @param config - The i18next toolkit configuration object
|
|
85
|
-
* @returns Promise that resolves to a complete status report
|
|
86
|
-
* @throws {Error} When key extraction fails or configuration is invalid
|
|
87
|
-
*/
|
|
88
|
-
async function generateStatusReport (config: I18nextToolkitConfig): Promise<StatusReport> {
|
|
89
|
-
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
90
|
-
config.extract.secondaryLanguages ||= config.locales.filter((l: string) => l !== config?.extract?.primaryLanguage)
|
|
91
|
-
|
|
92
|
-
const { allKeys: allExtractedKeys } = await findKeys(config)
|
|
93
|
-
const { secondaryLanguages, keySeparator = '.', defaultNS = 'translation', mergeNamespaces = false, pluralSeparator = '_' } = config.extract
|
|
94
|
-
|
|
95
|
-
const keysByNs = new Map<string, ExtractedKey[]>()
|
|
96
|
-
for (const key of allExtractedKeys.values()) {
|
|
97
|
-
const ns = key.ns || defaultNS || 'translation'
|
|
98
|
-
if (!keysByNs.has(ns)) keysByNs.set(ns, [])
|
|
99
|
-
keysByNs.get(ns)!.push(key)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const report: StatusReport = {
|
|
103
|
-
totalBaseKeys: allExtractedKeys.size,
|
|
104
|
-
keysByNs,
|
|
105
|
-
locales: new Map(),
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
for (const locale of secondaryLanguages) {
|
|
109
|
-
let totalTranslatedForLocale = 0
|
|
110
|
-
let totalKeysForLocale = 0
|
|
111
|
-
const namespaces = new Map<string, any>()
|
|
112
|
-
|
|
113
|
-
const mergedTranslations = mergeNamespaces
|
|
114
|
-
? await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale))) || {}
|
|
115
|
-
: null
|
|
116
|
-
|
|
117
|
-
for (const [ns, keysInNs] of keysByNs.entries()) {
|
|
118
|
-
const translationsForNs = mergeNamespaces
|
|
119
|
-
? mergedTranslations?.[ns] || {}
|
|
120
|
-
: await loadTranslationFile(resolve(process.cwd(), getOutputPath(config.extract.output, locale, ns))) || {}
|
|
121
|
-
|
|
122
|
-
let translatedInNs = 0
|
|
123
|
-
let totalInNs = 0
|
|
124
|
-
const keyDetails: Array<{ key: string; isTranslated: boolean }> = []
|
|
125
|
-
|
|
126
|
-
// Get the plural categories for THIS specific locale
|
|
127
|
-
const getLocalePluralCategories = (locale: string, isOrdinal: boolean): string[] => {
|
|
128
|
-
try {
|
|
129
|
-
const type = isOrdinal ? 'ordinal' : 'cardinal'
|
|
130
|
-
const pluralRules = new Intl.PluralRules(locale, { type })
|
|
131
|
-
return pluralRules.resolvedOptions().pluralCategories
|
|
132
|
-
} catch (e) {
|
|
133
|
-
// Fallback to English if locale is invalid
|
|
134
|
-
const fallbackRules = new Intl.PluralRules('en', { type: isOrdinal ? 'ordinal' : 'cardinal' })
|
|
135
|
-
return fallbackRules.resolvedOptions().pluralCategories
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const { key: baseKey, hasCount, isOrdinal, isExpandedPlural } of keysInNs) {
|
|
140
|
-
if (hasCount) {
|
|
141
|
-
if (isExpandedPlural) {
|
|
142
|
-
// This is an already-expanded plural variant key (e.g., key_one, key_other)
|
|
143
|
-
// Check if this specific variant is needed for the target locale
|
|
144
|
-
const keyParts = baseKey.split(pluralSeparator)
|
|
145
|
-
const lastPart = keyParts[keyParts.length - 1]
|
|
146
|
-
|
|
147
|
-
// Determine if this is an ordinal or cardinal plural
|
|
148
|
-
const isOrdinalVariant = keyParts.length >= 2 && keyParts[keyParts.length - 2] === 'ordinal'
|
|
149
|
-
const category = isOrdinalVariant ? keyParts[keyParts.length - 1] : lastPart
|
|
150
|
-
|
|
151
|
-
// Get the plural categories for this locale
|
|
152
|
-
const localePluralCategories = getLocalePluralCategories(locale, isOrdinalVariant)
|
|
153
|
-
|
|
154
|
-
// Only count this key if it's a plural form used by this locale
|
|
155
|
-
if (localePluralCategories.includes(category)) {
|
|
156
|
-
totalInNs++
|
|
157
|
-
const value = getNestedValue(translationsForNs, baseKey, keySeparator ?? '.')
|
|
158
|
-
const isTranslated = !!value
|
|
159
|
-
if (isTranslated) translatedInNs++
|
|
160
|
-
keyDetails.push({ key: baseKey, isTranslated })
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
// This is a base plural key without expanded variants
|
|
164
|
-
// Expand it according to THIS locale's plural rules
|
|
165
|
-
const localePluralCategories = getLocalePluralCategories(locale, isOrdinal || false)
|
|
166
|
-
|
|
167
|
-
for (const category of localePluralCategories) {
|
|
168
|
-
totalInNs++
|
|
169
|
-
const pluralKey = isOrdinal
|
|
170
|
-
? `${baseKey}${pluralSeparator}ordinal${pluralSeparator}${category}`
|
|
171
|
-
: `${baseKey}${pluralSeparator}${category}`
|
|
172
|
-
const value = getNestedValue(translationsForNs, pluralKey, keySeparator ?? '.')
|
|
173
|
-
const isTranslated = !!value
|
|
174
|
-
if (isTranslated) translatedInNs++
|
|
175
|
-
keyDetails.push({ key: pluralKey, isTranslated })
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// It's a simple key
|
|
180
|
-
totalInNs++
|
|
181
|
-
const value = getNestedValue(translationsForNs, baseKey, keySeparator ?? '.')
|
|
182
|
-
const isTranslated = !!value
|
|
183
|
-
if (isTranslated) translatedInNs++
|
|
184
|
-
keyDetails.push({ key: baseKey, isTranslated })
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
namespaces.set(ns, { totalKeys: totalInNs, translatedKeys: translatedInNs, keyDetails })
|
|
189
|
-
totalTranslatedForLocale += translatedInNs
|
|
190
|
-
totalKeysForLocale += totalInNs
|
|
191
|
-
}
|
|
192
|
-
report.locales.set(locale, { totalKeys: totalKeysForLocale, totalTranslated: totalTranslatedForLocale, namespaces })
|
|
193
|
-
}
|
|
194
|
-
return report
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Main display router that calls the appropriate display function based on options.
|
|
199
|
-
*
|
|
200
|
-
* Routes to one of three display modes:
|
|
201
|
-
* - Detailed locale report: Shows per-key status for a specific locale
|
|
202
|
-
* - Namespace summary: Shows translation progress for all locales in a specific namespace
|
|
203
|
-
* - Overall summary: Shows high-level statistics across all locales and namespaces
|
|
204
|
-
*
|
|
205
|
-
* @param report - The generated status report data
|
|
206
|
-
* @param config - The i18next toolkit configuration object
|
|
207
|
-
* @param options - Display options determining which report type to show
|
|
208
|
-
*/
|
|
209
|
-
async function displayStatusReport (report: StatusReport, config: I18nextToolkitConfig, options: StatusOptions) {
|
|
210
|
-
if (options.detail) {
|
|
211
|
-
await displayDetailedLocaleReport(report, config, options.detail, options.namespace)
|
|
212
|
-
} else if (options.namespace) {
|
|
213
|
-
await displayNamespaceSummaryReport(report, config, options.namespace)
|
|
214
|
-
} else {
|
|
215
|
-
await displayOverallSummaryReport(report, config)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Displays the detailed, grouped report for a single locale.
|
|
221
|
-
*
|
|
222
|
-
* Shows:
|
|
223
|
-
* - Overall progress for the locale
|
|
224
|
-
* - Progress for each namespace (or filtered namespace)
|
|
225
|
-
* - Individual key status (translated/missing) with visual indicators
|
|
226
|
-
* - Summary message with total missing translations
|
|
227
|
-
*
|
|
228
|
-
* @param report - The generated status report data
|
|
229
|
-
* @param config - The i18next toolkit configuration object
|
|
230
|
-
* @param locale - The locale code to display details for
|
|
231
|
-
* @param namespaceFilter - Optional namespace to filter the display
|
|
232
|
-
*/
|
|
233
|
-
async function displayDetailedLocaleReport (report: StatusReport, config: I18nextToolkitConfig, locale: string, namespaceFilter?: string) {
|
|
234
|
-
if (locale === config.extract.primaryLanguage) {
|
|
235
|
-
console.log(chalk.yellow(`Locale "${locale}" is the primary language. All keys are considered present.`))
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
if (!config.locales.includes(locale)) {
|
|
239
|
-
console.error(chalk.red(`Error: Locale "${locale}" is not defined in your configuration.`))
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const localeData = report.locales.get(locale)
|
|
244
|
-
|
|
245
|
-
if (!localeData) {
|
|
246
|
-
console.error(chalk.red(`Error: Locale "${locale}" is not a valid secondary language.`))
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log(chalk.bold(`\nKey Status for "${chalk.cyan(locale)}":`))
|
|
251
|
-
|
|
252
|
-
const totalKeysForLocale = Array.from(report.keysByNs.values()).flat().length
|
|
253
|
-
printProgressBar('Overall', localeData.totalTranslated, localeData.totalKeys)
|
|
254
|
-
|
|
255
|
-
const namespacesToDisplay = namespaceFilter ? [namespaceFilter] : Array.from(localeData.namespaces.keys()).sort()
|
|
256
|
-
|
|
257
|
-
for (const ns of namespacesToDisplay) {
|
|
258
|
-
const nsData = localeData.namespaces.get(ns)
|
|
259
|
-
if (!nsData) continue
|
|
260
|
-
|
|
261
|
-
console.log(chalk.cyan.bold(`\nNamespace: ${ns}`))
|
|
262
|
-
printProgressBar('Namespace Progress', nsData.translatedKeys, nsData.totalKeys)
|
|
263
|
-
|
|
264
|
-
nsData.keyDetails.forEach(({ key, isTranslated }) => {
|
|
265
|
-
const icon = isTranslated ? chalk.green('ā') : chalk.red('ā')
|
|
266
|
-
console.log(` ${icon} ${key}`)
|
|
267
|
-
})
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const missingCount = totalKeysForLocale - localeData.totalTranslated
|
|
271
|
-
if (missingCount > 0) {
|
|
272
|
-
console.log(chalk.yellow.bold(`\nSummary: Found ${missingCount} missing translations for "${locale}".`))
|
|
273
|
-
} else {
|
|
274
|
-
console.log(chalk.green.bold(`\nSummary: š All keys are translated for "${locale}".`))
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
await printLocizeFunnel()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Displays a summary report filtered by a single namespace.
|
|
282
|
-
*
|
|
283
|
-
* Shows translation progress for the specified namespace across all secondary locales,
|
|
284
|
-
* including percentage completion and translated/total key counts.
|
|
285
|
-
*
|
|
286
|
-
* @param report - The generated status report data
|
|
287
|
-
* @param config - The i18next toolkit configuration object
|
|
288
|
-
* @param namespace - The namespace to display summary for
|
|
289
|
-
*/
|
|
290
|
-
async function displayNamespaceSummaryReport (report: StatusReport, config: I18nextToolkitConfig, namespace: string) {
|
|
291
|
-
const nsData = report.keysByNs.get(namespace)
|
|
292
|
-
if (!nsData) {
|
|
293
|
-
console.error(chalk.red(`Error: Namespace "${namespace}" was not found in your source code.`))
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
console.log(chalk.cyan.bold(`\nStatus for Namespace: "${namespace}"`))
|
|
298
|
-
console.log('------------------------')
|
|
299
|
-
|
|
300
|
-
for (const [locale, localeData] of report.locales.entries()) {
|
|
301
|
-
const nsLocaleData = localeData.namespaces.get(namespace)
|
|
302
|
-
if (nsLocaleData) {
|
|
303
|
-
const percentage = nsLocaleData.totalKeys > 0 ? Math.round((nsLocaleData.translatedKeys / nsLocaleData.totalKeys) * 100) : 100
|
|
304
|
-
const bar = generateProgressBarText(percentage)
|
|
305
|
-
console.log(`- ${locale}: ${bar} ${percentage}% (${nsLocaleData.translatedKeys}/${nsLocaleData.totalKeys} keys)`)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
await printLocizeFunnel()
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Displays the default, high-level summary report for all locales.
|
|
314
|
-
*
|
|
315
|
-
* Shows:
|
|
316
|
-
* - Project overview (total keys, locales, primary language)
|
|
317
|
-
* - Translation progress for each secondary locale with progress bars
|
|
318
|
-
* - Promotional message for locize service
|
|
319
|
-
*
|
|
320
|
-
* @param report - The generated status report data
|
|
321
|
-
* @param config - The i18next toolkit configuration object
|
|
322
|
-
*/
|
|
323
|
-
async function displayOverallSummaryReport (report: StatusReport, config: I18nextToolkitConfig) {
|
|
324
|
-
const { primaryLanguage } = config.extract
|
|
325
|
-
|
|
326
|
-
console.log(chalk.cyan.bold('\ni18next Project Status'))
|
|
327
|
-
console.log('------------------------')
|
|
328
|
-
console.log(`š Keys Found: ${chalk.bold(report.totalBaseKeys)}`)
|
|
329
|
-
console.log(`š Namespaces Found: ${chalk.bold(report.keysByNs.size)}`)
|
|
330
|
-
console.log(`š Locales: ${chalk.bold(config.locales.join(', '))}`)
|
|
331
|
-
console.log(`ā
Primary Language: ${chalk.bold(primaryLanguage)}`)
|
|
332
|
-
console.log('\nTranslation Progress:')
|
|
333
|
-
|
|
334
|
-
for (const [locale, localeData] of report.locales.entries()) {
|
|
335
|
-
const percentage = localeData.totalKeys > 0 ? Math.round((localeData.totalTranslated / localeData.totalKeys) * 100) : 100
|
|
336
|
-
const bar = generateProgressBarText(percentage)
|
|
337
|
-
console.log(`- ${locale}: ${bar} ${percentage}% (${localeData.totalTranslated}/${localeData.totalKeys} keys)`)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
await printLocizeFunnel()
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Prints a formatted progress bar with label, percentage, and counts.
|
|
345
|
-
*
|
|
346
|
-
* @param label - The label to display before the progress bar
|
|
347
|
-
* @param current - The current count (translated keys)
|
|
348
|
-
* @param total - The total count (all keys)
|
|
349
|
-
*/
|
|
350
|
-
function printProgressBar (label: string, current: number, total: number) {
|
|
351
|
-
const percentage = total > 0 ? Math.round((current / total) * 100) : 100
|
|
352
|
-
const bar = generateProgressBarText(percentage)
|
|
353
|
-
console.log(`${chalk.bold(label)}: ${bar} ${percentage}% (${current}/${total})`)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Generates a visual progress bar string based on percentage completion.
|
|
358
|
-
*
|
|
359
|
-
* Creates a 20-character progress bar using filled (ā ) and empty (ā”) squares,
|
|
360
|
-
* with the filled portion colored green.
|
|
361
|
-
*
|
|
362
|
-
* @param percentage - The completion percentage (0-100)
|
|
363
|
-
* @returns A formatted progress bar string with colors
|
|
364
|
-
*/
|
|
365
|
-
function generateProgressBarText (percentage: number): string {
|
|
366
|
-
const totalBars = 20
|
|
367
|
-
const filledBars = Math.floor((percentage / 100) * totalBars)
|
|
368
|
-
const emptyBars = totalBars - filledBars
|
|
369
|
-
return `[${chalk.green(''.padStart(filledBars, 'ā '))}${''.padStart(emptyBars, 'ā”')}]`
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async function printLocizeFunnel () {
|
|
373
|
-
if (!(await shouldShowFunnel('status'))) return
|
|
374
|
-
|
|
375
|
-
console.log(chalk.yellow.bold('\n⨠Take your localization to the next level!'))
|
|
376
|
-
console.log('Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started')
|
|
377
|
-
console.log(`Run ${chalk.cyan('npx i18next-cli locize-migrate')} to get started.`)
|
|
378
|
-
|
|
379
|
-
return recordFunnelShown('status')
|
|
380
|
-
}
|
package/src/syncer.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { glob } from 'glob'
|
|
3
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { basename, dirname, resolve } from 'node:path'
|
|
5
|
-
import ora from 'ora'
|
|
6
|
-
import type { I18nextToolkitConfig } from './types'
|
|
7
|
-
import { resolveDefaultValue } from './utils/default-value'
|
|
8
|
-
import { getOutputPath, loadTranslationFile, serializeTranslationFile } from './utils/file-utils'
|
|
9
|
-
import { recordFunnelShown, shouldShowFunnel } from './utils/funnel-msg-tracker'
|
|
10
|
-
import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Synchronizes translation files across different locales by ensuring all secondary
|
|
14
|
-
* language files contain the same keys as the primary language file.
|
|
15
|
-
*
|
|
16
|
-
* This function:
|
|
17
|
-
* 1. Reads the primary language translation file
|
|
18
|
-
* 2. Extracts all translation keys from the primary file
|
|
19
|
-
* 3. For each secondary language:
|
|
20
|
-
* - Preserves existing translations
|
|
21
|
-
* - Adds missing keys with empty values or configured default
|
|
22
|
-
* - Removes keys that no longer exist in primary
|
|
23
|
-
* 4. Only writes files that have changes
|
|
24
|
-
*
|
|
25
|
-
* @param config - The i18next toolkit configuration object
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* // Configuration
|
|
30
|
-
* const config = {
|
|
31
|
-
* locales: ['en', 'de', 'fr'],
|
|
32
|
-
* extract: {
|
|
33
|
-
* output: 'locales/{{language}}/{{namespace}}.json',
|
|
34
|
-
* defaultNS: 'translation'
|
|
35
|
-
* defaultValue: '[MISSING]'
|
|
36
|
-
* }
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* await runSyncer(config)
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export async function runSyncer (config: I18nextToolkitConfig) {
|
|
43
|
-
const spinner = ora('Running i18next locale synchronizer...\n').start()
|
|
44
|
-
try {
|
|
45
|
-
const primaryLanguage = config.extract.primaryLanguage || config.locales[0] || 'en'
|
|
46
|
-
const secondaryLanguages = config.locales.filter((l) => l !== primaryLanguage)
|
|
47
|
-
const {
|
|
48
|
-
output,
|
|
49
|
-
keySeparator = '.',
|
|
50
|
-
outputFormat = 'json',
|
|
51
|
-
indentation = 2,
|
|
52
|
-
defaultValue = '',
|
|
53
|
-
} = config.extract
|
|
54
|
-
|
|
55
|
-
const logMessages: string[] = []
|
|
56
|
-
let wasAnythingSynced = false
|
|
57
|
-
|
|
58
|
-
// 1. Find all namespace files for the primary language
|
|
59
|
-
const primaryNsPattern = getOutputPath(output, primaryLanguage, '*')
|
|
60
|
-
const primaryNsFiles = await glob(primaryNsPattern)
|
|
61
|
-
|
|
62
|
-
if (primaryNsFiles.length === 0) {
|
|
63
|
-
spinner.warn(`No translation files found for primary language "${primaryLanguage}". Nothing to sync.`)
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 2. Loop through each primary namespace file
|
|
68
|
-
for (const primaryPath of primaryNsFiles) {
|
|
69
|
-
const ns = basename(primaryPath).split('.')[0]
|
|
70
|
-
const primaryTranslations = await loadTranslationFile(primaryPath)
|
|
71
|
-
|
|
72
|
-
if (!primaryTranslations) {
|
|
73
|
-
logMessages.push(` ${chalk.yellow('-')} Could not read primary file: ${primaryPath}`)
|
|
74
|
-
continue
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const primaryKeys = getNestedKeys(primaryTranslations, keySeparator ?? '.')
|
|
78
|
-
|
|
79
|
-
// 3. For each secondary language, sync the current namespace
|
|
80
|
-
for (const lang of secondaryLanguages) {
|
|
81
|
-
const secondaryPath = getOutputPath(output, lang, ns)
|
|
82
|
-
const fullSecondaryPath = resolve(process.cwd(), secondaryPath)
|
|
83
|
-
const existingSecondaryTranslations = await loadTranslationFile(fullSecondaryPath) || {}
|
|
84
|
-
const newSecondaryTranslations: Record<string, any> = {}
|
|
85
|
-
|
|
86
|
-
for (const key of primaryKeys) {
|
|
87
|
-
const primaryValue = getNestedValue(primaryTranslations, key, keySeparator ?? '.')
|
|
88
|
-
const existingValue = getNestedValue(existingSecondaryTranslations, key, keySeparator ?? '.')
|
|
89
|
-
|
|
90
|
-
// Use the resolved default value if no existing value
|
|
91
|
-
const valueToSet = existingValue ?? resolveDefaultValue(defaultValue, key, ns, lang, primaryValue)
|
|
92
|
-
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator ?? '.')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Use JSON.stringify for a reliable object comparison, regardless of format
|
|
96
|
-
const oldContent = JSON.stringify(existingSecondaryTranslations)
|
|
97
|
-
const newContent = JSON.stringify(newSecondaryTranslations)
|
|
98
|
-
|
|
99
|
-
if (newContent !== oldContent) {
|
|
100
|
-
wasAnythingSynced = true
|
|
101
|
-
const serializedContent = serializeTranslationFile(newSecondaryTranslations, outputFormat, indentation)
|
|
102
|
-
await mkdir(dirname(fullSecondaryPath), { recursive: true })
|
|
103
|
-
await writeFile(fullSecondaryPath, serializedContent)
|
|
104
|
-
logMessages.push(` ${chalk.green('ā')} Synchronized: ${secondaryPath}`)
|
|
105
|
-
} else {
|
|
106
|
-
logMessages.push(` ${chalk.gray('-')} Already in sync: ${secondaryPath}`)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
spinner.succeed(chalk.bold('Synchronization complete!'))
|
|
112
|
-
logMessages.forEach(msg => console.log(msg))
|
|
113
|
-
|
|
114
|
-
if (wasAnythingSynced) {
|
|
115
|
-
await printLocizeFunnel()
|
|
116
|
-
} else {
|
|
117
|
-
console.log(chalk.green.bold('\nā
All locales are already in sync.'))
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
spinner.fail(chalk.red('Synchronization failed.'))
|
|
121
|
-
console.error(error)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function printLocizeFunnel () {
|
|
126
|
-
if (!(await shouldShowFunnel('syncer'))) return
|
|
127
|
-
|
|
128
|
-
console.log(chalk.green.bold('\nā
Sync complete.'))
|
|
129
|
-
console.log(chalk.yellow('š Ready to collaborate with translators? Move your files to the cloud.'))
|
|
130
|
-
console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
|
|
131
|
-
|
|
132
|
-
return recordFunnelShown('syncer')
|
|
133
|
-
}
|
package/src/types-generator.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { mergeResourcesAsInterface } from 'i18next-resources-for-ts'
|
|
2
|
-
import { glob } from 'glob'
|
|
3
|
-
import ora from 'ora'
|
|
4
|
-
import chalk from 'chalk'
|
|
5
|
-
import { mkdir, readFile, writeFile, access } from 'node:fs/promises'
|
|
6
|
-
import { basename, extname, resolve, dirname, join, relative } from 'node:path'
|
|
7
|
-
import type { I18nextToolkitConfig } from './types'
|
|
8
|
-
import { getOutputPath } from './utils/file-utils'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Represents a translation resource with its namespace name and content
|
|
12
|
-
*/
|
|
13
|
-
interface Resource {
|
|
14
|
-
/** The namespace name (filename without extension) */
|
|
15
|
-
name: string;
|
|
16
|
-
/** The parsed JSON resources object */
|
|
17
|
-
resources: object;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Generates TypeScript type definitions for i18next translations.
|
|
22
|
-
*
|
|
23
|
-
* This function:
|
|
24
|
-
* 1. Reads translation files based on the input glob patterns
|
|
25
|
-
* 2. Generates TypeScript interfaces using i18next-resources-for-ts
|
|
26
|
-
* 3. Creates separate resources.d.ts and main i18next.d.ts files
|
|
27
|
-
* 4. Handles namespace detection from filenames
|
|
28
|
-
* 5. Supports type-safe selector API when enabled
|
|
29
|
-
*
|
|
30
|
-
* @param config - The i18next toolkit configuration object
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```typescript
|
|
34
|
-
* // Configuration
|
|
35
|
-
* const config = {
|
|
36
|
-
* types: {
|
|
37
|
-
* input: ['locales/en/*.json'],
|
|
38
|
-
* output: 'src/types/i18next.d.ts',
|
|
39
|
-
* enableSelector: true
|
|
40
|
-
* }
|
|
41
|
-
* }
|
|
42
|
-
*
|
|
43
|
-
* await runTypesGenerator(config)
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export async function runTypesGenerator (config: I18nextToolkitConfig) {
|
|
47
|
-
const spinner = ora('Generating TypeScript types for translations...\n').start()
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
51
|
-
let defaultTypesInputPath = config.extract.output || `locales/${config.extract.primaryLanguage}/*.json`
|
|
52
|
-
defaultTypesInputPath = getOutputPath(defaultTypesInputPath, config.extract.primaryLanguage || 'en', '*')
|
|
53
|
-
|
|
54
|
-
if (!config.types) config.types = { input: defaultTypesInputPath, output: 'src/@types/i18next.d.ts' }
|
|
55
|
-
if (config.types.input === undefined) config.types.input = defaultTypesInputPath
|
|
56
|
-
if (!config.types.output) config.types.output = 'src/@types/i18next.d.ts'
|
|
57
|
-
if (!config.types.resourcesFile) config.types.resourcesFile = join(dirname(config.types?.output), 'resources.d.ts')
|
|
58
|
-
|
|
59
|
-
if (!config.types?.input || config.types?.input.length < 0) {
|
|
60
|
-
console.log('No input defined!')
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const resourceFiles = await glob(config.types?.input || [], {
|
|
65
|
-
cwd: process.cwd(),
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const resources: Resource[] = []
|
|
69
|
-
|
|
70
|
-
for (const file of resourceFiles) {
|
|
71
|
-
const namespace = basename(file, extname(file))
|
|
72
|
-
const content = await readFile(file, 'utf-8')
|
|
73
|
-
const parsedContent = JSON.parse(content)
|
|
74
|
-
|
|
75
|
-
// If mergeNamespaces is used, a single file can contain multiple namespaces
|
|
76
|
-
// (e.g. { "translation": { ... }, "common": { ... } } in a per-language file).
|
|
77
|
-
// In that case, expose each top-level key as a namespace entry so the type
|
|
78
|
-
// generator will produce top-level namespace interfaces (not a language wrapper).
|
|
79
|
-
if (config.extract?.mergeNamespaces && parsedContent && typeof parsedContent === 'object') {
|
|
80
|
-
const keys = Object.keys(parsedContent)
|
|
81
|
-
const allObjects = keys.length > 0 && keys.every(k => parsedContent[k] && typeof parsedContent[k] === 'object')
|
|
82
|
-
if (allObjects) {
|
|
83
|
-
for (const nsName of keys) {
|
|
84
|
-
resources.push({ name: nsName, resources: parsedContent[nsName] })
|
|
85
|
-
}
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
resources.push({ name: namespace, resources: parsedContent })
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const logMessages: string[] = []
|
|
94
|
-
|
|
95
|
-
const enableSelector = config.types?.enableSelector || false
|
|
96
|
-
const indentation = config.types?.indentation ?? config.extract.indentation ?? 2
|
|
97
|
-
const interfaceDefinition = `// This file is automatically generated by i18next-cli. Do not edit manually.
|
|
98
|
-
${mergeResourcesAsInterface(resources, { optimize: !!enableSelector, indentation })}`
|
|
99
|
-
|
|
100
|
-
const outputPath = resolve(process.cwd(), config.types?.output || '')
|
|
101
|
-
const resourcesOutputPath = resolve(process.cwd(), config.types.resourcesFile)
|
|
102
|
-
await mkdir(dirname(resourcesOutputPath), { recursive: true })
|
|
103
|
-
await writeFile(resourcesOutputPath, interfaceDefinition)
|
|
104
|
-
logMessages.push(` ${chalk.green('ā')} Resources interface written to ${config.types.resourcesFile}`)
|
|
105
|
-
|
|
106
|
-
let outputPathExists
|
|
107
|
-
try {
|
|
108
|
-
await access(outputPath)
|
|
109
|
-
outputPathExists = true
|
|
110
|
-
} catch (e) {
|
|
111
|
-
outputPathExists = false
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (!outputPathExists) {
|
|
115
|
-
// The main output file will now import from the resources file
|
|
116
|
-
const importPath = relative(dirname(outputPath), resourcesOutputPath)
|
|
117
|
-
.replace(/\\/g, '/').replace(/\.d\.ts$/, '') // Make it a valid module path
|
|
118
|
-
|
|
119
|
-
const fileContent = `// This file is automatically generated by i18next-cli. Do not edit manually.
|
|
120
|
-
import Resources from './${importPath}';
|
|
121
|
-
|
|
122
|
-
declare module 'i18next' {
|
|
123
|
-
interface CustomTypeOptions {
|
|
124
|
-
enableSelector: ${typeof enableSelector === 'string' ? `"${enableSelector}"` : enableSelector};
|
|
125
|
-
defaultNS: '${config.extract.defaultNS || 'translation'}';
|
|
126
|
-
resources: Resources;
|
|
127
|
-
}
|
|
128
|
-
}`
|
|
129
|
-
await mkdir(dirname(outputPath), { recursive: true })
|
|
130
|
-
await writeFile(outputPath, fileContent)
|
|
131
|
-
logMessages.push(` ${chalk.green('ā')} TypeScript definitions written to ${config.types.output || ''}`)
|
|
132
|
-
}
|
|
133
|
-
spinner.succeed(chalk.bold('TypeScript definitions generated successfully.'))
|
|
134
|
-
logMessages.forEach(msg => console.log(msg))
|
|
135
|
-
} catch (error) {
|
|
136
|
-
spinner.fail(chalk.red('Failed to generate TypeScript definitions.'))
|
|
137
|
-
console.error(error)
|
|
138
|
-
}
|
|
139
|
-
}
|