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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0](https://github.com/i18next/i18next-cli/compare/v0.9.0...v1.0.0) - 2025-xx-yy
|
|
9
|
+
|
|
10
|
+
- not yet released
|
|
11
|
+
|
|
12
|
+
## [0.9.0] - 2025-09-25
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
This is the initial public release of `i18next-cli`, a complete, high-performance replacement for `i18next-parser` and `i18next-scanner`.
|
|
17
|
+
|
|
18
|
+
#### Core Engine & Extractor
|
|
19
|
+
- Initial high-performance, SWC-based parsing engine for JavaScript and TypeScript.
|
|
20
|
+
- Advanced, scope-aware AST analysis for intelligent key extraction.
|
|
21
|
+
- Support for `t()` functions, `<Trans>` components, `useTranslation` hooks (including `keyPrefix` and aliasing), and `getFixedT`.
|
|
22
|
+
- Handles complex i18next features: namespaces (via ns-separator, options, and hooks), plurals, and context.
|
|
23
|
+
- Support for the type-safe selector API (`t($=>$.key.path)`).
|
|
24
|
+
- Extraction from commented-out code to support documentation-driven keys.
|
|
25
|
+
|
|
26
|
+
#### Commands
|
|
27
|
+
- **`init`**: Interactive wizard to create a new configuration file (`i18next.config.ts` or `.js`).
|
|
28
|
+
- **`extract`**: Extracts keys and updates translation files, with `--watch` and `--ci` modes.
|
|
29
|
+
- **`types`**: Generates TypeScript definitions for type-safe i18next usage and autocompletion.
|
|
30
|
+
- **`sync`**: Synchronizes secondary language files with a primary language file, adding missing keys and removing unused ones.
|
|
31
|
+
- **`lint`**: Lints the codebase for potential issues like hardcoded strings.
|
|
32
|
+
- **`status`**: Displays a project health dashboard with a summary view and a detailed, key-by-key view (`status [locale]`).
|
|
33
|
+
- **`migrate-config`**: Provides an automatic migration path from a legacy `i18next-parser.config.js`.
|
|
34
|
+
- **`locize-*`**: Full suite of commands (`sync`, `download`, `migrate`) for seamless integration with the locize TMS, including an interactive setup for credentials.
|
|
35
|
+
|
|
36
|
+
#### Configuration & DX
|
|
37
|
+
- **Zero-Config Mode**: `status` and `lint` commands work out-of-the-box on most projects by heuristically detecting the project structure.
|
|
38
|
+
- **Typed Configuration**: Fully-typed config file (`i18next.config.ts`) with a `defineConfig` helper.
|
|
39
|
+
- **Robust TS Support**: On-the-fly TypeScript config file loading (`.ts`) is supported in consumer projects via `jiti`.
|
|
40
|
+
- **Dynamic Key Support**: `preservePatterns` option to support dynamic keys.
|
|
41
|
+
- **Configurable Linter**: The linter can be customized with an `ignoredAttributes` option.
|
|
42
|
+
- **Polished UX**: Consistent `ora` spinners provide clear feedback on all asynchronous commands.
|
|
43
|
+
- **Dual CJS/ESM Support**: Modern package structure for broad compatibility.
|
|
44
|
+
|
|
45
|
+
#### Plugin System
|
|
46
|
+
- Initial plugin architecture with `setup`, `onLoad`, `onVisitNode`, and `onEnd` hooks, allowing for custom extraction logic and support for other file types (e.g., HTML, Handlebars).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present i18next
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# i18next-cli š
|
|
2
|
+
|
|
3
|
+
A unified, high-performance i18next CLI toolchain, powered by SWC.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/i18next/i18next-cli/actions?query=workflow%3Anode)
|
|
6
|
+
[](https://www.npmjs.com/package/i18next-cli)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
`i18next-cli` is a complete reimagining of the static analysis toolchain for the i18next ecosystem. It consolidates key extraction, type safety generation, locale syncing, linting, and cloud integrations into a single, cohesive, and blazing-fast CLI.
|
|
11
|
+
|
|
12
|
+
> ### š Try it Now - Zero Config!
|
|
13
|
+
> You can get an instant analysis of your existing i18next project **without any configuration**. Just run this command in your repository's root directory:
|
|
14
|
+
>
|
|
15
|
+
> ```bash
|
|
16
|
+
> npx i18next-cli status
|
|
17
|
+
> ```
|
|
18
|
+
> Or find hardcoded strings:
|
|
19
|
+
>
|
|
20
|
+
> ```bash
|
|
21
|
+
> npx i18next-cli lint
|
|
22
|
+
> ```
|
|
23
|
+
|
|
24
|
+
## Why i18next-cli?
|
|
25
|
+
|
|
26
|
+
`i18next-cli` is built from the ground up to meet the demands of modern web development.
|
|
27
|
+
|
|
28
|
+
- **š Performance:** By leveraging a native Rust-based parser (SWC), it delivers orders-of-magnitude faster performance than JavaScript-based parsers.
|
|
29
|
+
- **š§ Intelligence:** A stateful, scope-aware analyzer correctly understands complex patterns like `useTranslation('ns1', { keyPrefix: '...' })`, `getFixedT`, and aliased `t` functions, minimizing the need for manual workarounds.
|
|
30
|
+
- **ā
Unified Workflow:** One tool, one configuration file, one integrated workflow. It replaces various syncing scripts.
|
|
31
|
+
- **š Extensibility:** A modern plugin architecture allows the tool to adapt to any framework or custom workflow.
|
|
32
|
+
- **š§āš» Developer Experience:** A fully-typed configuration file, live `--watch` modes, CLI output, and a migration from legacy tools.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Key Extraction**: Extract translation keys from JavaScript/TypeScript files with advanced AST analysis.
|
|
37
|
+
- **Type Safety**: Generate TypeScript definitions for full autocomplete and type safety.
|
|
38
|
+
- **Locale Synchronization**: Keep all language files in sync with your primary language.
|
|
39
|
+
- **Accurate Code Linting**: Detect hardcoded strings with high precision and configurable rules.
|
|
40
|
+
- **Translation Status**: Get a high-level overview or a detailed, key-by-key report of your project's translation completeness.
|
|
41
|
+
- **Plugin System**: Extensible architecture for custom extraction patterns and file types (e.g., HTML, Handlebars).
|
|
42
|
+
- **Legacy Migration**: Automatic migration from `i18next-parser` configurations.
|
|
43
|
+
- **Cloud Integration**: Seamless integration with the [locize](https://locize.com) translation management platform.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install --save-dev i18next-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### 1. Initialize Configuration
|
|
54
|
+
|
|
55
|
+
Create a configuration interactively:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx i18next-cli init
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or manually create `i18next.config.ts` in your project root:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { defineConfig } from 'i18next-cli';
|
|
65
|
+
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
locales: ['en', 'de'],
|
|
68
|
+
extract: {
|
|
69
|
+
input: ['src/**/*.{js,jsx,ts,tsx}'],
|
|
70
|
+
output: 'public/locales/{{language}}/{{namespace}}.json',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Check your Translation Status
|
|
76
|
+
|
|
77
|
+
Get an overview of your project's localization health:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx i18next-cli status
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Extract Translation Keys
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx i18next-cli extract
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. Generate Types (Optional)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx i18next-cli types
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Commands
|
|
96
|
+
|
|
97
|
+
### `init`
|
|
98
|
+
Interactive setup wizard to create your configuration file.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx i18next-cli init
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `extract`
|
|
105
|
+
Parses source files, extracts keys, and updates your JSON translation files.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx i18next-cli extract [options]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Options:**
|
|
112
|
+
- `--watch, -w`: Re-run automatically when files change
|
|
113
|
+
- `--ci`: Exit with non-zero status if any files are updated (for CI/CD)
|
|
114
|
+
|
|
115
|
+
**Examples:**
|
|
116
|
+
```bash
|
|
117
|
+
# One-time extraction
|
|
118
|
+
npx i18next-cli extract
|
|
119
|
+
|
|
120
|
+
# Watch mode for development
|
|
121
|
+
npx i18next-cli extract --watch
|
|
122
|
+
|
|
123
|
+
# CI mode (fails if files changed)
|
|
124
|
+
npx i18next-cli extract --ci
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `status [locale]`
|
|
128
|
+
|
|
129
|
+
Displays a health check of your project's translation status. Can run without a config file.
|
|
130
|
+
|
|
131
|
+
- Run `npx i18next-cli status` for a high-level summary.
|
|
132
|
+
- Run `npx i18next-cli status <locale>` for a detailed, key-by-key report grouped by namespace.
|
|
133
|
+
|
|
134
|
+
This command provides:
|
|
135
|
+
- Total number of translation keys found in your source code
|
|
136
|
+
- Translation progress for each secondary language with visual progress bars
|
|
137
|
+
- Percentage and key counts for easy tracking
|
|
138
|
+
|
|
139
|
+
### `types`
|
|
140
|
+
Generates TypeScript definitions from your translation files for full type-safety and autocompletion.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx i18next-cli types [options]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Options:**
|
|
147
|
+
- `--watch, -w`: Re-run automatically when translation files change
|
|
148
|
+
|
|
149
|
+
### `sync`
|
|
150
|
+
Synchronizes secondary language files against your primary language file, adding missing keys and removing extraneous ones.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npx i18next-cli sync
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `lint`
|
|
157
|
+
Analyzes your source code for internationalization issues like hardcoded strings. Can run without a config file.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx i18next-cli lint
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `migrate-config`
|
|
164
|
+
Automatically migrates a legacy `i18next-parser.config.js` file to the new `i18next.config.ts` format.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx i18next-cli migrate-config
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Locize Integration
|
|
171
|
+
|
|
172
|
+
**Prerequisites:** The locize commands require `locize-cli` to be installed:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Install globally (recommended)
|
|
176
|
+
npm install -g locize-cli
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Sync translations with the Locize translation management platform:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Download translations from Locize
|
|
183
|
+
npx i18next-cli locize-download
|
|
184
|
+
|
|
185
|
+
# Upload/sync translations to Locize
|
|
186
|
+
npx i18next-cli locize-sync
|
|
187
|
+
|
|
188
|
+
# Migrate local translations to Locize
|
|
189
|
+
npx i18next-cli locize-migrate
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Locize Command Options:**
|
|
193
|
+
|
|
194
|
+
The `locize-sync` command supports additional options:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npx i18next-cli locize-sync [options]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Options:**
|
|
201
|
+
- `--update-values`: Update values of existing translations on locize
|
|
202
|
+
- `--src-lng-only`: Check for changes in source language only
|
|
203
|
+
- `--compare-mtime`: Compare modification times when syncing
|
|
204
|
+
- `--dry-run`: Run the command without making any changes
|
|
205
|
+
|
|
206
|
+
**Interactive Setup:** If your locize credentials are missing or invalid, the toolkit will guide you through an interactive setup process to configure your Project ID, API Key, and version.
|
|
207
|
+
|
|
208
|
+
## Configuration
|
|
209
|
+
|
|
210
|
+
The configuration file supports both TypeScript (`.ts`) and JavaScript (`.js`) formats. Use the `defineConfig` helper for type safety and IntelliSense.
|
|
211
|
+
|
|
212
|
+
### Basic Configuration
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// i18next.config.ts
|
|
216
|
+
import { defineConfig } from 'i18next-cli';
|
|
217
|
+
|
|
218
|
+
export default defineConfig({
|
|
219
|
+
locales: ['en', 'de', 'fr'],
|
|
220
|
+
extract: {
|
|
221
|
+
input: ['src/**/*.{ts,tsx,js,jsx}'],
|
|
222
|
+
output: 'locales/{{language}}/{{namespace}}.json',
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Advanced Configuration
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { defineConfig } from 'i18next-cli';
|
|
231
|
+
|
|
232
|
+
export default defineConfig({
|
|
233
|
+
locales: ['en', 'de', 'fr'],
|
|
234
|
+
|
|
235
|
+
// Key extraction settings
|
|
236
|
+
extract: {
|
|
237
|
+
input: ['src/**/*.{ts,tsx}'],
|
|
238
|
+
output: 'locales/{{language}}/{{namespace}}.json',
|
|
239
|
+
|
|
240
|
+
// Translation functions to detect
|
|
241
|
+
functions: ['t', 'i18n.t', 'i18next.t'],
|
|
242
|
+
|
|
243
|
+
// React components to analyze
|
|
244
|
+
transComponents: ['Trans', 'Translation'],
|
|
245
|
+
|
|
246
|
+
// useTranslation hook variations
|
|
247
|
+
useTranslationNames: ['useTranslation', 'useAppTranslation'],
|
|
248
|
+
|
|
249
|
+
// Add custom JSX attributes to ignore during linting
|
|
250
|
+
ignoredAttributes: ['data-testid', 'aria-label'],
|
|
251
|
+
|
|
252
|
+
// Namespace and key configuration
|
|
253
|
+
defaultNS: 'translation',
|
|
254
|
+
nsSeparator: ':',
|
|
255
|
+
keySeparator: '.',
|
|
256
|
+
contextSeparator: '_',
|
|
257
|
+
pluralSeparator: '_',
|
|
258
|
+
|
|
259
|
+
// Preserve dynamic keys matching patterns
|
|
260
|
+
preservePatterns: [
|
|
261
|
+
'dynamic.feature.*',
|
|
262
|
+
'generated.*.key'
|
|
263
|
+
],
|
|
264
|
+
|
|
265
|
+
// Output formatting
|
|
266
|
+
sort: true,
|
|
267
|
+
indentation: 2,
|
|
268
|
+
|
|
269
|
+
// Primary language settings
|
|
270
|
+
primaryLanguage: 'en',
|
|
271
|
+
secondaryLanguages: ['de', 'fr'],
|
|
272
|
+
|
|
273
|
+
defaultValue: '', // Default value for missing keys
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// TypeScript type generation
|
|
277
|
+
types: {
|
|
278
|
+
input: ['locales/en/*.json'],
|
|
279
|
+
output: 'src/types/i18next.d.ts',
|
|
280
|
+
resourcesFile: 'src/types/resources.d.ts',
|
|
281
|
+
enableSelector: true, // Enable type-safe key selection
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Locize integration
|
|
285
|
+
locize: {
|
|
286
|
+
projectId: 'your-project-id',
|
|
287
|
+
apiKey: process.env.LOCIZE_API_KEY, // Recommended: use environment variables
|
|
288
|
+
version: 'latest',
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// Plugin system
|
|
292
|
+
plugins: [
|
|
293
|
+
// Add custom plugins here
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Advanced Features
|
|
299
|
+
|
|
300
|
+
### Plugin System
|
|
301
|
+
|
|
302
|
+
Create custom plugins to extend extraction capabilities. The plugin system is powerful enough to support non-JavaScript files (e.g., HTML, Handlebars) by using the `onEnd` hook with custom parsers.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { defineConfig, Plugin } from 'i18next-cli';
|
|
306
|
+
|
|
307
|
+
const myCustomPlugin = (): Plugin => ({
|
|
308
|
+
name: 'my-custom-plugin',
|
|
309
|
+
|
|
310
|
+
async setup() {
|
|
311
|
+
// Initialize plugin
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async onLoad(code: string, file: string) {
|
|
315
|
+
// Transform code before parsing
|
|
316
|
+
return code;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
onVisitNode(node: any, context: PluginContext) {
|
|
320
|
+
// Custom AST node processing
|
|
321
|
+
if (node.type === 'CallExpression') {
|
|
322
|
+
// Extract custom translation patterns
|
|
323
|
+
context.addKey({
|
|
324
|
+
key: 'custom.key',
|
|
325
|
+
defaultValue: 'Custom Value',
|
|
326
|
+
ns: 'custom'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async onEnd(allKeys: Map<string, ExtractedKey>) {
|
|
332
|
+
// Process all extracted keys or add additional keys from non-JS files
|
|
333
|
+
// Example: Parse HTML files for data-i18n attributes
|
|
334
|
+
const htmlFiles = await glob('src/**/*.html');
|
|
335
|
+
for (const file of htmlFiles) {
|
|
336
|
+
const content = await readFile(file, 'utf-8');
|
|
337
|
+
const matches = content.match(/data-i18n="([^"]+)"/g) || [];
|
|
338
|
+
for (const match of matches) {
|
|
339
|
+
const key = match.replace(/data-i18n="([^"]+)"/, '$1');
|
|
340
|
+
allKeys.set(`translation:${key}`, { key, ns: 'translation' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
export default defineConfig({
|
|
347
|
+
locales: ['en', 'de'],
|
|
348
|
+
plugins: [myCustomPlugin()],
|
|
349
|
+
// ... other config
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Dynamic Key Preservation
|
|
354
|
+
|
|
355
|
+
Use `preservePatterns` to maintain dynamically generated keys:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// Code like this:
|
|
359
|
+
const key = `user.${role}.permission`;
|
|
360
|
+
t(key);
|
|
361
|
+
|
|
362
|
+
// With this config:
|
|
363
|
+
export default defineConfig({
|
|
364
|
+
extract: {
|
|
365
|
+
preservePatterns: ['user.*.permission']
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Will preserve existing keys matching the pattern
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Comment-Based Extraction
|
|
373
|
+
|
|
374
|
+
Extract keys from comments for documentation or edge cases:
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
// t('welcome.message', 'Welcome to our app!')
|
|
378
|
+
// t('user.greeting', { defaultValue: 'Hello!', ns: 'common' })
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Migration from i18next-parser
|
|
382
|
+
|
|
383
|
+
Automatically migrate from legacy `i18next-parser.config.js`:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
npx i18next-cli migrate-config
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
This will:
|
|
390
|
+
- Convert your existing configuration to the new format
|
|
391
|
+
- Map old options to new equivalents
|
|
392
|
+
- Preserve custom settings where possible
|
|
393
|
+
- Create a new `i18next.config.ts` file
|
|
394
|
+
|
|
395
|
+
## CI/CD Integration
|
|
396
|
+
|
|
397
|
+
Use the `--ci` flag to fail builds when translations are outdated:
|
|
398
|
+
|
|
399
|
+
```yaml
|
|
400
|
+
# GitHub Actions example
|
|
401
|
+
- name: Check translations
|
|
402
|
+
run: npx i18next-cli extract --ci
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Watch Mode
|
|
406
|
+
|
|
407
|
+
For development, use watch mode to automatically update translations:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
npx i18next-cli extract --watch
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Type Safety
|
|
414
|
+
|
|
415
|
+
Generate TypeScript definitions for full type safety:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// Generated types enable autocomplete and validation
|
|
419
|
+
t('user.profile.name'); // ā
Valid key
|
|
420
|
+
t('invalid.key'); // ā TypeScript error
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Supported Patterns
|
|
426
|
+
|
|
427
|
+
The toolkit automatically detects these i18next usage patterns:
|
|
428
|
+
|
|
429
|
+
### Function Calls
|
|
430
|
+
```javascript
|
|
431
|
+
// Basic usage
|
|
432
|
+
t('key')
|
|
433
|
+
t('key', 'Default value')
|
|
434
|
+
t('key', { defaultValue: 'Default' })
|
|
435
|
+
|
|
436
|
+
// With namespaces
|
|
437
|
+
t('ns:key')
|
|
438
|
+
t('key', { ns: 'namespace' })
|
|
439
|
+
|
|
440
|
+
// With interpolation
|
|
441
|
+
t('key', { name: 'John' })
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### React Components
|
|
445
|
+
```jsx
|
|
446
|
+
// Trans component
|
|
447
|
+
<Trans i18nKey="welcome">Welcome {{name}}</Trans>
|
|
448
|
+
<Trans ns="common">user.greeting</Trans>
|
|
449
|
+
|
|
450
|
+
// useTranslation hook
|
|
451
|
+
const { t } = useTranslation('namespace');
|
|
452
|
+
const { t } = useTranslation(['ns1', 'ns2']);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Complex Patterns
|
|
456
|
+
```javascript
|
|
457
|
+
// Aliased functions
|
|
458
|
+
const translate = t;
|
|
459
|
+
translate('key');
|
|
460
|
+
|
|
461
|
+
// Destructured hooks
|
|
462
|
+
const { t: translate } = useTranslation();
|
|
463
|
+
|
|
464
|
+
// getFixedT
|
|
465
|
+
const fixedT = getFixedT('en', 'namespace');
|
|
466
|
+
fixedT('key');
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
<h3 align="center">Gold Sponsors</h3>
|
|
472
|
+
|
|
473
|
+
<p align="center">
|
|
474
|
+
<a href="https://www.locize.com/" target="_blank">
|
|
475
|
+
<img src="https://raw.githubusercontent.com/i18next/i18next/master/assets/locize_sponsor_240.gif" width="240px">
|
|
476
|
+
</a>
|
|
477
|
+
</p>
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
**From the creators of i18next: localization as a service - locize.com**
|
|
482
|
+
|
|
483
|
+
A translation management system built around the i18next ecosystem - [locize.com](https://www.locize.com).
|
|
484
|
+
|
|
485
|
+

|
|
486
|
+
|
|
487
|
+
With using [locize](https://locize.com/?utm_source=i18next_readme&utm_medium=github) you directly support the future of i18next.
|
|
488
|
+
|
|
489
|
+
---
|
package/dist/cjs/cli.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var e=require("commander"),o=require("chokidar"),t=require("glob"),n=require("chalk"),i=require("./config.js"),a=require("./heuristic-config.js"),r=require("./extractor/core/extractor.js");require("node:fs/promises"),require("node:path");var c=require("./types-generator.js"),s=require("./syncer.js"),l=require("./migrator.js"),u=require("./init.js"),d=require("./linter.js"),g=require("./status.js"),f=require("./locize.js");const p=new e.Command;p.name("i18next-cli").description("A unified, high-performance i18next CLI.").version("0.9.0"),p.command("extract").description("Extract translation keys from source files and update resource files.").option("-w, --watch","Watch for file changes and re-run the extractor.").option("--ci","Exit with a non-zero status code if any files are updated.").action(async e=>{const a=await i.ensureConfig(),c=async()=>{const o=await r.runExtractor(a);e.ci&&o&&(console.error(n.red.bold("\n[CI Mode] Error: Translation files were updated. Please commit the changes.")),console.log(n.yellow("š” Tip: Tired of committing JSON files? locize syncs your team automatically => https://www.locize.com/docs/getting-started")),console.log(` Learn more: ${n.cyan("npx i18next-cli locize-sync")}`),process.exit(1))};if(await c(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(a.extract.input),{ignored:/node_modules/,persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),c()})}}),p.command("status [locale]").description("Display translation status. Provide a locale for a detailed key-by-key view.").action(async e=>{let o=await i.loadConfig();if(!o){console.log(n.blue("No config file found. Attempting to detect project structure..."));const e=await a.detectConfig();e||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i18next-cli init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),o=e}await g.runStatus(o,{detail:e})}),p.command("types").description("Generate TypeScript definitions from translation resource files.").option("-w, --watch","Watch for file changes and re-run the type generator.").action(async e=>{const n=await i.ensureConfig(),a=()=>c.runTypesGenerator(n);if(await a(),e.watch){console.log("\nWatching for changes...");o.watch(await t.glob(n.types?.input||[]),{persistent:!0}).on("change",e=>{console.log(`\nFile changed: ${e}`),a()})}}),p.command("sync").description("Synchronize secondary language files with the primary language file.").action(async()=>{const e=await i.ensureConfig();await s.runSyncer(e)}),p.command("migrate-config").description("Migrate a legacy i18next-parser.config.js to the new format.").action(async()=>{await l.runMigrator()}),p.command("init").description("Create a new i18next.config.ts/js file with an interactive setup wizard.").action(u.runInit),p.command("lint").description("Find potential issues like hardcoded strings in your codebase.").action(async()=>{let e=await i.loadConfig();if(!e){console.log(n.blue("No config file found. Attempting to detect project structure..."));const o=await a.detectConfig();o||(console.error(n.red("Could not automatically detect your project structure.")),console.log(`Please create a config file first by running: ${n.cyan("npx i1e-toolkit init")}`),process.exit(1)),console.log(n.green("Project structure detected successfully!")),e=o}await d.runLinter(e)}),p.command("locize-sync").description("Synchronize local translations with your locize project.").option("--update-values","Update values of existing translations on locize.").option("--src-lng-only","Check for changes in source language only.").option("--compare-mtime","Compare modification times when syncing.").option("--dry-run","Run the command without making any changes.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeSync(o,e)}),p.command("locize-download").description("Download all translations from your locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeDownload(o,e)}),p.command("locize-migrate").description("Migrate local translation files to a new locize project.").action(async e=>{const o=await i.ensureConfig();await f.runLocizeMigrate(o,e)}),p.parse(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("node:path"),r=require("node:url"),n=require("node:fs/promises"),o=require("jiti"),t=require("inquirer"),i=require("chalk"),a=require("./init.js"),c="undefined"!=typeof document?document.currentScript:null;const s=["i18next.config.ts","i18next.config.js","i18next.config.mjs","i18next.config.cjs"];async function u(){const t=await async function(){for(const r of s){const o=e.resolve(process.cwd(),r);try{return await n.access(o),o}catch{}}return null}();if(!t)return null;try{let e;if(t.endsWith(".ts")){const r=o.createJiti("undefined"==typeof document?require("url").pathToFileURL(__filename).href:c&&"SCRIPT"===c.tagName.toUpperCase()&&c.src||new URL("config.js",document.baseURI).href),n=await r.import(t,{default:!0});e=n}else{const n=r.pathToFileURL(t).href,o=await import(`${n}?t=${Date.now()}`);e=o.default}return e?(e.extract||={},e.extract.primaryLanguage||=e.locales[0]||"en",e.extract.secondaryLanguages||=e.locales.filter(r=>r!==e.extract.primaryLanguage),e):(console.error(`Error: No default export found in ${t}`),null)}catch(e){return console.error(`Error loading configuration from ${t}`),console.error(e),null}}exports.defineConfig=function(e){return e},exports.ensureConfig=async function(){let e=await u();if(e)return e;const{shouldInit:r}=await t.prompt([{type:"confirm",name:"shouldInit",message:i.yellow("Configuration file not found. Would you like to create one now?"),default:!0}]);if(r){if(await a.runInit(),console.log(i.green("Configuration created. Resuming command...")),e=await u(),e)return e;console.error(i.red("Error: Failed to load configuration after creation. Please try running the command again.")),process.exit(1)}else console.log("Operation cancelled. Please create a configuration file to proceed."),process.exit(0)},exports.loadConfig=u;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var t=require("ora"),e=require("chalk"),r=require("@swc/core"),a=require("node:fs/promises"),n=require("node:path"),s=require("./key-finder.js"),o=require("./translation-manager.js"),i=require("../../utils/validation.js"),c=require("../plugin-manager.js"),u=require("../parsers/comment-parser.js"),l=require("../parsers/ast-visitors.js"),g=require("../../utils/logger.js");function f(t,e,r){if(t&&"object"==typeof t){for(const a of e)try{a.onVisitNode?.(t,r)}catch(t){console.warn(`Plugin ${a.name} onVisitNode failed:`,t)}for(const a of Object.keys(t)){const n=t[a];if(Array.isArray(n))for(const t of n)t&&"object"==typeof t&&f(t,e,r);else n&&"object"==typeof n&&f(n,e,r)}}}exports.extract=async function(t){t.extract.primaryLanguage||(t.extract.primaryLanguage=t.locales[0]),t.extract.secondaryLanguages||(t.extract.secondaryLanguages=t.locales.filter(e=>e!==t?.extract?.primaryLanguage)),t.extract.functions||(t.extract.functions=["t"]),t.extract.transComponents||(t.extract.transComponents=["Trans"]);const e=await s.findKeys(t);return o.getTranslations(e,t)},exports.processFile=async function(t,e,n,s){try{let o=await a.readFile(t,"utf-8");for(const r of e.plugins||[])o=await(r.onLoad?.(o,t))??o;const i=await r.parse(o,{syntax:"typescript",tsx:!0,comments:!0}),g=c.createPluginContext(s);u.extractKeysFromComments(o,e.extract.functions||["t"],g,e);new l.ASTVisitors(e,g,n).visit(i),(e.plugins||[]).length>0&&f(i,e.plugins||[],g)}catch(e){throw new i.ExtractorError("Failed to process file",t,e)}},exports.runExtractor=async function(r,c=new g.ConsoleLogger){r.extract.primaryLanguage||(r.extract.primaryLanguage=r.locales[0]||"en"),r.extract.secondaryLanguages||(r.extract.secondaryLanguages=r.locales.filter(t=>t!==r?.extract?.primaryLanguage)),i.validateExtractorConfig(r);const u=t("Running i18next key extractor...\n").start();try{const t=await s.findKeys(r,c);u.text=`Found ${t.size} unique keys. Updating translation files...`;const i=await o.getTranslations(t,r);let l=!1;for(const t of i)t.updated&&(l=!0,await a.mkdir(n.dirname(t.path),{recursive:!0}),await a.writeFile(t.path,JSON.stringify(t.newTranslations,null,2)),c.info(e.green(`Updated: ${t.path}`)));return u.succeed(e.bold("Extraction complete!")),l}catch(t){throw u.fail(e.red("Extraction failed.")),t}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("glob"),n=require("./extractor.js"),r=require("../../utils/logger.js"),i=require("../plugin-manager.js");exports.findKeys=async function(o,s=new r.ConsoleLogger){const t=await async function(n){return await e.glob(n.extract.input,{ignore:"node_modules/**",cwd:process.cwd()})}(o),a=new Map;await i.initializePlugins(o.plugins||[]);for(const e of t)await n.processFile(e,o,s,a);for(const e of o.plugins||[])await(e.onEnd?.(a));return a};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("node:fs/promises"),t=require("node:path"),a=require("../../utils/nested-object.js"),s=require("../../utils/file-utils.js");function r(e){const t=`^${e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*")}$`;return new RegExp(t)}exports.getTranslations=async function(n,o){const c=o.extract.defaultNS??"translation",u=o.extract.keySeparator??".",l=(o.extract.preservePatterns??[]).map(r);o.extract.primaryLanguage||(o.extract.primaryLanguage=o.locales[0]||"en"),o.extract.secondaryLanguages||(o.extract.secondaryLanguages=o.locales.filter(e=>e!==o.extract.primaryLanguage));const i=new Map;for(const e of n.values()){const t=e.ns||c;i.has(t)||i.set(t,[]),i.get(t).push(e)}const g=[];for(const r of o.locales)for(const[n,c]of i.entries()){const i=s.getOutputPath(o.extract.output,r,n),p=t.resolve(process.cwd(),i);let f="",d={};try{f=await e.readFile(p,"utf-8"),d=JSON.parse(f)}catch(e){}const x={},y=a.getNestedKeys(d,u);for(const e of y)if(l.some(t=>t.test(e))){const t=a.getNestedValue(d,e,u);a.setNestedValue(x,e,t,u)}const m=!1===o.extract.sort?c:c.sort((e,t)=>e.key.localeCompare(t.key));for(const{key:e,defaultValue:t}of m){const s=a.getNestedValue(d,e,u)??(r===o.extract?.primaryLanguage?t:"");a.setNestedValue(x,e,s,u)}const N=o.extract.indentation??2,h=JSON.stringify(x,null,N);g.push({path:p,updated:h!==f,newTranslations:x,existingTranslations:d})}return g};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("./jsx-parser.js");exports.ASTVisitors=class{pluginContext;config;logger;scopeStack=[];constructor(e,t,n){this.pluginContext=t,this.config=e,this.logger=n}visit(e){this.enterScope(),this.walk(e),this.exitScope()}walk(e){if(!e)return;let t=!1;switch("Function"!==e.type&&"ArrowFunctionExpression"!==e.type&&"FunctionExpression"!==e.type||(this.enterScope(),t=!0),e.type){case"VariableDeclarator":this.handleVariableDeclarator(e);break;case"CallExpression":this.handleCallExpression(e);break;case"JSXElement":this.handleJSXElement(e)}for(const t in e){if("span"===t)continue;const n=e[t];if(Array.isArray(n))for(const e of n)e&&"object"==typeof e&&e.type&&this.walk(e);else n&&n.type&&this.walk(n)}t&&this.exitScope()}enterScope(){this.scopeStack.push(new Map)}exitScope(){this.scopeStack.pop()}setVarInScope(e,t){this.scopeStack.length>0&&this.scopeStack[this.scopeStack.length-1].set(e,t)}getVarFromScope(e){for(let t=this.scopeStack.length-1;t>=0;t--)if(this.scopeStack[t].has(e))return this.scopeStack[t].get(e)}handleVariableDeclarator(e){if("CallExpression"!==e.init?.type)return;const t=e.init.callee;"Identifier"===t.type&&(this.config.extract.useTranslationNames||["useTranslation"]).indexOf(t.value)>-1?this.handleUseTranslationDeclarator(e):"MemberExpression"===t.type&&"Identifier"===t.property.type&&"getFixedT"===t.property.value&&this.handleGetFixedTDeclarator(e)}handleUseTranslationDeclarator(e){if(!e.init||"CallExpression"!==e.init.type)return;let t;if("ArrayPattern"===e.id.type){const n=e.id.elements[0];"Identifier"===n?.type&&(t=n.value)}if("ObjectPattern"===e.id.type)for(const n of e.id.properties){if("AssignmentPatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value){t="t";break}if("KeyValuePatternProperty"===n.type&&"Identifier"===n.key.type&&"t"===n.key.value&&"Identifier"===n.value.type){t=n.value.value;break}}if(!t)return;const n=e.init.arguments?.[0]?.expression;let i;"StringLiteral"===n?.type?i=n.value:"ArrayExpression"===n?.type&&"StringLiteral"===n.elements[0]?.expression.type&&(i=n.elements[0].expression.value);const r=e.init.arguments?.[1]?.expression;let s;"ObjectExpression"===r?.type&&(s=this.getObjectPropValue(r,"keyPrefix")),this.setVarInScope(t,{defaultNs:i,keyPrefix:s})}handleGetFixedTDeclarator(e){if("Identifier"!==e.id.type||!e.init||"CallExpression"!==e.init.type)return;const t=e.id.value,n=e.init.arguments,i=n[1]?.expression,r=n[2]?.expression,s="StringLiteral"===i?.type?i.value:void 0,a="StringLiteral"===r?.type?r.value:void 0;(s||a)&&this.setVarInScope(t,{defaultNs:s,keyPrefix:a})}handleCallExpression(e){const t=e.callee;if("Identifier"!==t.type)return;const n=(this.config.extract.functions||[]).includes(t.value),i=this.getVarFromScope(t.value);if(!n&&!(void 0!==i))return;if(0===e.arguments.length)return;const r=e.arguments[0].expression;let s,a=null;if("StringLiteral"===r.type?a=r.value:"ArrowFunctionExpression"===r.type&&(a=this.extractKeyFromSelector(r)),!a)return;let o=a;const l=e.arguments.length>1?e.arguments[1].expression:void 0;"ObjectExpression"===l?.type&&(s=this.getObjectPropValue(l,"ns")),!s&&i?.defaultNs&&(s=i.defaultNs);const p=this.config.extract.nsSeparator??":",u=this.config.extract.contextSeparator??"_";if(!s&&p&&a.includes(p)){const e=a.split(p);s=e.shift(),a=e.join(p),o=a}if(s||(s=this.config.extract.defaultNS),i?.keyPrefix){const e=this.config.extract.keySeparator??".";o=`${i.keyPrefix}${e}${a}`}const c="StringLiteral"===r.type?this.getDefaultValue(e,a):a;if("ObjectExpression"===l?.type){const e=this.getObjectPropValue(l,"context");if(e)return void this.pluginContext.addKey({key:`${o}${u}${e}`,ns:s,defaultValue:c});if(void 0!==this.getObjectPropValue(l,"count"))return void this.handlePluralKeys(o,c,s)}this.pluginContext.addKey({key:o,ns:s,defaultValue:c})}handlePluralKeys(e,t,n){try{const i=new Intl.PluralRules(this.config.extract?.primaryLanguage).resolvedOptions().pluralCategories,r=this.config.extract.pluralSeparator??"_";for(const s of i)this.pluginContext.addKey({key:`${e}${r}${s}`,ns:n,defaultValue:t,hasCount:!0})}catch(i){this.logger.warn(`Could not determine plural rules for language "${this.config.extract?.primaryLanguage}". Falling back to simple key extraction.`),this.pluginContext.addKey({key:e,defaultValue:t,ns:n})}}getDefaultValue(e,t){if(e.arguments.length<=1)return t;const n=e.arguments[1].expression;return"StringLiteral"===n.type?n.value||t:"ObjectExpression"===n.type&&this.getObjectPropValue(n,"defaultValue")||t}handleJSXElement(t){const n=this.getElementName(t);if(n&&(this.config.extract.transComponents||["Trans"]).includes(n)){const n=e.extractFromTransComponent(t,this.config);if(n){if(!n.ns){const e=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"t"===e.name.value);if("JSXAttribute"===e?.type&&"JSXExpressionContainer"===e.value?.type&&"Identifier"===e.value.expression.type){const t=e.value.expression.value,i=this.getVarFromScope(t);i?.defaultNs&&(n.ns=i.defaultNs)}}n.ns||(n.ns=this.config.extract.defaultNS),n.hasCount?this.handlePluralKeys(n.key,n.defaultValue,n.ns):this.pluginContext.addKey(n)}}}getElementName(e){if("Identifier"===e.opening.name.type)return e.opening.name.value;if("JSXMemberExpression"===e.opening.name.type){let t=e.opening.name;const n=[];for(;"JSXMemberExpression"===t.type;)"Identifier"===t.property.type&&n.unshift(t.property.value),t=t.object;return"Identifier"===t.type&&n.unshift(t.value),n.join(".")}}getObjectPropValue(e,t){const n=e.properties.find(e=>"KeyValueProperty"===e.type&&("Identifier"===e.key?.type&&e.key.value===t||"StringLiteral"===e.key?.type&&e.key.value===t));if("KeyValueProperty"===n?.type){const e=n.value;return"StringLiteral"===e.type?e.value:""}}extractKeyFromSelector(e){let t=e.body;if("BlockStatement"===t.type){const e=t.stmts.find(e=>"ReturnStatement"===e.type);if("ReturnStatement"!==e?.type||!e.argument)return null;t=e.argument}let n=t;const i=[];for(;"MemberExpression"===n.type;){const e=n.property;if("Identifier"===e.type)i.unshift(e.value);else{if("Computed"!==e.type||"StringLiteral"!==e.expression.type)return null;i.unshift(e.expression.value)}n=n.object}if(i.length>0){const e=this.config.extract.keySeparator,t="string"==typeof e?e:".";return i.join(t)}return null}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function e(e){const t=/^\s*,\s*(['"])(.*?)\1/.exec(e);if(t)return t[2];const s=/^\s*,\s*\{[^}]*defaultValue\s*:\s*(['"])(.*?)\1/.exec(e);return s?s[2]:void 0}function t(e){const t=/^\s*,\s*\{[^}]*ns\s*:\s*(['"])(.*?)\1/.exec(e);if(t)return t[2]}exports.extractKeysFromComments=function(s,n,c,o){const r=n.map(e=>e.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join("|"),u=new RegExp(`(?:${r})\\s*\\(\\s*(['"])([^'"]+)\\1`,"g"),i=function(e){const t=[],s=new Set,n=/\/\/(.*)|\/\*([\s\S]*?)\*\//g;let c;for(;null!==(c=n.exec(e));){const e=(c[1]??c[2]).trim();e&&!s.has(e)&&(s.add(e),t.push(e))}return t}(s);for(const s of i){let n;for(;null!==(n=u.exec(s));){let r,u=n[2];const i=s.slice(n.index+n[0].length),l=e(i);r=t(i);const a=o.extract.nsSeparator??":";if(!r&&a&&u.includes(a)){const e=u.split(a);r=e.shift(),u=e.join(a)}r||(r=o.extract.defaultNS),c.addKey({key:u,ns:r,defaultValue:l??u})}}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function e(e,t){const n=new Set(t.extract.transKeepBasicHtmlNodesFor??["br","strong","i","p"]);return function e(t){let i="";return t.forEach((t,r)=>{if("JSXText"===t.type)i+=t.value;else if("JSXExpressionContainer"===t.type){const e=t.expression;if("StringLiteral"===e.type)i+=e.value;else if("Identifier"===e.type)i+=`{{${e.value}}}`;else if("ObjectExpression"===e.type){const t=e.properties[0];t&&"Identifier"===t.type&&(i+=`{{${t.value}}}`)}}else if("JSXElement"===t.type){let a;"Identifier"===t.opening.name.type&&(a=t.opening.name.value);const l=e(t.children);a&&n.has(a)?i+=`<${a}>${l}</${a}>`:i+=`<${r}>${l}</${r}>`}else"JSXFragment"===t.type&&(i+=e(t.children))}),i}(e).trim().replace(/\s{2,}/g," ")}exports.extractFromTransComponent=function(t,n){const i=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"i18nKey"===e.name.value),r=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"defaults"===e.name.value),a=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"count"===e.name.value),l=!!a;let u;if(u="JSXAttribute"===i?.type&&"StringLiteral"===i.value?.type?i.value.value:e(t.children,n),!u)return null;const p=t.opening.attributes?.find(e=>"JSXAttribute"===e.type&&"Identifier"===e.name.type&&"ns"===e.name.value),s="JSXAttribute"===p?.type&&"StringLiteral"===p.value?.type?p.value.value:void 0;let o=n.extract.defaultValue||"";return o="JSXAttribute"===r?.type&&"StringLiteral"===r.value?.type?r.value.value:e(t.children,n),{key:u,ns:s,defaultValue:o||u,hasCount:l}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";exports.createPluginContext=function(t){return{addKey:e=>{const n=`${e.ns??"translation"}:${e.key}`;if(!t.has(n)){const s=e.defaultValue??e.key;t.set(n,{...e,defaultValue:s})}}}},exports.initializePlugins=async function(t){for(const e of t)await(e.setup?.())};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("glob"),s=require("node:fs/promises"),n=require("node:path");const o=["public/locales/dev/*.json","locales/dev/*.json","src/locales/dev/*.json","src/assets/locales/dev/*.json","public/locales/en/*.json","locales/en/*.json","src/locales/en/*.json","src/assets/locales/en/*.json"];exports.detectConfig=async function(){for(const t of o){const o=await e.glob(t,{ignore:"node_modules/**"});if(o.length>0){const e=o[0],t=n.dirname(n.dirname(e));try{let e=(await s.readdir(t)).filter(e=>/^(dev|[a-z]{2}(-[A-Z]{2})?)$/.test(e));if(e.length>0)return e.sort(),e.includes("dev")&&(e=["dev",...e.filter(e=>"dev"!==e)]),e.includes("en")&&(e=["en",...e.filter(e=>"en"!==e)]),{locales:e,extract:{input:["src/**/*.{js,jsx,ts,tsx}"],output:n.join(t,"{{language}}","{{namespace}}.json"),primaryLanguage:e.includes("en")?"en":e[0]}}}catch{continue}}}return null};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("./config.js"),r=require("./extractor/core/extractor.js"),t=require("./extractor/core/key-finder.js"),o=require("./extractor/core/translation-manager.js");exports.defineConfig=e.defineConfig,exports.extract=r.extract,exports.findKeys=t.findKeys,exports.getTranslations=o.getTranslations;
|
package/dist/cjs/init.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("inquirer"),t=require("node:fs/promises"),n=require("node:path");exports.runInit=async function(){console.log("Welcome to the i18next-cli setup wizard!");const i=await e.prompt([{type:"list",name:"fileType",message:"What kind of configuration file do you want?",choices:["TypeScript (i18next.config.ts)","JavaScript (i18next.config.js)"]},{type:"input",name:"locales",message:"What locales does your project support? (comma-separated)",default:"en,de,fr",filter:e=>e.split(",").map(e=>e.trim())},{type:"input",name:"input",message:"What is the glob pattern for your source files?",default:"src/**/*.{js,jsx,ts,tsx}"},{type:"input",name:"output",message:"What is the path for your output resource files?",default:"public/locales/{{language}}/{{namespace}}.json"}]),o=i.fileType.includes("TypeScript"),r=await async function(){try{const e=n.resolve(process.cwd(),"package.json"),i=await t.readFile(e,"utf-8");return"module"===JSON.parse(i).type}catch{return!0}}(),s=o?"i18next.config.ts":"i18next.config.js",a={locales:i.locales,extract:{input:i.input,output:i.output}};function p(e,t=2,n=0){const i=e=>" ".repeat(e*t),o=i(n),r=i(n+1);if(null===e||"number"==typeof e||"boolean"==typeof e)return JSON.stringify(e);if("string"==typeof e)return JSON.stringify(e);if(Array.isArray(e)){if(0===e.length)return"[]";return`[\n${e.map(e=>`${r}${p(e,t,n+1)}`).join(",\n")}\n${o}]`}if("object"==typeof e){const i=Object.keys(e);if(0===i.length)return"{}";return`{\n${i.map(i=>{const o=/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(i)?i:JSON.stringify(i);return`${r}${o}: ${p(e[i],t,n+1)}`}).join(",\n")}\n${o}}`}return JSON.stringify(e)}let f="";f=o?`import { defineConfig } from 'i18next-cli';\n\nexport default defineConfig(${p(a)});`:r?`import { defineConfig } from 'i18next-cli';\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nexport default defineConfig(${p(a)});`:`const { defineConfig } = require('i18next-cli');\n\n/** @type {import('i18next-cli').I18nextToolkitConfig} */\nmodule.exports = defineConfig(${p(a)});`;const u=n.resolve(process.cwd(),s);await t.writeFile(u,f.trim()),console.log(`ā
Configuration file created at: ${u}`)};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("glob"),t=require("node:fs/promises"),r=require("@swc/core"),n=require("swc-walk"),s=require("chalk"),o=require("ora");function i(e,t,r){const s=[],o=[0];for(let e=0;e<t.length;e++)"\n"===t[e]&&o.push(e+1);const i=e=>{let t=1;for(let r=0;r<o.length&&!(o[r]>e);r++)t=r+1;return t},a=r.extract.transComponents||["Trans"],l=r.extract.ignoredAttributes||[],c=new Set(["className","key","id","style","href","i18nKey","defaults","type",...l]);return n.ancestor(e,{JSXText(e,t){const r=t[t.length-2],n=r?.opening?.name?.value;if(n&&(a.includes(n)||"script"===n||"style"===n))return;if(t.some(e=>{if("JSXElement"!==e.type)return!1;const t=e.opening?.name?.value;return a.includes(t)||["script","style","code"].includes(t)}))return;const o=e.value.trim();o&&isNaN(Number(o))&&!o.startsWith("{{")&&s.push({text:o,line:i(e.span.start)})},StringLiteral(e,t){const r=t[t.length-2];if("JSXAttribute"===r?.type&&!c.has(r.name.value)){const t=e.value.trim();t&&isNaN(Number(t))&&s.push({text:t,line:i(e.span.start)})}}}),s}exports.runLinter=async function(n){const a=o("Analyzing source files...\n").start();try{const o=await e.glob(n.extract.input);let l=0;const c=new Map;for(const e of o){const s=await t.readFile(e,"utf-8"),o=i(await r.parse(s,{syntax:"typescript",tsx:!0}),s,n);o.length>0&&(l+=o.length,c.set(e,o))}if(l>0){a.fail(s.red.bold(`Linter found ${l} potential issues.`));for(const[e,t]of c.entries())console.log(s.yellow(`\n${e}`)),t.forEach(({text:e,line:t})=>{console.log(` ${s.gray(`${t}:`)} ${s.red("Error:")} Found hardcoded string: "${e}"`)});process.exit(1)}else a.succeed(s.green.bold("No issues found."))}catch(e){a.fail(s.red("Linter failed to run.")),console.error(e),process.exit(1)}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("execa"),o=require("chalk"),n=require("ora"),t=require("inquirer"),r=require("node:path");function i(e,o={},n={}){const t=[];if("sync"===e){(o.updateValues??n.updateValues)&&t.push("--update-values","true");(o.srcLngOnly??n.sourceLanguageOnly)&&t.push("--reference-language-only","true");(o.compareMtime??n.compareModificationTime)&&t.push("--compare-modification-time","true");(o.dryRun??n.dryRun)&&t.push("--dry","true")}return t}async function s(s,c,a={}){await async function(){try{await e.execa("locize",["--version"])}catch(e){"ENOENT"===e.code&&(console.error(o.red("Error: `locize-cli` command not found.")),console.log(o.yellow("Please install it globally to use the locize integration:")),console.log(o.cyan("npm install -g locize-cli")),process.exit(1))}}();const l=n(`Running 'locize ${s}'...\n`).start(),u=c.locize||{},{projectId:p,apiKey:d,version:y}=u;let g=[s];p&&g.push("--project-id",p),d&&g.push("--api-key",d),y&&g.push("--ver",y),g.push(...i(s,a,u));const m=r.resolve(process.cwd(),c.extract.output.split("/{{language}}/")[0]);g.push("--path",m);try{console.log(o.cyan(`\nRunning 'locize ${g.join(" ")}'...`));const n=await e.execa("locize",g,{stdio:"pipe"});l.succeed(o.green(`'locize ${s}' completed successfully.`)),n?.stdout&&console.log(n.stdout)}catch(n){const r=n.stderr||"";if(r.includes("missing required argument")){const n=await async function(e){console.log(o.yellow("\nLocize configuration is missing or invalid. Let's set it up!"));const n=await t.prompt([{type:"input",name:"projectId",message:"What is your locize Project ID? (Find this in your project settings on www.locize.app)",validate:e=>!!e||"Project ID cannot be empty."},{type:"password",name:"apiKey",message:'What is your locize API key? (Create or use one in your project settings > "API Keys")',validate:e=>!!e||"API Key cannot be empty."},{type:"input",name:"version",message:"What version do you want to sync with?",default:"latest"}]);if(!n.projectId)return void console.error(o.red("Project ID is required to continue."));e.locize={projectId:n.projectId,apiKey:n.apiKey,version:n.version};const{save:r}=await t.prompt([{type:"confirm",name:"save",message:"Would you like to see how to save these credentials for future use?",default:!0}]);if(r){const e=`\n# Add this to your .env file (and ensure .env is in your .gitignore!)\nLOCIZE_API_KEY=${n.apiKey}\n`,t=`\n // Add this to your i18next.config.ts file\n locize: {\n projectId: '${n.projectId}',\n // For security, apiKey is best set via an environment variable\n apiKey: process.env.LOCIZE_API_KEY,\n version: '${n.version}',\n },`;console.log(o.cyan("\nGreat! For the best security, we recommend using environment variables for your API key.")),console.log(o.bold("\nRecommended approach (.env file):")),console.log(o.green(e)),console.log(o.bold("Then, in your i18next.config.ts:")),console.log(o.green(t))}return e.locize}(c);if(n){g=[s],n.projectId&&g.push("--project-id",n.projectId),n.apiKey&&g.push("--api-key",n.apiKey),n.version&&g.push("--ver",n.version),g.push(...i(s,a,u)),g.push("--path",m);try{l.start("Retrying with new credentials...");const n=await e.execa("locize",g,{stdio:"pipe"});l.succeed(o.green("Retry successful!")),n?.stdout&&console.log(n.stdout)}catch(e){l.fail(o.red("Error during retry.")),console.error(e.stderr||e.message),process.exit(1)}}else l.fail("Operation cancelled."),process.exit(1)}else l.fail(o.red(`Error executing 'locize ${s}'.`)),console.error(r||n.message),process.exit(1)}console.log(o.green(`\nā
'locize ${s}' completed successfully.`))}exports.runLocizeDownload=(e,o)=>s("download",e,o),exports.runLocizeMigrate=(e,o)=>s("migrate",e,o),exports.runLocizeSync=(e,o)=>s("sync",e,o);
|