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.
Files changed (127) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +489 -0
  4. package/dist/cjs/cli.js +2 -0
  5. package/dist/cjs/config.js +1 -0
  6. package/dist/cjs/extractor/core/extractor.js +1 -0
  7. package/dist/cjs/extractor/core/key-finder.js +1 -0
  8. package/dist/cjs/extractor/core/translation-manager.js +1 -0
  9. package/dist/cjs/extractor/parsers/ast-visitors.js +1 -0
  10. package/dist/cjs/extractor/parsers/comment-parser.js +1 -0
  11. package/dist/cjs/extractor/parsers/jsx-parser.js +1 -0
  12. package/dist/cjs/extractor/plugin-manager.js +1 -0
  13. package/dist/cjs/heuristic-config.js +1 -0
  14. package/dist/cjs/index.js +1 -0
  15. package/dist/cjs/init.js +1 -0
  16. package/dist/cjs/linter.js +1 -0
  17. package/dist/cjs/locize.js +1 -0
  18. package/dist/cjs/migrator.js +1 -0
  19. package/dist/cjs/package.json +1 -0
  20. package/dist/cjs/status.js +1 -0
  21. package/dist/cjs/syncer.js +1 -0
  22. package/dist/cjs/types-generator.js +1 -0
  23. package/dist/cjs/utils/file-utils.js +1 -0
  24. package/dist/cjs/utils/logger.js +1 -0
  25. package/dist/cjs/utils/nested-object.js +1 -0
  26. package/dist/cjs/utils/validation.js +1 -0
  27. package/dist/esm/cli.js +2 -0
  28. package/dist/esm/config.js +1 -0
  29. package/dist/esm/extractor/core/extractor.js +1 -0
  30. package/dist/esm/extractor/core/key-finder.js +1 -0
  31. package/dist/esm/extractor/core/translation-manager.js +1 -0
  32. package/dist/esm/extractor/parsers/ast-visitors.js +1 -0
  33. package/dist/esm/extractor/parsers/comment-parser.js +1 -0
  34. package/dist/esm/extractor/parsers/jsx-parser.js +1 -0
  35. package/dist/esm/extractor/plugin-manager.js +1 -0
  36. package/dist/esm/heuristic-config.js +1 -0
  37. package/dist/esm/index.js +1 -0
  38. package/dist/esm/init.js +1 -0
  39. package/dist/esm/linter.js +1 -0
  40. package/dist/esm/locize.js +1 -0
  41. package/dist/esm/migrator.js +1 -0
  42. package/dist/esm/status.js +1 -0
  43. package/dist/esm/syncer.js +1 -0
  44. package/dist/esm/types-generator.js +1 -0
  45. package/dist/esm/utils/file-utils.js +1 -0
  46. package/dist/esm/utils/logger.js +1 -0
  47. package/dist/esm/utils/nested-object.js +1 -0
  48. package/dist/esm/utils/validation.js +1 -0
  49. package/package.json +81 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +158 -0
  52. package/src/extractor/core/extractor.ts +195 -0
  53. package/src/extractor/core/key-finder.ts +70 -0
  54. package/src/extractor/core/translation-manager.ts +115 -0
  55. package/src/extractor/index.ts +7 -0
  56. package/src/extractor/parsers/ast-visitors.ts +637 -0
  57. package/src/extractor/parsers/comment-parser.ts +125 -0
  58. package/src/extractor/parsers/jsx-parser.ts +166 -0
  59. package/src/extractor/plugin-manager.ts +54 -0
  60. package/src/extractor.ts +15 -0
  61. package/src/heuristic-config.ts +64 -0
  62. package/src/index.ts +12 -0
  63. package/src/init.ts +156 -0
  64. package/src/linter.ts +191 -0
  65. package/src/locize.ts +251 -0
  66. package/src/migrator.ts +139 -0
  67. package/src/status.ts +192 -0
  68. package/src/syncer.ts +114 -0
  69. package/src/types-generator.ts +116 -0
  70. package/src/types.ts +312 -0
  71. package/src/utils/file-utils.ts +81 -0
  72. package/src/utils/logger.ts +36 -0
  73. package/src/utils/nested-object.ts +113 -0
  74. package/src/utils/validation.ts +69 -0
  75. package/tryme.js +8 -0
  76. package/tsconfig.json +71 -0
  77. package/types/cli.d.ts +3 -0
  78. package/types/cli.d.ts.map +1 -0
  79. package/types/config.d.ts +50 -0
  80. package/types/config.d.ts.map +1 -0
  81. package/types/extractor/core/extractor.d.ts +66 -0
  82. package/types/extractor/core/extractor.d.ts.map +1 -0
  83. package/types/extractor/core/key-finder.d.ts +31 -0
  84. package/types/extractor/core/key-finder.d.ts.map +1 -0
  85. package/types/extractor/core/translation-manager.d.ts +31 -0
  86. package/types/extractor/core/translation-manager.d.ts.map +1 -0
  87. package/types/extractor/index.d.ts +8 -0
  88. package/types/extractor/index.d.ts.map +1 -0
  89. package/types/extractor/parsers/ast-visitors.d.ts +235 -0
  90. package/types/extractor/parsers/ast-visitors.d.ts.map +1 -0
  91. package/types/extractor/parsers/comment-parser.d.ts +24 -0
  92. package/types/extractor/parsers/comment-parser.d.ts.map +1 -0
  93. package/types/extractor/parsers/jsx-parser.d.ts +35 -0
  94. package/types/extractor/parsers/jsx-parser.d.ts.map +1 -0
  95. package/types/extractor/plugin-manager.d.ts +37 -0
  96. package/types/extractor/plugin-manager.d.ts.map +1 -0
  97. package/types/extractor.d.ts +7 -0
  98. package/types/extractor.d.ts.map +1 -0
  99. package/types/heuristic-config.d.ts +10 -0
  100. package/types/heuristic-config.d.ts.map +1 -0
  101. package/types/index.d.ts +4 -0
  102. package/types/index.d.ts.map +1 -0
  103. package/types/init.d.ts +29 -0
  104. package/types/init.d.ts.map +1 -0
  105. package/types/linter.d.ts +33 -0
  106. package/types/linter.d.ts.map +1 -0
  107. package/types/locize.d.ts +5 -0
  108. package/types/locize.d.ts.map +1 -0
  109. package/types/migrator.d.ts +37 -0
  110. package/types/migrator.d.ts.map +1 -0
  111. package/types/status.d.ts +20 -0
  112. package/types/status.d.ts.map +1 -0
  113. package/types/syncer.d.ts +33 -0
  114. package/types/syncer.d.ts.map +1 -0
  115. package/types/types-generator.d.ts +29 -0
  116. package/types/types-generator.d.ts.map +1 -0
  117. package/types/types.d.ts +268 -0
  118. package/types/types.d.ts.map +1 -0
  119. package/types/utils/file-utils.d.ts +61 -0
  120. package/types/utils/file-utils.d.ts.map +1 -0
  121. package/types/utils/logger.d.ts +34 -0
  122. package/types/utils/logger.d.ts.map +1 -0
  123. package/types/utils/nested-object.d.ts +71 -0
  124. package/types/utils/nested-object.d.ts.map +1 -0
  125. package/types/utils/validation.d.ts +47 -0
  126. package/types/utils/validation.d.ts.map +1 -0
  127. 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
+ }