i18next-cli 1.24.13 → 1.24.15

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 (41) hide show
  1. package/dist/cjs/cli.js +1 -1
  2. package/dist/cjs/rename-key.js +1 -1
  3. package/dist/esm/cli.js +1 -1
  4. package/dist/esm/rename-key.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/CHANGELOG.md +0 -599
  9. package/src/cli.ts +0 -283
  10. package/src/config.ts +0 -215
  11. package/src/extractor/core/ast-visitors.ts +0 -259
  12. package/src/extractor/core/extractor.ts +0 -250
  13. package/src/extractor/core/key-finder.ts +0 -142
  14. package/src/extractor/core/translation-manager.ts +0 -750
  15. package/src/extractor/index.ts +0 -7
  16. package/src/extractor/parsers/ast-utils.ts +0 -87
  17. package/src/extractor/parsers/call-expression-handler.ts +0 -793
  18. package/src/extractor/parsers/comment-parser.ts +0 -424
  19. package/src/extractor/parsers/expression-resolver.ts +0 -391
  20. package/src/extractor/parsers/jsx-handler.ts +0 -488
  21. package/src/extractor/parsers/jsx-parser.ts +0 -1463
  22. package/src/extractor/parsers/scope-manager.ts +0 -445
  23. package/src/extractor/plugin-manager.ts +0 -116
  24. package/src/extractor.ts +0 -15
  25. package/src/heuristic-config.ts +0 -92
  26. package/src/index.ts +0 -22
  27. package/src/init.ts +0 -175
  28. package/src/linter.ts +0 -345
  29. package/src/locize.ts +0 -263
  30. package/src/migrator.ts +0 -208
  31. package/src/rename-key.ts +0 -398
  32. package/src/status.ts +0 -380
  33. package/src/syncer.ts +0 -133
  34. package/src/types-generator.ts +0 -139
  35. package/src/types.ts +0 -577
  36. package/src/utils/default-value.ts +0 -45
  37. package/src/utils/file-utils.ts +0 -167
  38. package/src/utils/funnel-msg-tracker.ts +0 -84
  39. package/src/utils/logger.ts +0 -36
  40. package/src/utils/nested-object.ts +0 -135
  41. package/src/utils/validation.ts +0 -72
