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.
Files changed (39) hide show
  1. package/dist/cjs/cli.js +1 -1
  2. package/dist/esm/cli.js +1 -1
  3. package/package.json +6 -6
  4. package/types/cli.d.ts +3 -1
  5. package/types/cli.d.ts.map +1 -1
  6. package/CHANGELOG.md +0 -599
  7. package/src/cli.ts +0 -283
  8. package/src/config.ts +0 -215
  9. package/src/extractor/core/ast-visitors.ts +0 -259
  10. package/src/extractor/core/extractor.ts +0 -250
  11. package/src/extractor/core/key-finder.ts +0 -142
  12. package/src/extractor/core/translation-manager.ts +0 -750
  13. package/src/extractor/index.ts +0 -7
  14. package/src/extractor/parsers/ast-utils.ts +0 -87
  15. package/src/extractor/parsers/call-expression-handler.ts +0 -793
  16. package/src/extractor/parsers/comment-parser.ts +0 -424
  17. package/src/extractor/parsers/expression-resolver.ts +0 -391
  18. package/src/extractor/parsers/jsx-handler.ts +0 -488
  19. package/src/extractor/parsers/jsx-parser.ts +0 -1463
  20. package/src/extractor/parsers/scope-manager.ts +0 -445
  21. package/src/extractor/plugin-manager.ts +0 -116
  22. package/src/extractor.ts +0 -15
  23. package/src/heuristic-config.ts +0 -92
  24. package/src/index.ts +0 -22
  25. package/src/init.ts +0 -175
  26. package/src/linter.ts +0 -345
  27. package/src/locize.ts +0 -263
  28. package/src/migrator.ts +0 -208
  29. package/src/rename-key.ts +0 -398
  30. package/src/status.ts +0 -380
  31. package/src/syncer.ts +0 -133
  32. package/src/types-generator.ts +0 -139
  33. package/src/types.ts +0 -577
  34. package/src/utils/default-value.ts +0 -45
  35. package/src/utils/file-utils.ts +0 -167
  36. package/src/utils/funnel-msg-tracker.ts +0 -84
  37. package/src/utils/logger.ts +0 -36
  38. package/src/utils/nested-object.ts +0 -135
  39. 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
- }
@@ -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
- }