i18next-cli 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/config.js +1 -0
- package/dist/cjs/extractor/core/extractor.js +1 -0
- package/dist/cjs/extractor/core/key-finder.js +1 -0
- package/dist/cjs/extractor/core/translation-manager.js +1 -0
- package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
- package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
- package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
- package/dist/cjs/extractor/plugin-manager.js +1 -0
- package/dist/cjs/heuristic-config.js +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/init.js +1 -0
- package/dist/cjs/linter.js +1 -0
- package/dist/cjs/locize.js +1 -0
- package/dist/cjs/migrator.js +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/status.js +1 -0
- package/dist/cjs/syncer.js +1 -0
- package/dist/cjs/types-generator.js +1 -0
- package/dist/cjs/utils/file-utils.js +1 -0
- package/dist/cjs/utils/logger.js +1 -0
- package/dist/cjs/utils/nested-object.js +1 -0
- package/dist/cjs/utils/validation.js +1 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/config.js +1 -0
- package/dist/esm/extractor/core/extractor.js +1 -0
- package/dist/esm/extractor/core/key-finder.js +1 -0
- package/dist/esm/extractor/core/translation-manager.js +1 -0
- package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
- package/dist/esm/extractor/parsers/comment-parser.js +1 -0
- package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
- package/dist/esm/extractor/plugin-manager.js +1 -0
- package/dist/esm/heuristic-config.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/init.js +1 -0
- package/dist/esm/linter.js +1 -0
- package/dist/esm/locize.js +1 -0
- package/dist/esm/migrator.js +1 -0
- package/dist/esm/status.js +1 -0
- package/dist/esm/syncer.js +1 -0
- package/dist/esm/types-generator.js +1 -0
- package/dist/esm/utils/file-utils.js +1 -0
- package/dist/esm/utils/logger.js +1 -0
- package/dist/esm/utils/nested-object.js +1 -0
- package/dist/esm/utils/validation.js +1 -0
- package/package.json +81 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +158 -0
- package/src/extractor/core/extractor.ts +195 -0
- package/src/extractor/core/key-finder.ts +70 -0
- package/src/extractor/core/translation-manager.ts +115 -0
- package/src/extractor/index.ts +7 -0
- package/src/extractor/parsers/ast-visitors.ts +637 -0
- package/src/extractor/parsers/comment-parser.ts +125 -0
- package/src/extractor/parsers/jsx-parser.ts +166 -0
- package/src/extractor/plugin-manager.ts +54 -0
- package/src/extractor.ts +15 -0
- package/src/heuristic-config.ts +64 -0
- package/src/index.ts +12 -0
- package/src/init.ts +156 -0
- package/src/linter.ts +191 -0
- package/src/locize.ts +251 -0
- package/src/migrator.ts +139 -0
- package/src/status.ts +192 -0
- package/src/syncer.ts +114 -0
- package/src/types-generator.ts +116 -0
- package/src/types.ts +312 -0
- package/src/utils/file-utils.ts +81 -0
- package/src/utils/logger.ts +36 -0
- package/src/utils/nested-object.ts +113 -0
- package/src/utils/validation.ts +69 -0
- package/tryme.js +8 -0
- package/tsconfig.json +71 -0
- package/types/cli.d.ts +3 -0
- package/types/cli.d.ts.map +1 -0
- package/types/config.d.ts +50 -0
- package/types/config.d.ts.map +1 -0
- package/types/extractor/core/extractor.d.ts +66 -0
- package/types/extractor/core/extractor.d.ts.map +1 -0
- package/types/extractor/core/key-finder.d.ts +31 -0
- package/types/extractor/core/key-finder.d.ts.map +1 -0
- package/types/extractor/core/translation-manager.d.ts +31 -0
- package/types/extractor/core/translation-manager.d.ts.map +1 -0
- package/types/extractor/index.d.ts +8 -0
- package/types/extractor/index.d.ts.map +1 -0
- package/types/extractor/parsers/ast-visitors.d.ts +235 -0
- package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
- package/types/extractor/parsers/comment-parser.d.ts +24 -0
- package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
- package/types/extractor/parsers/jsx-parser.d.ts +35 -0
- package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
- package/types/extractor/plugin-manager.d.ts +37 -0
- package/types/extractor/plugin-manager.d.ts.map +1 -0
- package/types/extractor.d.ts +7 -0
- package/types/extractor.d.ts.map +1 -0
- package/types/heuristic-config.d.ts +10 -0
- package/types/heuristic-config.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/init.d.ts +29 -0
- package/types/init.d.ts.map +1 -0
- package/types/linter.d.ts +33 -0
- package/types/linter.d.ts.map +1 -0
- package/types/locize.d.ts +5 -0
- package/types/locize.d.ts.map +1 -0
- package/types/migrator.d.ts +37 -0
- package/types/migrator.d.ts.map +1 -0
- package/types/status.d.ts +20 -0
- package/types/status.d.ts.map +1 -0
- package/types/syncer.d.ts +33 -0
- package/types/syncer.d.ts.map +1 -0
- package/types/types-generator.d.ts +29 -0
- package/types/types-generator.d.ts.map +1 -0
- package/types/types.d.ts +268 -0
- package/types/types.d.ts.map +1 -0
- package/types/utils/file-utils.d.ts +61 -0
- package/types/utils/file-utils.d.ts.map +1 -0
- package/types/utils/logger.d.ts +34 -0
- package/types/utils/logger.d.ts.map +1 -0
- package/types/utils/nested-object.d.ts +71 -0
- package/types/utils/nested-object.d.ts.map +1 -0
- package/types/utils/validation.d.ts +47 -0
- package/types/utils/validation.d.ts.map +1 -0
- package/vitest.config.ts +13 -0
package/src/status.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora, { Ora } from 'ora'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { findKeys } from './extractor/core/key-finder'
|
|
6
|
+
import { getNestedKeys, getNestedValue } from './utils/nested-object'
|
|
7
|
+
import type { I18nextToolkitConfig, ExtractedKey } from './types'
|
|
8
|
+
import { getOutputPath } from './utils/file-utils'
|
|
9
|
+
|
|
10
|
+
interface StatusOptions {
|
|
11
|
+
detail?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs a health check on the project's i18next translations and displays a status report.
|
|
16
|
+
*
|
|
17
|
+
* This command provides a high-level overview of the localization status by:
|
|
18
|
+
* 1. Extracting all keys from the source code using the core extractor.
|
|
19
|
+
* 2. Reading all existing translation files for each locale.
|
|
20
|
+
* 3. Calculating the translation completeness for each secondary language against the primary.
|
|
21
|
+
* 4. Displaying a formatted report with key counts, locales, and progress bars.
|
|
22
|
+
* 5. Serving as a value-driven funnel to introduce the locize commercial service.
|
|
23
|
+
*
|
|
24
|
+
* @param config - The i18next toolkit configuration object.
|
|
25
|
+
* @param options Options object, may contain a `detail` property with a locale string.
|
|
26
|
+
*/
|
|
27
|
+
export async function runStatus (config: I18nextToolkitConfig, options: StatusOptions = {}) {
|
|
28
|
+
const spinner = ora('Analyzing project localization status...\n').start()
|
|
29
|
+
try {
|
|
30
|
+
if (options.detail) {
|
|
31
|
+
await displayDetailedStatus(config, options.detail, spinner)
|
|
32
|
+
} else {
|
|
33
|
+
await displaySummaryStatus(config, spinner)
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
spinner.fail('Failed to generate status report.')
|
|
37
|
+
console.error(error)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Displays a detailed, key-by-key translation status for a specific locale,
|
|
43
|
+
* grouped by namespace.
|
|
44
|
+
* @param config The toolkit configuration.
|
|
45
|
+
* @param locale The locale to display the detailed status for.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
async function displayDetailedStatus (config: I18nextToolkitConfig, locale: string, spinner: Ora) {
|
|
49
|
+
const { primaryLanguage, keySeparator = '.', defaultNS = 'translation' } = config.extract
|
|
50
|
+
|
|
51
|
+
if (!config.locales.includes(locale)) {
|
|
52
|
+
console.error(chalk.red(`Error: Locale "${locale}" is not defined in your configuration.`))
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (locale === primaryLanguage) {
|
|
56
|
+
console.log(chalk.yellow(`Locale "${locale}" is the primary language, so all keys are considered present.`))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`Analyzing detailed status for locale: ${chalk.bold.cyan(locale)}...`)
|
|
61
|
+
|
|
62
|
+
const allExtractedKeys = await findKeys(config)
|
|
63
|
+
|
|
64
|
+
spinner.succeed('Analysis complete.')
|
|
65
|
+
|
|
66
|
+
if (allExtractedKeys.size === 0) {
|
|
67
|
+
console.log(chalk.green('No keys found in source code.'))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Group keys by namespace to read the correct files
|
|
72
|
+
const keysByNs = new Map<string, ExtractedKey[]>()
|
|
73
|
+
for (const key of allExtractedKeys.values()) {
|
|
74
|
+
const ns = key.ns || defaultNS
|
|
75
|
+
if (!keysByNs.has(ns)) keysByNs.set(ns, [])
|
|
76
|
+
keysByNs.get(ns)!.push(key)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const translationsByNs = new Map<string, Record<string, any>>()
|
|
80
|
+
for (const ns of keysByNs.keys()) {
|
|
81
|
+
const langFilePath = getOutputPath(config.extract.output, locale, ns)
|
|
82
|
+
try {
|
|
83
|
+
const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
|
|
84
|
+
translationsByNs.set(ns, JSON.parse(content))
|
|
85
|
+
} catch {
|
|
86
|
+
translationsByNs.set(ns, {}) // File not found, treat as empty
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let missingCount = 0
|
|
91
|
+
console.log(chalk.bold(`\nKey Status for "${locale}":`))
|
|
92
|
+
|
|
93
|
+
// 1. Get and sort the namespace names alphabetically
|
|
94
|
+
const sortedNamespaces = Array.from(keysByNs.keys()).sort()
|
|
95
|
+
|
|
96
|
+
// 2. Loop through each namespace
|
|
97
|
+
for (const ns of sortedNamespaces) {
|
|
98
|
+
console.log(chalk.cyan.bold(`\nNamespace: ${ns}`))
|
|
99
|
+
|
|
100
|
+
const keysForNs = keysByNs.get(ns) || []
|
|
101
|
+
const sortedKeysForNs = keysForNs.sort((a, b) => a.key.localeCompare(b.key))
|
|
102
|
+
const translations = translationsByNs.get(ns) || {}
|
|
103
|
+
|
|
104
|
+
// 3. Loop through the keys within the current namespace
|
|
105
|
+
for (const { key } of sortedKeysForNs) {
|
|
106
|
+
const value = getNestedValue(translations, key, keySeparator ?? '.')
|
|
107
|
+
|
|
108
|
+
if (value) {
|
|
109
|
+
console.log(` ${chalk.green('ā')} ${key}`)
|
|
110
|
+
} else {
|
|
111
|
+
missingCount++
|
|
112
|
+
console.log(` ${chalk.red('ā')} ${key}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (missingCount > 0) {
|
|
118
|
+
console.log(chalk.yellow.bold(`\n\nSummary: Found ${missingCount} missing translations for "${locale}".`))
|
|
119
|
+
} else {
|
|
120
|
+
console.log(chalk.green.bold(`\n\nSummary: š All ${allExtractedKeys.size} keys are translated for "${locale}".`))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Displays a high-level summary report of translation progress for all locales.
|
|
126
|
+
* @param config The toolkit configuration.
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
async function displaySummaryStatus (config: I18nextToolkitConfig, spinner: Ora) {
|
|
130
|
+
console.log('Analyzing project localization status...')
|
|
131
|
+
|
|
132
|
+
const allExtractedKeys = await findKeys(config)
|
|
133
|
+
const totalKeys = allExtractedKeys.size
|
|
134
|
+
|
|
135
|
+
const { primaryLanguage, keySeparator = '.', defaultNS = 'translation' } = config.extract
|
|
136
|
+
const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
|
|
137
|
+
|
|
138
|
+
const allNamespaces = new Set<string>(
|
|
139
|
+
Array.from(allExtractedKeys.values()).map(k => k.ns || defaultNS)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
spinner.succeed('Analysis complete.')
|
|
143
|
+
|
|
144
|
+
console.log(chalk.cyan.bold('\ni18next Project Status'))
|
|
145
|
+
console.log('------------------------')
|
|
146
|
+
console.log(`š Keys Found: ${chalk.bold(totalKeys)}`)
|
|
147
|
+
console.log(`š Locales: ${chalk.bold(config.locales.join(', '))}`)
|
|
148
|
+
console.log(`ā
Primary Language: ${chalk.bold(primaryLanguage)}`)
|
|
149
|
+
console.log('\nTranslation Progress:')
|
|
150
|
+
|
|
151
|
+
for (const lang of secondaryLanguages) {
|
|
152
|
+
let translatedKeysCount = 0
|
|
153
|
+
|
|
154
|
+
for (const ns of allNamespaces) {
|
|
155
|
+
const langFilePath = getOutputPath(config.extract.output, lang, ns)
|
|
156
|
+
try {
|
|
157
|
+
const content = await readFile(resolve(process.cwd(), langFilePath), 'utf-8')
|
|
158
|
+
const translations = JSON.parse(content)
|
|
159
|
+
const translatedKeysInFile = getNestedKeys(translations, keySeparator ?? '.')
|
|
160
|
+
|
|
161
|
+
const countForNs = translatedKeysInFile.filter(k => {
|
|
162
|
+
const value = getNestedValue(translations, k, keySeparator ?? '.')
|
|
163
|
+
// A key is counted if it has a non-empty value AND it was extracted from the source for this namespace
|
|
164
|
+
return !!value && allExtractedKeys.has(`${ns}:${k}`)
|
|
165
|
+
}).length
|
|
166
|
+
translatedKeysCount += countForNs
|
|
167
|
+
} catch {
|
|
168
|
+
// File not found for this namespace, so its contribution to the count is 0
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const percentage = totalKeys > 0 ? Math.round((translatedKeysCount / totalKeys) * 100) : 100
|
|
173
|
+
const progressBar = generateProgressBar(percentage)
|
|
174
|
+
console.log(`- ${lang}: ${progressBar} ${percentage}% (${translatedKeysCount}/${totalKeys} keys)`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(chalk.yellow.bold('\n⨠Take your localization to the next level!'))
|
|
178
|
+
console.log('Manage translations with your team in the cloud with locize => https://www.locize.com/docs/getting-started')
|
|
179
|
+
console.log(`Run ${chalk.cyan('npx i18next-cli locize-migrate')} to get started.`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generates a simple text-based progress bar.
|
|
184
|
+
* @param percentage - The percentage to display (0-100).
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
function generateProgressBar (percentage: number): string {
|
|
188
|
+
const totalBars = 20
|
|
189
|
+
const filledBars = Math.round((percentage / 100) * totalBars)
|
|
190
|
+
const emptyBars = totalBars - filledBars
|
|
191
|
+
return `[${chalk.green(''.padStart(filledBars, 'ā '))}${''.padStart(emptyBars, 'ā”')}]`
|
|
192
|
+
}
|
package/src/syncer.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { resolve, dirname } from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import type { I18nextToolkitConfig } from './types'
|
|
6
|
+
import { getNestedKeys, getNestedValue, setNestedValue } from './utils/nested-object'
|
|
7
|
+
import { getOutputPath } from './utils/file-utils'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Synchronizes translation files across different locales by ensuring all secondary
|
|
11
|
+
* language files contain the same keys as the primary language file.
|
|
12
|
+
*
|
|
13
|
+
* This function:
|
|
14
|
+
* 1. Reads the primary language translation file
|
|
15
|
+
* 2. Extracts all translation keys from the primary file
|
|
16
|
+
* 3. For each secondary language:
|
|
17
|
+
* - Preserves existing translations
|
|
18
|
+
* - Adds missing keys with empty values or configured default
|
|
19
|
+
* - Removes keys that no longer exist in primary
|
|
20
|
+
* 4. Only writes files that have changes
|
|
21
|
+
*
|
|
22
|
+
* @param config - The i18next toolkit configuration object
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // Configuration
|
|
27
|
+
* const config = {
|
|
28
|
+
* locales: ['en', 'de', 'fr'],
|
|
29
|
+
* extract: {
|
|
30
|
+
* output: 'locales/{{language}}/{{namespace}}.json',
|
|
31
|
+
* defaultNS: 'translation'
|
|
32
|
+
* defaultValue: '[MISSING]'
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* await runSyncer(config)
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export async function runSyncer (config: I18nextToolkitConfig) {
|
|
40
|
+
const spinner = ora('Running i18next locale synchronizer...\n').start()
|
|
41
|
+
|
|
42
|
+
config.extract.primaryLanguage ||= config.locales[0] || 'en'
|
|
43
|
+
const { primaryLanguage } = config.extract
|
|
44
|
+
const secondaryLanguages = config.locales.filter(l => l !== primaryLanguage)
|
|
45
|
+
const keySeparator = config.extract.keySeparator ?? '.'
|
|
46
|
+
|
|
47
|
+
const logMessages: string[] = []
|
|
48
|
+
let wasAnythingSynced = false
|
|
49
|
+
|
|
50
|
+
// Assume sync operates on the default namespace for simplicity
|
|
51
|
+
const defaultNS = config.extract.defaultNS ?? 'translation'
|
|
52
|
+
|
|
53
|
+
// 1. Get all keys from the primary language file
|
|
54
|
+
const primaryPath = getOutputPath(config.extract.output, primaryLanguage, defaultNS)
|
|
55
|
+
|
|
56
|
+
const fullPrimaryPath = resolve(process.cwd(), primaryPath)
|
|
57
|
+
|
|
58
|
+
let primaryTranslations: Record<string, any>
|
|
59
|
+
try {
|
|
60
|
+
const primaryContent = await readFile(fullPrimaryPath, 'utf-8')
|
|
61
|
+
primaryTranslations = JSON.parse(primaryContent)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`Primary language file not found at ${primaryPath}. Cannot sync.`)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const primaryKeys = getNestedKeys(primaryTranslations, keySeparator)
|
|
68
|
+
|
|
69
|
+
// 2. Iterate through secondary languages and sync them
|
|
70
|
+
for (const lang of secondaryLanguages) {
|
|
71
|
+
const secondaryPath = getOutputPath(config.extract.output, lang, defaultNS)
|
|
72
|
+
const fullSecondaryPath = resolve(process.cwd(), secondaryPath)
|
|
73
|
+
|
|
74
|
+
let secondaryTranslations: Record<string, any> = {}
|
|
75
|
+
let oldContent = ''
|
|
76
|
+
try {
|
|
77
|
+
oldContent = await readFile(fullSecondaryPath, 'utf-8')
|
|
78
|
+
secondaryTranslations = JSON.parse(oldContent)
|
|
79
|
+
} catch (e) { /* File doesn't exist, will be created */ }
|
|
80
|
+
|
|
81
|
+
const newSecondaryTranslations: Record<string, any> = {}
|
|
82
|
+
|
|
83
|
+
// Rebuild the secondary file based on the primary file's keys
|
|
84
|
+
for (const key of primaryKeys) {
|
|
85
|
+
const existingValue = getNestedValue(secondaryTranslations, key, keySeparator)
|
|
86
|
+
// If value exists in old file, keep it. Otherwise, add as empty string.
|
|
87
|
+
const valueToSet = existingValue ?? (config.extract?.defaultValue || '')
|
|
88
|
+
setNestedValue(newSecondaryTranslations, key, valueToSet, keySeparator)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const indentation = config.extract.indentation ?? 2
|
|
92
|
+
const newContent = JSON.stringify(newSecondaryTranslations, null, indentation)
|
|
93
|
+
|
|
94
|
+
if (newContent !== oldContent) {
|
|
95
|
+
wasAnythingSynced = true
|
|
96
|
+
await mkdir(dirname(fullSecondaryPath), { recursive: true })
|
|
97
|
+
await writeFile(fullSecondaryPath, newContent)
|
|
98
|
+
logMessages.push(` ${chalk.green('ā')} Synchronized: ${secondaryPath}`)
|
|
99
|
+
} else {
|
|
100
|
+
logMessages.push(` ${chalk.gray('-')} Already in sync: ${secondaryPath}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
spinner.succeed(chalk.bold('Synchronization complete!'))
|
|
105
|
+
logMessages.forEach(msg => console.log(msg))
|
|
106
|
+
|
|
107
|
+
if (wasAnythingSynced) {
|
|
108
|
+
console.log(chalk.green.bold('\nā
Sync complete.'))
|
|
109
|
+
console.log(chalk.yellow('š Ready to collaborate with translators? Move your files to the cloud.'))
|
|
110
|
+
console.log(` Get started with the official TMS for i18next: ${chalk.cyan('npx i18next-cli locize-migrate')}`)
|
|
111
|
+
} else {
|
|
112
|
+
console.log(chalk.green.bold('\nā
All locales are already in sync.'))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a translation resource with its namespace name and content
|
|
11
|
+
*/
|
|
12
|
+
interface Resource {
|
|
13
|
+
/** The namespace name (filename without extension) */
|
|
14
|
+
name: string;
|
|
15
|
+
/** The parsed JSON resources object */
|
|
16
|
+
resources: object;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generates TypeScript type definitions for i18next translations.
|
|
21
|
+
*
|
|
22
|
+
* This function:
|
|
23
|
+
* 1. Reads translation files based on the input glob patterns
|
|
24
|
+
* 2. Generates TypeScript interfaces using i18next-resources-for-ts
|
|
25
|
+
* 3. Creates separate resources.d.ts and main i18next.d.ts files
|
|
26
|
+
* 4. Handles namespace detection from filenames
|
|
27
|
+
* 5. Supports type-safe selector API when enabled
|
|
28
|
+
*
|
|
29
|
+
* @param config - The i18next toolkit configuration object
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Configuration
|
|
34
|
+
* const config = {
|
|
35
|
+
* types: {
|
|
36
|
+
* input: ['locales/en/*.json'],
|
|
37
|
+
* output: 'src/types/i18next.d.ts',
|
|
38
|
+
* enableSelector: true
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* await runTypesGenerator(config)
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export async function runTypesGenerator (config: I18nextToolkitConfig) {
|
|
46
|
+
const spinner = ora('Generating TypeScript types for translations...\n').start()
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (!config.types) config.types = { input: ['locales/en/*.json'], output: 'src/types/i18next.d.ts' }
|
|
50
|
+
if (config.types.input === undefined) config.types.input = ['locales/en/*.json']
|
|
51
|
+
if (!config.types.output) config.types.output = 'src/types/i18next.d.ts'
|
|
52
|
+
if (!config.types.resourcesFile) config.types.resourcesFile = join(dirname(config.types?.output), 'resources.d.ts')
|
|
53
|
+
|
|
54
|
+
if (!config.types?.input || config.types?.input.length < 0) {
|
|
55
|
+
console.log('No input defined!')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resourceFiles = await glob(config.types?.input || [], {
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const resources: Resource[] = []
|
|
64
|
+
|
|
65
|
+
for (const file of resourceFiles) {
|
|
66
|
+
const namespace = basename(file, extname(file))
|
|
67
|
+
const content = await readFile(file, 'utf-8')
|
|
68
|
+
const parsedContent = JSON.parse(content)
|
|
69
|
+
resources.push({ name: namespace, resources: parsedContent })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const logMessages: string[] = []
|
|
73
|
+
|
|
74
|
+
const enableSelector = config.types?.enableSelector || false
|
|
75
|
+
const interfaceDefinition = mergeResourcesAsInterface(resources, { optimize: !!enableSelector })
|
|
76
|
+
const outputPath = resolve(process.cwd(), config.types?.output || '')
|
|
77
|
+
const resourcesOutputPath = resolve(process.cwd(), config.types.resourcesFile)
|
|
78
|
+
await mkdir(dirname(resourcesOutputPath), { recursive: true })
|
|
79
|
+
await writeFile(resourcesOutputPath, interfaceDefinition)
|
|
80
|
+
logMessages.push(` ${chalk.green('ā')} Resources interface written to ${config.types.resourcesFile}`)
|
|
81
|
+
|
|
82
|
+
let outputPathExists
|
|
83
|
+
try {
|
|
84
|
+
await access(outputPath)
|
|
85
|
+
outputPathExists = true
|
|
86
|
+
} catch (e) {
|
|
87
|
+
outputPathExists = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!outputPathExists) {
|
|
91
|
+
// The main output file will now import from the resources file
|
|
92
|
+
const importPath = relative(dirname(outputPath), resourcesOutputPath)
|
|
93
|
+
.replace(/\\/g, '/').replace(/\.d\.ts$/, '') // Make it a valid module path
|
|
94
|
+
|
|
95
|
+
const fileContent = `// This file is automatically generated by i18next-cli. Do not edit manually.
|
|
96
|
+
import Resources from './${importPath}';
|
|
97
|
+
|
|
98
|
+
declare module 'i18next' {
|
|
99
|
+
interface CustomTypeOptions {
|
|
100
|
+
enableSelector: ${typeof enableSelector === 'string' ? `"${enableSelector}"` : enableSelector};
|
|
101
|
+
defaultNS: '${config.extract.defaultNS || 'translation'}';
|
|
102
|
+
resources: Resources;
|
|
103
|
+
}
|
|
104
|
+
}`
|
|
105
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
106
|
+
await writeFile(outputPath, fileContent)
|
|
107
|
+
logMessages.push(` ${chalk.green('ā')} TypeScript definitions written to ${config.types.output || ''}`)
|
|
108
|
+
|
|
109
|
+
spinner.succeed(chalk.bold('TypeScript definitions generated successfully.'))
|
|
110
|
+
logMessages.forEach(msg => console.log(msg))
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
spinner.fail(chalk.red('Failed to generate TypeScript definitions.'))
|
|
114
|
+
console.error(error)
|
|
115
|
+
}
|
|
116
|
+
}
|