@@ -1,167 +0,0 @@
1
- import { mkdir, readFile, writeFile, access } from 'node:fs/promises'
2
- import { dirname, extname, resolve, normalize } from 'node:path'
3
- import { createJiti } from 'jiti'
4
- import type { I18nextToolkitConfig } from '../types'
5
- import { getTsConfigAliases } from '../config'
6
-
7
- /**
8
- * Ensures that the directory for a given file path exists.
9
- * Creates all necessary parent directories recursively if they don't exist.
10
- *
11
- * @param filePath - The file path for which to ensure the directory exists
12
- *
13
- * @example
14
- * ```typescript
15
- * await ensureDirectoryExists('/path/to/nested/file.json')
16
- * // Creates /path/to/nested/ directory if it doesn't exist
17
- * ```
18
- */
19
- export async function ensureDirectoryExists (filePath: string): Promise<void> {
20
- const dir = dirname(filePath)
21
- await mkdir(dir, { recursive: true })
22
- }
23
-
24
- /**
25
- * Reads a file asynchronously and returns its content as a UTF-8 string.
26
- *
27
- * @param filePath - The path to the file to read
28
- * @returns Promise resolving to the file content as a string
29
- *
30
- * @example
31
- * ```typescript
32
- * const content = await readFileAsync('./config.json')
33
- * const config = JSON.parse(content)
34
- * ```
35
- */
36
- export async function readFileAsync (filePath: string): Promise<string> {
37
- return await readFile(filePath, 'utf-8')
38
- }
39
-
40
- /**
41
- * Writes data to a file asynchronously.
42
- *
43
- * @param filePath - The path where to write the file
44
- * @param data - The string data to write to the file
45
- *
46
- * @example
47
- * ```typescript
48
- * const jsonData = JSON.stringify({ key: 'value' }, null, 2)
49
- * await writeFileAsync('./output.json', jsonData)
50
- * ```
51
- */
52
- export async function writeFileAsync (filePath: string, data: string): Promise<void> {
53
- await writeFile(filePath, data)
54
- }
55
-
56
- /**
57
- * Resolve an output template (string or function) into an actual path string.
58
- *
59
- * - If `outputTemplate` is a function, call it with (language, namespace)
60
- * - If it's a string, replace placeholders:
61
- * - {{language}} or {{lng}} -> language
62
- * - {{namespace}} -> namespace (or removed if namespace is undefined)
63
- * - Normalizes duplicate slashes and returns a platform-correct path.
64
- */
65
- export function getOutputPath (
66
- outputTemplate: string | ((language: string, namespace?: string) => string) | undefined,
67
- language: string,
68
- namespace?: string
69
- ): string {
70
- if (!outputTemplate) {
71
- // Fallback to a sensible default
72
- return normalize(`locales/${language}/${namespace ?? 'translation'}.json`)
73
- }
74
-
75
- if (typeof outputTemplate === 'function') {
76
- try {
77
- const result = String(outputTemplate(language, namespace))
78
- return normalize(result.replace(/\/\/+/g, '/'))
79
- } catch {
80
- // If user function throws, fallback to default path
81
- return normalize(`locales/${language}/${namespace ?? 'translation'}.json`)
82
- }
83
- }
84
-
85
- // It's a string template
86
- let out = String(outputTemplate)
87
- out = out.replace(/\{\{language\}\}|\{\{lng\}\}/g, language)
88
-
89
- if (namespace !== undefined && namespace !== null) {
90
- out = out.replace(/\{\{namespace\}\}/g, namespace)
91
- } else {
92
- // remove any occurrences of /{{namespace}} or {{namespace}} (keeping surrounding slashes tidy)
93
- out = out.replace(/\/?\{\{namespace\}\}/g, '')
94
- }
95
-
96
- // collapse duplicate slashes and normalize to platform-specific separators
97
- out = out.replace(/\/\/+/g, '/')
98
- return normalize(out)
99
- }
100
-
101
- /**
102
- * Dynamically loads a translation file, supporting .json, .js, and .ts formats.
103
- * @param filePath - The path to the translation file.
104
- * @returns The parsed content of the file, or null if not found or failed to parse.
105
- */
106
- export async function loadTranslationFile (filePath: string): Promise<Record<string, any> | null> {
107
- const fullPath = resolve(process.cwd(), filePath)
108
- try {
109
- await access(fullPath)
110
- } catch {
111
- return null // File doesn't exist
112
- }
113
-
114
- try {
115
- const ext = extname(fullPath).toLowerCase()
116
-
117
- if (ext === '.json') {
118
- const content = await readFile(fullPath, 'utf-8')
119
- return JSON.parse(content)
120
- } else if (ext === '.ts' || ext === '.js') {
121
- // Load TypeScript path aliases for proper module resolution
122
- const aliases = await getTsConfigAliases()
123
-
124
- const jiti = createJiti(process.cwd(), {
125
- alias: aliases,
126
- interopDefault: true,
127
- })
128
-
129
- const module = await jiti.import(fullPath, { default: true }) as unknown
130
- return module as Record<string, any> | null
131
- }
132
-
133
- return null // Unsupported file type
134
- } catch (error) {
135
- console.warn(`Could not parse translation file ${filePath}:`, error)
136
- return null
137
- }
138
- }
139
-
140
- /**
141
- * Serializes a translation object into a string based on the desired format.
142
- * @param data - The translation data object.
143
- * @param format - The desired output format ('json', 'js-esm', etc.).
144
- * @param indentation - The number of spaces for indentation.
145
- * @returns The serialized file content as a string.
146
- */
147
- export function serializeTranslationFile (
148
- data: Record<string, any>,
149
- format: I18nextToolkitConfig['extract']['outputFormat'] = 'json',
150
- indentation: number | string = 2
151
- ): string {
152
- const jsonString = JSON.stringify(data, null, indentation)
153
-
154
- switch (format) {
155
- case 'js':
156
- case 'js-esm':
157
- return `export default ${jsonString};\n`
158
- case 'js-cjs':
159
- return `module.exports = ${jsonString};\n`
160
- case 'ts':
161
- // Using `as const` provides better type inference for TypeScript users
162
- return `export default ${jsonString} as const;\n`
163
- case 'json':
164
- default:
165
- return `${jsonString}\n`
166
- }
167
- }
@@ -1,84 +0,0 @@
1
- import { join } from 'node:path'
2
- import { tmpdir } from 'node:os'
3
- import { readFile, writeFile } from 'node:fs/promises'
4
-
5
- /**
6
- * In-memory cache to track which funnel messages have been shown in the current session.
7
- */
8
- const hasLocizeFunnelBeenShown: Record<string, boolean> = {}
9
-
10
- /**
11
- * Path to the persistent file that stores the last time each funnel message was shown.
12
- * Stored in the OS temporary directory to persist across CLI sessions.
13
- */
14
- const LAST_FUNNEL_FILE = join(tmpdir(), 'i18next-cli-last-funnel-message-shown.json') // Store in OS temp dir
15
-
16
- /**
17
- * Cooldown period in milliseconds before a funnel message can be shown again.
18
- * Currently set to 24 hours.
19
- */
20
- const TIP_COOLDOWN_MS = 24 * 60 * 60 * 1000 // 24 hours
21
-
22
- /**
23
- * Determines whether a funnel message should be shown to the user.
24
- *
25
- * A funnel message will not be shown if:
26
- * - It has already been shown in the current session (in-memory cache)
27
- * - It was shown within the last 24 hours (persistent file cache)
28
- *
29
- * @param funnelMessage - The unique identifier for the funnel message
30
- * @returns Promise that resolves to true if the message should be shown, false otherwise
31
- */
32
- export async function shouldShowFunnel (funnelMessage: string): Promise<boolean> {
33
- if (hasLocizeFunnelBeenShown[funnelMessage]) return false
34
-
35
- try {
36
- const content = await readFile(LAST_FUNNEL_FILE, 'utf-8')
37
- const cnt: Record<string, number> = JSON.parse(content)
38
- if (Date.now() - (cnt[funnelMessage] || 0) < TIP_COOLDOWN_MS) {
39
- return false // Less than 24 hours since last shown
40
- }
41
- } catch (e) {
42
- // File doesn't exist or is invalid, assume it's okay to show the tip
43
- }
44
- return true
45
- }
46
-
47
- /**
48
- * Records that a funnel message has been shown to the user.
49
- *
50
- * Updates both the in-memory cache and the persistent file cache with the current timestamp.
51
- * This prevents the message from being shown again within the cooldown period.
52
- *
53
- * @param funnelMessage - The unique identifier for the funnel message that was shown
54
- * @returns Promise that resolves when the record has been updated
55
- */
56
- export async function recordFunnelShown (funnelMessage: string): Promise<void> {
57
- try {
58
- hasLocizeFunnelBeenShown[funnelMessage] = true
59
- let data: Record<string, number> = {}
60
- try {
61
- const existing = await readFile(LAST_FUNNEL_FILE, 'utf-8')
62
- data = JSON.parse(existing) as Record<string, number>
63
- } catch (err) {
64
- // ignore, we'll create a new file
65
- }
66
- data[funnelMessage] = Date.now()
67
- await writeFile(LAST_FUNNEL_FILE, JSON.stringify(data))
68
- } catch (e) {
69
- // Ignore errors here, it's just a best-effort cache
70
- }
71
- }
72
-
73
- /**
74
- * Resets the in-memory cache for a specific funnel message.
75
- *
76
- * This function is intended for testing purposes only. It clears the session cache
77
- * but does not affect the persistent file cache.
78
- *
79
- * @param funnelMessage - The unique identifier for the funnel message to reset
80
- */
81
- // just for the tests
82
- export function reset (funnelMessage: string) {
83
- delete hasLocizeFunnelBeenShown[funnelMessage]
84
- }
@@ -1,36 +0,0 @@
1
- import type { Logger } from '../types'
2
-
3
- /**
4
- * Default console-based logger implementation for the i18next toolkit.
5
- * Provides basic logging functionality with different severity levels.
6
- *
7
- * @example
8
- * ```typescript
9
- * const logger = new ConsoleLogger()
10
- * logger.info('Extraction started')
11
- * logger.warn('Deprecated configuration option used')
12
- * logger.error('Failed to parse file')
13
- * ```
14
- */
15
- export class ConsoleLogger implements Logger {
16
- /**
17
- * Logs an informational message to the console.
18
- *
19
- * @param message - The message to log
20
- */
21
- info (message: string): void { console.log(message) }
22
-
23
- /**
24
- * Logs a warning message to the console.
25
- *
26
- * @param message - The warning message to log
27
- */
28
- warn (message: string): void { console.warn(message) }
29
-
30
- /**
31
- * Logs an error message to the console.
32
- *
33
- * @param message - The error message to log
34
- */
35
- error (message: string): void { console.error(message) }
36
- }
@@ -1,135 +0,0 @@
1
- /**
2
- * Sets a nested value in an object using a key path and separator.
3
- * Creates intermediate objects as needed.
4
- *
5
- * @param obj - The target object to modify
6
- * @param path - The key path (e.g., 'user.profile.name')
7
- * @param value - The value to set
8
- * @param keySeparator - The separator to use for splitting the path, or false for flat keys
9
- *
10
- * @example
11
- * ```typescript
12
- * const obj = {}
13
- * setNestedValue(obj, 'user.profile.name', 'John', '.')
14
- * // Result: { user: { profile: { name: 'John' } } }
15
- *
16
- * // With flat keys
17
- * setNestedValue(obj, 'user.name', 'Jane', false)
18
- * // Result: { 'user.name': 'Jane' }
19
- * ```
20
- */
21
- export function setNestedValue (
22
- obj: Record<string, any>,
23
- path: string,
24
- value: any,
25
- keySeparator: string | false
26
- ): void {
27
- if (keySeparator === false) {
28
- obj[path] = value
29
- return
30
- }
31
-
32
- const keys = path.split(keySeparator)
33
- let current = obj
34
-
35
- for (let i = 0; i < keys.length; i++) {
36
- const key = keys[i]
37
- const isLastKey = i === keys.length - 1
38
-
39
- if (isLastKey) {
40
- // We've reached the end of the path, set the value.
41
- current[key] = value
42
- return
43
- }
44
-
45
- const nextLevel = current[key]
46
-
47
- // Check for a conflict: the path requires an object, but a primitive exists.
48
- if (nextLevel !== undefined && (typeof nextLevel !== 'object' || nextLevel === null)) {
49
- // Conflict detected. The parent path is already a leaf node.
50
- // We must set the entire original path as a flat key on the root object.
51
- obj[path] = value
52
- return // Stop processing to prevent overwriting the parent.
53
- }
54
-
55
- // If the path doesn't exist, create an empty object to continue.
56
- if (nextLevel === undefined) {
57
- current[key] = {}
58
- }
59
-
60
- current = current[key]
61
- }
62
- }
63
-
64
- /**
65
- * Retrieves a nested value from an object using a key path and separator.
66
- *
67
- * @param obj - The object to search in
68
- * @param path - The key path (e.g., 'user.profile.name')
69
- * @param keySeparator - The separator to use for splitting the path, or false for flat keys
70
- * @returns The found value or undefined if not found
71
- *
72
- * @example
73
- * ```typescript
74
- * const obj = { user: { profile: { name: 'John' } } }
75
- * const name = getNestedValue(obj, 'user.profile.name', '.')
76
- * // Returns: 'John'
77
- *
78
- * // With flat keys
79
- * const flatObj = { 'user.name': 'Jane' }
80
- * const name = getNestedValue(flatObj, 'user.name', false)
81
- * // Returns: 'Jane'
82
- * ```
83
- */
84
- export function getNestedValue (
85
- obj: Record<string, any>,
86
- path: string,
87
- keySeparator: string | false
88
- ): any {
89
- if (keySeparator === false) {
90
- return obj[path]
91
- }
92
- return path.split(keySeparator).reduce((acc, key) => acc && acc[key], obj)
93
- }
94
-
95
- /**
96
- * Extracts all nested keys from an object, optionally with a prefix.
97
- * Recursively traverses the object structure to build a flat list of key paths.
98
- *
99
- * @param obj - The object to extract keys from
100
- * @param keySeparator - The separator to use for joining keys, or false for flat keys
101
- * @param prefix - Optional prefix to prepend to all keys
102
- * @returns Array of all nested key paths
103
- *
104
- * @example
105
- * ```typescript
106
- * const obj = {
107
- * user: {
108
- * profile: { name: 'John', age: 30 },
109
- * settings: { theme: 'dark' }
110
- * }
111
- * }
112
- *
113
- * const keys = getNestedKeys(obj, '.')
114
- * // Returns: ['user.profile.name', 'user.profile.age', 'user.settings.theme']
115
- *
116
- * // With flat keys
117
- * const flatObj = { 'user.name': 'Jane', 'user.age': 25 }
118
- * const flatKeys = getNestedKeys(flatObj, false)
119
- * // Returns: ['user.name', 'user.age']
120
- * ```
121
- */
122
- export function getNestedKeys (obj: object, keySeparator: string | false, prefix = ''): string[] {
123
- if (keySeparator === false) {
124
- return Object.keys(obj)
125
- }
126
- return Object.entries(obj).reduce((acc, [key, value]) => {
127
- const newKey = prefix ? `${prefix}${keySeparator}${key}` : key
128
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
129
- acc.push(...getNestedKeys(value, keySeparator, newKey))
130
- } else {
131
- acc.push(newKey)
132
- }
133
- return acc
134
- }, [] as string[])
135
- }
@@ -1,72 +0,0 @@
1
- import type { I18nextToolkitConfig } from '../types'
2
-
3
- /**
4
- * Validates the extractor configuration to ensure required fields are present and properly formatted.
5
- *
6
- * This function performs the following validations:
7
- * - Ensures extract.input is specified and non-empty
8
- * - Ensures extract.output is specified
9
- * - Ensures locales array is specified and non-empty
10
- * - Ensures extract.output contains the required {{language}} placeholder
11
- *
12
- * @param config - The i18next toolkit configuration object to validate
13
- *
14
- * @throws {ExtractorError} When any validation rule fails
15
- *
16
- * @example
17
- * ```typescript
18
- * try {
19
- * validateExtractorConfig(config)
20
- * console.log('Configuration is valid')
21
- * } catch (error) {
22
- * console.error('Invalid configuration:', error.message)
23
- * }
24
- * ```
25
- */
26
- export function validateExtractorConfig (config: I18nextToolkitConfig): void {
27
- if (!config.extract.input?.length) {
28
- throw new ExtractorError('extract.input must be specified and non-empty')
29
- }
30
-
31
- if (!config.extract.output) {
32
- throw new ExtractorError('extract.output must be specified')
33
- }
34
-
35
- if (!config.locales?.length) {
36
- throw new ExtractorError('locales must be specified and non-empty')
37
- }
38
-
39
- // If output is a function, we accept it (user is responsible for producing a valid path).
40
- if (typeof config.extract.output === 'string') {
41
- if (!config.extract.output.includes('{{language}}') && !config.extract.output.includes('{{lng}}')) {
42
- throw new ExtractorError('extract.output must contain {{language}} placeholder')
43
- }
44
- }
45
- }
46
-
47
- /**
48
- * Custom error class for extraction-related errors.
49
- * Provides additional context like file path and underlying cause.
50
- *
51
- * @example
52
- * ```typescript
53
- * throw new ExtractorError('Failed to parse file', 'src/component.tsx', syntaxError)
54
- * ```
55
- */
56
- export class ExtractorError extends Error {
57
- /**
58
- * Creates a new ExtractorError with optional file context and cause.
59
- *
60
- * @param message - The error message
61
- * @param file - Optional file path where the error occurred
62
- * @param cause - Optional underlying error that caused this error
63
- */
64
- constructor (
65
- message: string,
66
- public readonly file?: string,
67
- public readonly cause?: Error
68
- ) {
69
- super(file ? `${message} in file ${file}` : message)
70
- this.name = 'ExtractorError'
71
- }
72
- }