i18next-cli 1.24.12 → 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 (42) hide show
  1. package/dist/cjs/cli.js +1 -1
  2. package/dist/cjs/extractor/parsers/expression-resolver.js +1 -1
  3. package/dist/esm/cli.js +1 -1
  4. package/dist/esm/extractor/parsers/expression-resolver.js +1 -1
  5. package/package.json +6 -6
  6. package/types/cli.d.ts +3 -1
  7. package/types/cli.d.ts.map +1 -1
  8. package/types/extractor/parsers/expression-resolver.d.ts.map +1 -1
  9. package/CHANGELOG.md +0 -595
  10. package/src/cli.ts +0 -283
  11. package/src/config.ts +0 -215
  12. package/src/extractor/core/ast-visitors.ts +0 -259
  13. package/src/extractor/core/extractor.ts +0 -250
  14. package/src/extractor/core/key-finder.ts +0 -142
  15. package/src/extractor/core/translation-manager.ts +0 -750
  16. package/src/extractor/index.ts +0 -7
  17. package/src/extractor/parsers/ast-utils.ts +0 -87
  18. package/src/extractor/parsers/call-expression-handler.ts +0 -793
  19. package/src/extractor/parsers/comment-parser.ts +0 -424
  20. package/src/extractor/parsers/expression-resolver.ts +0 -353
  21. package/src/extractor/parsers/jsx-handler.ts +0 -488
  22. package/src/extractor/parsers/jsx-parser.ts +0 -1463
  23. package/src/extractor/parsers/scope-manager.ts +0 -445
  24. package/src/extractor/plugin-manager.ts +0 -116
  25. package/src/extractor.ts +0 -15
  26. package/src/heuristic-config.ts +0 -92
  27. package/src/index.ts +0 -22
  28. package/src/init.ts +0 -175
  29. package/src/linter.ts +0 -345
  30. package/src/locize.ts +0 -263
  31. package/src/migrator.ts +0 -208
  32. package/src/rename-key.ts +0 -398
  33. package/src/status.ts +0 -380
  34. package/src/syncer.ts +0 -133
  35. package/src/types-generator.ts +0 -139
  36. package/src/types.ts +0 -577
  37. package/src/utils/default-value.ts +0 -45
  38. package/src/utils/file-utils.ts +0 -167
  39. package/src/utils/funnel-msg-tracker.ts +0 -84
  40. package/src/utils/logger.ts +0 -36
  41. package/src/utils/nested-object.ts +0 -135
  42. package/src/utils/validation.ts +0 -72
package/src/init.ts DELETED
@@ -1,175 +0,0 @@
1
- import inquirer from 'inquirer'
2
- import { writeFile, readFile } from 'node:fs/promises'
3
- import { resolve } from 'node:path'
4
- import { detectConfig } from './heuristic-config'
5
-
6
- /**
7
- * Determines if the current project is configured as an ESM project.
8
- * Checks the package.json file for `"type": "module"`.
9
- *
10
- * @returns Promise resolving to true if ESM, false if CommonJS
11
- *
12
- * @example
13
- * ```typescript
14
- * const isESM = await isEsmProject()
15
- * if (isESM) {
16
- * // Generate ESM syntax
17
- * } else {
18
- * // Generate CommonJS syntax
19
- * }
20
- * ```
21
- */
22
- async function isEsmProject (): Promise<boolean> {
23
- try {
24
- const packageJsonPath = resolve(process.cwd(), 'package.json')
25
- const content = await readFile(packageJsonPath, 'utf-8')
26
- const packageJson = JSON.parse(content)
27
- return packageJson.type === 'module'
28
- } catch {
29
- return true // Default to ESM if package.json is not found or readable
30
- }
31
- }
32
-
33
- /**
34
- * Interactive setup wizard for creating a new i18next-cli configuration file.
35
- *
36
- * This function provides a guided setup experience that:
37
- * 1. Asks the user for their preferred configuration file type (TypeScript or JavaScript)
38
- * 2. Collects basic project settings (locales, input patterns, output paths)
39
- * 3. Detects the project module system (ESM vs CommonJS) for JavaScript files
40
- * 4. Generates an appropriate configuration file with proper syntax
41
- * 5. Provides helpful defaults for common use cases
42
- *
43
- * The generated configuration includes:
44
- * - Locale specification
45
- * - Input file patterns for source scanning
46
- * - Output path templates with placeholders
47
- * - Proper imports and exports for the detected module system
48
- * - JSDoc type annotations for JavaScript files
49
- *
50
- * @example
51
- * ```typescript
52
- * // Run the interactive setup
53
- * await runInit()
54
- *
55
- * // This will create either:
56
- * // - i18next.config.ts (TypeScript)
57
- * // - i18next.config.js (JavaScript ESM/CommonJS)
58
- * ```
59
- */
60
- export async function runInit () {
61
- console.log('Welcome to the i18next-cli setup wizard!')
62
- console.log('Scanning your project for a recommended configuration...')
63
-
64
- const detectedConfig = await detectConfig()
65
- if (detectedConfig) {
66
- console.log('✅ Found a potential project structure. Using it for suggestions.')
67
- } else {
68
- console.log('Could not detect a project structure. Using standard defaults.')
69
- }
70
- if (typeof detectedConfig?.extract?.input === 'string') detectedConfig.extract.input = [detectedConfig?.extract?.input]
71
-
72
- // If heuristic detection returned a function for extract.output, don't use it as a prompt default.
73
- // Prompt defaults must be strings; leave undefined so the prompt falls back to a sensible default.
74
- if (detectedConfig && typeof detectedConfig.extract?.output === 'function') {
75
- delete (detectedConfig.extract as any).output
76
- }
77
-
78
- const answers = await inquirer.prompt([
79
- {
80
- type: 'list',
81
- name: 'fileType',
82
- message: 'What kind of configuration file do you want?',
83
- choices: ['TypeScript (i18next.config.ts)', 'JavaScript (i18next.config.js)'],
84
- },
85
- {
86
- type: 'input',
87
- name: 'locales',
88
- message: 'What locales does your project support? (comma-separated)',
89
- default: detectedConfig?.locales?.join(',') || 'en,de,fr',
90
- filter: (input: string) => input.split(',').map(s => s.trim()),
91
- },
92
- {
93
- type: 'input',
94
- name: 'input',
95
- message: 'What is the glob pattern for your source files?',
96
- default: detectedConfig?.extract?.input ? (detectedConfig.extract.input || [])[0] : 'src/**/*.{js,jsx,ts,tsx}',
97
- },
98
- {
99
- type: 'input',
100
- name: 'output',
101
- message: 'What is the path for your output resource files?',
102
- // ensure the default is a string (detectedConfig.extract.output may be a function)
103
- default: typeof detectedConfig?.extract?.output === 'string'
104
- ? detectedConfig!.extract!.output!
105
- : 'public/locales/{{language}}/{{namespace}}.json',
106
- },
107
- ])
108
-
109
- const isTypeScript = answers.fileType.includes('TypeScript')
110
- const isEsm = await isEsmProject()
111
- const fileName = isTypeScript ? 'i18next.config.ts' : 'i18next.config.js'
112
-
113
- const configObject = {
114
- locales: answers.locales,
115
- extract: {
116
- input: answers.input,
117
- output: answers.output,
118
- },
119
- }
120
-
121
- // Helper to serialize a JS value as a JS literal:
122
- function toJs (value: any, indent = 2, level = 0): string {
123
- const pad = (n: number) => ' '.repeat(n * indent)
124
- const currentPad = pad(level)
125
- const nextPad = pad(level + 1)
126
-
127
- if (value === null || typeof value === 'number' || typeof value === 'boolean') {
128
- return JSON.stringify(value)
129
- }
130
- if (typeof value === 'string') {
131
- return JSON.stringify(value) // keeps double quotes and proper escaping
132
- }
133
- if (Array.isArray(value)) {
134
- if (value.length === 0) return '[]'
135
- const items = value.map(v => `${nextPad}${toJs(v, indent, level + 1)}`).join(',\n')
136
- return `[\n${items}\n${currentPad}]`
137
- }
138
- if (typeof value === 'object') {
139
- const keys = Object.keys(value)
140
- if (keys.length === 0) return '{}'
141
- const entries = keys.map(key => {
142
- // Use unquoted key if it's a valid identifier otherwise JSON.stringify(key)
143
- const validId = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
144
- const printedKey = validId ? key : JSON.stringify(key)
145
- return `${nextPad}${printedKey}: ${toJs(value[key], indent, level + 1)}`
146
- }).join(',\n')
147
- return `{\n${entries}\n${currentPad}}`
148
- }
149
-
150
- // Fallback
151
- return JSON.stringify(value)
152
- }
153
-
154
- let fileContent = ''
155
- if (isTypeScript) {
156
- fileContent = `import { defineConfig } from 'i18next-cli';
157
-
158
- export default defineConfig(${toJs(configObject)});`
159
- } else if (isEsm) {
160
- fileContent = `import { defineConfig } from 'i18next-cli';
161
-
162
- /** @type {import('i18next-cli').I18nextToolkitConfig} */
163
- export default defineConfig(${toJs(configObject)});`
164
- } else { // CJS
165
- fileContent = `const { defineConfig } = require('i18next-cli');
166
-
167
- /** @type {import('i18next-cli').I18nextToolkitConfig} */
168
- module.exports = defineConfig(${toJs(configObject)});`
169
- }
170
-
171
- const outputPath = resolve(process.cwd(), fileName)
172
- await writeFile(outputPath, fileContent.trim())
173
-
174
- console.log(`✅ Configuration file created at: ${outputPath}`)
175
- }
package/src/linter.ts DELETED
@@ -1,345 +0,0 @@
1
- import { glob } from 'glob'
2
- import { readFile } from 'node:fs/promises'
3
- import { parse } from '@swc/core'
4
- import { extname } from 'node:path'
5
- import { EventEmitter } from 'node:events'
6
- import chalk from 'chalk'
7
- import ora from 'ora'
8
- import type { I18nextToolkitConfig } from './types'
9
-
10
- type LinterEventMap = {
11
- progress: [{
12
- message: string;
13
- }];
14
- done: [{
15
- success: boolean;
16
- message: string;
17
- files: Record<string, HardcodedString[]>;
18
- }];
19
- error: [error: Error];
20
- }
21
-
22
- export class Linter extends EventEmitter<LinterEventMap> {
23
- private config: I18nextToolkitConfig
24
-
25
- constructor (config: I18nextToolkitConfig) {
26
- super({ captureRejections: true })
27
- this.config = config
28
- }
29
-
30
- wrapError (error: unknown) {
31
- const prefix = 'Linter failed to run: '
32
- if (error instanceof Error) {
33
- if (error.message.startsWith(prefix)) {
34
- return error
35
- }
36
- const wrappedError = new Error(`${prefix}${error.message}`)
37
- wrappedError.stack = error.stack
38
- return wrappedError
39
- }
40
- return new Error(`${prefix}${String(error)}`)
41
- }
42
-
43
- async run () {
44
- const { config } = this
45
- try {
46
- this.emit('progress', { message: 'Finding source files to analyze...' })
47
- const defaultIgnore = ['node_modules/**']
48
- const userIgnore = Array.isArray(config.extract.ignore)
49
- ? config.extract.ignore
50
- : config.extract.ignore ? [config.extract.ignore] : []
51
-
52
- const sourceFiles = await glob(config.extract.input, {
53
- ignore: [...defaultIgnore, ...userIgnore]
54
- })
55
- this.emit('progress', { message: `Analyzing ${sourceFiles.length} source files...` })
56
- let totalIssues = 0
57
- const issuesByFile = new Map<string, HardcodedString[]>()
58
-
59
- for (const file of sourceFiles) {
60
- const code = await readFile(file, 'utf-8')
61
-
62
- // Determine parser options from file extension so .ts is not parsed as TSX
63
- const fileExt = extname(file).toLowerCase()
64
- const isTypeScriptFile = fileExt === '.ts' || fileExt === '.tsx' || fileExt === '.mts' || fileExt === '.cts'
65
- const isTSX = fileExt === '.tsx'
66
- const isJSX = fileExt === '.jsx'
67
-
68
- let ast: any
69
- try {
70
- ast = await parse(code, {
71
- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript',
72
- tsx: isTSX,
73
- jsx: isJSX,
74
- decorators: true
75
- })
76
- } catch (err) {
77
- // Some projects use JSX/TSX in .ts files. Try one fallback parse with tsx:true
78
- // if the original file was a .ts (not .tsx). If that still fails, emit error and continue.
79
- if (fileExt === '.ts' && !isTSX) {
80
- try {
81
- ast = await parse(code, {
82
- syntax: 'typescript',
83
- tsx: true,
84
- decorators: true
85
- })
86
- // optional: emit a progress message so consumers know a fallback happened
87
- this.emit('progress', { message: `Parsed ${file} using TSX fallback` })
88
- } catch (err2) {
89
- const wrapped = this.wrapError(err2)
90
- this.emit('error', wrapped)
91
- continue
92
- }
93
- } else {
94
- const wrapped = this.wrapError(err)
95
- this.emit('error', wrapped)
96
- continue
97
- }
98
- }
99
-
100
- const hardcodedStrings = findHardcodedStrings(ast, code, config)
101
-
102
- if (hardcodedStrings.length > 0) {
103
- totalIssues += hardcodedStrings.length
104
- issuesByFile.set(file, hardcodedStrings)
105
- }
106
- }
107
-
108
- const files = Object.fromEntries(issuesByFile.entries())
109
- const data = { success: totalIssues === 0, message: totalIssues > 0 ? `Linter found ${totalIssues} potential issues.` : 'No issues found.', files }
110
- this.emit('done', data)
111
- return data
112
- } catch (error) {
113
- const wrappedError = this.wrapError(error)
114
- this.emit('error', wrappedError)
115
- throw wrappedError
116
- }
117
- }
118
- }
119
-
120
- /**
121
- * Runs the i18next linter to detect hardcoded strings and other potential issues.
122
- *
123
- * This function performs static analysis on source files to identify:
124
- * - Hardcoded text strings in JSX elements
125
- * - Hardcoded strings in JSX attributes (like alt text, titles, etc.)
126
- * - Text that should be extracted for translation
127
- *
128
- * The linter respects configuration settings:
129
- * - Uses the same input patterns as the extractor
130
- * - Ignores content inside configured Trans components
131
- * - Skips technical content like script/style tags
132
- * - Identifies numeric values and interpolation syntax to avoid false positives
133
- *
134
- * @param config - The toolkit configuration with input patterns and component names
135
- *
136
- * @example
137
- * ```typescript
138
- * const config = {
139
- * extract: {
140
- * input: ['src/**\/*.{ts,tsx}'],
141
- * transComponents: ['Trans', 'Translation']
142
- * }
143
- * }
144
- *
145
- * await runLinter(config)
146
- * // Outputs issues found or success message
147
- * ```
148
- */
149
- export async function runLinter (config: I18nextToolkitConfig) {
150
- return new Linter(config).run()
151
- }
152
-
153
- export async function runLinterCli (config: I18nextToolkitConfig) {
154
- const linter = new Linter(config)
155
- const spinner = ora().start()
156
- linter.on('progress', (event) => {
157
- spinner.text = event.message
158
- })
159
- try {
160
- const { success, message, files } = await linter.run()
161
- if (!success) {
162
- spinner.fail(chalk.red.bold(message))
163
-
164
- // Print detailed report after spinner fails
165
- for (const [file, issues] of Object.entries(files)) {
166
- console.log(chalk.yellow(`\n${file}`))
167
- issues.forEach(({ text, line }) => {
168
- console.log(` ${chalk.gray(`${line}:`)} ${chalk.red('Error:')} Found hardcoded string: "${text}"`)
169
- })
170
- }
171
- process.exit(1)
172
- } else {
173
- spinner.succeed(chalk.green.bold(message))
174
- }
175
- } catch (error) {
176
- const wrappedError = linter.wrapError(error)
177
- spinner.fail(wrappedError.message)
178
- console.error(wrappedError)
179
- process.exit(1)
180
- }
181
- }
182
-
183
- /**
184
- * Represents a found hardcoded string with its location information.
185
- */
186
- interface HardcodedString {
187
- /** The hardcoded text content */
188
- text: string;
189
- /** Line number where the string was found */
190
- line: number;
191
- }
192
-
193
- const isUrlOrPath = (text: string) => /^(https|http|\/\/|^\/)/.test(text)
194
-
195
- /**
196
- * Analyzes an AST to find potentially hardcoded strings that should be translated.
197
- *
198
- * This function traverses the syntax tree looking for:
199
- * 1. JSX text nodes with translatable content
200
- * 2. String literals in JSX attributes that might need translation
201
- *
202
- * It applies several filters to reduce false positives:
203
- * - Ignores content inside Trans components (already handled)
204
- * - Skips script and style tag content (technical, not user-facing)
205
- * - Filters out numeric values (usually not translatable)
206
- * - Ignores interpolation syntax starting with `{{`
207
- * - Filters out ellipsis/spread operator notation `...`
208
- * - Only reports non-empty, trimmed strings
209
- *
210
- * @param ast - The parsed AST to analyze
211
- * @param code - Original source code for line number calculation
212
- * @param config - Configuration containing Trans component names
213
- * @returns Array of found hardcoded strings with location info
214
- *
215
- * @example
216
- * ```typescript
217
- * const issues = findHardcodedStrings(ast, sourceCode, config)
218
- * issues.forEach(issue => {
219
- * console.log(`Line ${issue.line}: "${issue.text}"`)
220
- * })
221
- * ```
222
- */
223
- function findHardcodedStrings (ast: any, code: string, config: I18nextToolkitConfig): HardcodedString[] {
224
- const issues: HardcodedString[] = []
225
- // A list of AST nodes that have been identified as potential issues.
226
- const nodesToLint: any[] = []
227
-
228
- const getLineNumber = (pos: number): number => {
229
- return code.substring(0, pos).split('\n').length
230
- }
231
-
232
- const transComponents = config.extract.transComponents || ['Trans']
233
- const defaultIgnoredAttributes = ['className', 'key', 'id', 'style', 'href', 'i18nKey', 'defaults', 'type', 'target']
234
- const defaultIgnoredTags = ['script', 'style', 'code']
235
- const customIgnoredTags = config.extract.ignoredTags || []
236
- const allIgnoredTags = new Set([...transComponents, ...defaultIgnoredTags, ...customIgnoredTags])
237
- const customIgnoredAttributes = config.extract.ignoredAttributes || []
238
- const ignoredAttributes = new Set([...defaultIgnoredAttributes, ...customIgnoredAttributes])
239
-
240
- // Helper: robustly extract a JSX element name from different node shapes
241
- const extractJSXName = (node: any): string | null => {
242
- if (!node) return null
243
- // node might be JSXOpeningElement / JSXSelfClosingElement (has .name)
244
- const nameNode = node.name ?? node.opening?.name ?? node.opening?.name
245
- if (!nameNode) {
246
- // maybe this node is a full JSXElement with opening.name
247
- if (node.opening?.name) return extractJSXName({ name: node.opening.name })
248
- return null
249
- }
250
-
251
- const fromIdentifier = (n: any): string | null => {
252
- if (!n) return null
253
- if (n.type === 'JSXIdentifier' && (n.name || n.value)) return (n.name ?? n.value)
254
- if (n.type === 'Identifier' && (n.name || n.value)) return (n.name ?? n.value)
255
- if (n.type === 'JSXMemberExpression') {
256
- const object = fromIdentifier(n.object)
257
- const property = fromIdentifier(n.property)
258
- return object && property ? `${object}.${property}` : (property ?? object)
259
- }
260
- // fallback attempts
261
- return n.name ?? n.value ?? n.property?.name ?? n.property?.value ?? null
262
- }
263
-
264
- return fromIdentifier(nameNode)
265
- }
266
-
267
- // Helper: return true if any JSX ancestor is in the ignored tags set
268
- const isWithinIgnoredElement = (ancestors: any[]): boolean => {
269
- for (let i = ancestors.length - 1; i >= 0; i--) {
270
- const an = ancestors[i]
271
- if (!an || typeof an !== 'object') continue
272
- if (an.type === 'JSXElement' || an.type === 'JSXOpeningElement' || an.type === 'JSXSelfClosingElement') {
273
- const name = extractJSXName(an)
274
- if (name && allIgnoredTags.has(name)) return true
275
- }
276
- }
277
- return false
278
- }
279
-
280
- // --- PHASE 1: Collect all potentially problematic nodes ---
281
- const walk = (node: any, ancestors: any[]) => {
282
- if (!node || typeof node !== 'object') return
283
-
284
- const currentAncestors = [...ancestors, node]
285
-
286
- if (node.type === 'JSXText') {
287
- const isIgnored = isWithinIgnoredElement(currentAncestors)
288
-
289
- if (!isIgnored) {
290
- const text = node.value.trim()
291
- // Filter out: empty strings, single chars, URLs, numbers, interpolations, and ellipsis
292
- if (text && text.length > 1 && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text)) && !text.startsWith('{{')) {
293
- nodesToLint.push(node) // Collect the node
294
- }
295
- }
296
- }
297
-
298
- if (node.type === 'StringLiteral') {
299
- const parent = currentAncestors[currentAncestors.length - 2]
300
- // Determine whether this attribute is inside any ignored element (handles nested Trans etc.)
301
- const insideIgnored = isWithinIgnoredElement(currentAncestors)
302
-
303
- if (parent?.type === 'JSXAttribute' && !ignoredAttributes.has(parent.name.value) && !insideIgnored) {
304
- const text = node.value.trim()
305
- // Filter out: empty strings, URLs, numbers, and ellipsis
306
- if (text && text !== '...' && !isUrlOrPath(text) && isNaN(Number(text))) {
307
- nodesToLint.push(node) // Collect the node
308
- }
309
- }
310
- }
311
-
312
- // Recurse into children
313
- for (const key of Object.keys(node)) {
314
- if (key === 'span') continue
315
- const child = node[key]
316
- if (Array.isArray(child)) {
317
- child.forEach(item => walk(item, currentAncestors))
318
- } else if (child && typeof child === 'object') {
319
- walk(child, currentAncestors)
320
- }
321
- }
322
- }
323
-
324
- walk(ast, []) // Run the walk to collect nodes
325
-
326
- // --- PHASE 2: Find line numbers using a tracked search on the raw source code ---
327
- let lastSearchIndex = 0
328
- for (const node of nodesToLint) {
329
- // For StringLiterals, the `raw` property includes the quotes ("..."), which is
330
- // much more unique for searching than the plain `value`.
331
- const searchText = node.raw ?? node.value
332
-
333
- const position = code.indexOf(searchText, lastSearchIndex)
334
-
335
- if (position > -1) {
336
- issues.push({
337
- text: node.value.trim(),
338
- line: getLineNumber(position),
339
- })
340
- lastSearchIndex = position + searchText.length
341
- }
342
- }
343
-
344
- return issues
345
- }