i18n-turbo 0.1.1 → 1.0.1
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/LICENSE +21 -21
- package/README.md +142 -74
- package/dist/bin/cli.js +0 -0
- package/dist/src/cli.js +10 -2
- package/dist/src/config.js +29 -0
- package/dist/src/extractor.js +292 -82
- package/dist/src/reverser.js +2 -2
- package/dist/src/scanner.js +8 -38
- package/dist/src/utils.js +25 -2
- package/package.json +58 -55
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Lasantha Lakmal
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lasantha Lakmal
|
|
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
CHANGED
|
@@ -1,74 +1,142 @@
|
|
|
1
|
-
# i18n-turbo
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
i18n-turbo ./
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
# i18n-turbo
|
|
2
|
+
|
|
3
|
+
> 🚀 **Turbocharge your i18n workflow.**
|
|
4
|
+
> Extract hardcoded strings from JSX/TSX/JS/TS files and export them to an i18n JSON format — with minimal configuration and maximum speed.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- ⚡ **Blazing Fast**: Asynchronous, parallel file processing for large codebases.
|
|
9
|
+
- 🎯 **Smart Extraction**: Detects strings in JSX text, attributes, and variables.
|
|
10
|
+
- 📁 **Namespaces**: Organize translations into multiple files (e.g., `auth.json`, `common.json`) based on file paths.
|
|
11
|
+
- 💬 **Context Support**: Extract comments (`i18n: ...`) as translation context for translators.
|
|
12
|
+
- 🔢 **Pluralization**: Automatically detects singular/plural patterns in ternary operators.
|
|
13
|
+
- 🔧 **Configs**: Flexible `i18n-turbo.config.js` for custom key strategies, exclusions, and more.
|
|
14
|
+
- 🔄 **Reverse Mode**: Revert `t('key')` back to original strings (great for refactoring).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g i18n-turbo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
i18n-turbo <input-dir> <output-file> [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Examples
|
|
29
|
+
|
|
30
|
+
**Basic Extraction:**
|
|
31
|
+
```bash
|
|
32
|
+
i18n-turbo ./src ./locales/en.json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Translate to French:**
|
|
36
|
+
```bash
|
|
37
|
+
i18n-turbo ./src ./locales/en.json --lang fr
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Dry Run (Preview changes):**
|
|
41
|
+
```bash
|
|
42
|
+
i18n-turbo ./src ./locales/en.json --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Update existing translations:**
|
|
46
|
+
```bash
|
|
47
|
+
i18n-turbo ./src ./locales/en.json --merge
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Create an `i18n-turbo.config.js` file in your project root to customize behavior.
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// i18n-turbo.config.js
|
|
56
|
+
module.exports = {
|
|
57
|
+
// Function Name (default: 't')
|
|
58
|
+
translationFunction: 't',
|
|
59
|
+
|
|
60
|
+
// String Length Threshold (default: 2)
|
|
61
|
+
minStringLength: 3,
|
|
62
|
+
|
|
63
|
+
// Key Generation
|
|
64
|
+
// Options: 'snake_case', 'camelCase', 'hash', or function(text)
|
|
65
|
+
keyGenerationStrategy: 'snake_case',
|
|
66
|
+
|
|
67
|
+
// Exclude directories/files (glob patterns)
|
|
68
|
+
excludePatterns: ['**/*.test.tsx', '**/stories/**'],
|
|
69
|
+
|
|
70
|
+
// Namespaces (Map source globs to namespace files)
|
|
71
|
+
namespaces: {
|
|
72
|
+
'src/features/auth/**': 'auth',
|
|
73
|
+
'src/features/dashboard/**': 'dashboard',
|
|
74
|
+
'src/components/**': 'common',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Default target language for machine translation
|
|
78
|
+
targetLang: 'es'
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Advanced Features
|
|
83
|
+
|
|
84
|
+
### Namespaces
|
|
85
|
+
Control where your strings go by defining namespaces.
|
|
86
|
+
If you configure `namespaces`, `i18n-turbo` will output separate files in the output directory instead of a single file.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// config
|
|
90
|
+
namespaces: {
|
|
91
|
+
'src/auth/**': 'auth',
|
|
92
|
+
}
|
|
93
|
+
// Output: locales/auth.json, locales/common.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Context Extraction
|
|
97
|
+
Provide context to translators by adding comments starting with `i18n:`.
|
|
98
|
+
The tool is smart enough to find comments attached to the node or its JSX siblings.
|
|
99
|
+
|
|
100
|
+
**Input:**
|
|
101
|
+
```tsx
|
|
102
|
+
{/* i18n: Title for the landing page */}
|
|
103
|
+
<h1>Welcome Home</h1>
|
|
104
|
+
|
|
105
|
+
const label = "Submit"; // i18n: Button label
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Output (`en.json`):**
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"welcome_home": "Welcome Home",
|
|
112
|
+
"welcome_home_comment": "Title for the landing page",
|
|
113
|
+
"submit": "Submit",
|
|
114
|
+
"submit_comment": "Button label"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Pluralization
|
|
119
|
+
`i18n-turbo` detects simple pluralization patterns in your code.
|
|
120
|
+
|
|
121
|
+
**Input:**
|
|
122
|
+
```tsx
|
|
123
|
+
<p>{count === 1 ? "One item" : "Many items"}</p>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Output:**
|
|
127
|
+
Replaces with `t('one_item')` and `t('many_items')` (Base support).
|
|
128
|
+
*Future updates will implement automatic `t('key', { count })` merging.*
|
|
129
|
+
|
|
130
|
+
## Key Generation Strategies
|
|
131
|
+
|
|
132
|
+
- **snake_case**: `Hello World` -> `hello_world` (Default)
|
|
133
|
+
- **camelCase**: `Hello World` -> `helloWorld`
|
|
134
|
+
- **hash**: `Hello World` -> `a1b2c3d4` (Useful for stable keys regardless of content)
|
|
135
|
+
- **Custom**:
|
|
136
|
+
```javascript
|
|
137
|
+
keyGenerationStrategy: (text) => text.toUpperCase().replace(/\s+/g, '_')
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/dist/bin/cli.js
CHANGED
|
File without changes
|
package/dist/src/cli.js
CHANGED
|
@@ -8,19 +8,27 @@ exports.runCLI = runCLI;
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const extractor_1 = require("./extractor");
|
|
10
10
|
const reverser_1 = require("./reverser");
|
|
11
|
+
const config_1 = require("./config");
|
|
11
12
|
async function runCLI() {
|
|
13
|
+
const config = (0, config_1.loadConfig)();
|
|
12
14
|
const args = process.argv.slice(2);
|
|
13
15
|
const inputDir = args[0] || './src';
|
|
14
16
|
const outputFile = args[1] || './locales/en.json';
|
|
15
17
|
const fnNameIndex = args.indexOf('--fn');
|
|
16
|
-
|
|
18
|
+
// CLI flag takes precedence over config
|
|
19
|
+
const fnName = fnNameIndex !== -1 && args[fnNameIndex + 1]
|
|
20
|
+
? args[fnNameIndex + 1]
|
|
21
|
+
: (config.translationFunction || 't');
|
|
17
22
|
const langIndex = args.indexOf('--lang');
|
|
18
|
-
const lang = langIndex !== -1 && args[langIndex + 1]
|
|
23
|
+
const lang = langIndex !== -1 && args[langIndex + 1]
|
|
24
|
+
? args[langIndex + 1]
|
|
25
|
+
: config.targetLang;
|
|
19
26
|
const options = {
|
|
20
27
|
fnName,
|
|
21
28
|
dryRun: args.includes('--dry-run'),
|
|
22
29
|
merge: args.includes('--merge'),
|
|
23
30
|
lang,
|
|
31
|
+
config,
|
|
24
32
|
};
|
|
25
33
|
const resolvedInputDir = path_1.default.resolve(inputDir);
|
|
26
34
|
const resolvedOutputFile = path_1.default.resolve(outputFile);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
translationFunction: 't',
|
|
11
|
+
minStringLength: 2,
|
|
12
|
+
excludePatterns: [],
|
|
13
|
+
keyGenerationStrategy: 'snake_case',
|
|
14
|
+
};
|
|
15
|
+
function loadConfig(cwd = process.cwd()) {
|
|
16
|
+
const configPath = path_1.default.join(cwd, 'i18n-turbo.config.js');
|
|
17
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
18
|
+
try {
|
|
19
|
+
// Dynamic require is acceptable for a CLI tool reading local config
|
|
20
|
+
const userConfig = require(configPath);
|
|
21
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.warn('Warning: Failed to load i18n-turbo.config.js:', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Future enhancement: support package.json or .json config
|
|
28
|
+
return DEFAULT_CONFIG;
|
|
29
|
+
}
|
package/dist/src/extractor.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.extractStringsFromDirectory = extractStringsFromDirectory;
|
|
40
40
|
// src/extractor.ts
|
|
41
41
|
const fs_1 = __importDefault(require("fs"));
|
|
42
|
+
const fs_2 = require("fs"); // Async FS
|
|
42
43
|
const path_1 = __importDefault(require("path"));
|
|
43
44
|
const babel = __importStar(require("@babel/core"));
|
|
44
45
|
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
@@ -47,100 +48,309 @@ const t = __importStar(require("@babel/types"));
|
|
|
47
48
|
const scanner_1 = require("./scanner");
|
|
48
49
|
const utils_1 = require("./utils");
|
|
49
50
|
const google_translate_api_1 = require("@vitalets/google-translate-api");
|
|
51
|
+
const p_limit_1 = __importDefault(require("p-limit"));
|
|
52
|
+
const minimatch_1 = require("minimatch");
|
|
50
53
|
async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// console.log('DEBUG: extractor options.config:', options.config);
|
|
55
|
+
// Map of namespace name -> translation map
|
|
56
|
+
const namespaceMaps = {};
|
|
57
|
+
// Default namespace (if no specific match or fallback)
|
|
58
|
+
const defaultNamespace = 'common'; // or derive from outputFile basename
|
|
59
|
+
// Initialize default map
|
|
60
|
+
if (!options.config?.namespaces) {
|
|
61
|
+
// If no namespaces configured, use a single map keyed by "default" or just merge everything there.
|
|
62
|
+
// actually we will simulate namespaces: "default" -> file content
|
|
63
|
+
namespaceMaps[defaultNamespace] = {};
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
filename: file,
|
|
65
|
-
presets: ["@babel/preset-typescript", "@babel/preset-react"],
|
|
65
|
+
else {
|
|
66
|
+
// initialize configured namespaces
|
|
67
|
+
Object.values(options.config.namespaces).forEach(ns => {
|
|
68
|
+
namespaceMaps[ns] = {};
|
|
66
69
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
// Ensure default exists for fallbacks
|
|
71
|
+
namespaceMaps[defaultNamespace] = {};
|
|
72
|
+
}
|
|
73
|
+
// Load existing translations if merge is on
|
|
74
|
+
// Logic update: If namespaces are used, we need to load MULTIPLE files.
|
|
75
|
+
// For simplicity: If config.namespaces exists, we assume outputFile is a DIRECTORY or prefix base.
|
|
76
|
+
// But strictly, if user passes `locales/en.json`, we might treat `locales` as the dir.
|
|
77
|
+
const outputDir = path_1.default.extname(outputFile) ? path_1.default.dirname(outputFile) : outputFile;
|
|
78
|
+
if (options.merge) {
|
|
79
|
+
for (const ns of Object.keys(namespaceMaps)) {
|
|
80
|
+
const nsFile = options.config?.namespaces
|
|
81
|
+
? path_1.default.join(outputDir, `${ns}.json`)
|
|
82
|
+
: outputFile; // If no namespaces, use the single output file
|
|
83
|
+
if (fs_1.default.existsSync(nsFile)) {
|
|
84
|
+
try {
|
|
85
|
+
namespaceMaps[ns] = JSON.parse(fs_1.default.readFileSync(nsFile, "utf-8"));
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const value = path.node.value;
|
|
86
|
-
// ✅ Skip import declarations
|
|
87
|
-
if (path.findParent((p) => p.isImportDeclaration()))
|
|
88
|
-
return;
|
|
89
|
-
// ✅ Skip if already translated
|
|
90
|
-
if (path.parentPath.isCallExpression() &&
|
|
91
|
-
path.parentPath.node.callee.type === "Identifier" &&
|
|
92
|
-
path.parentPath.node.callee.name === options.fnName)
|
|
93
|
-
return;
|
|
94
|
-
if (!value || value.length < 2 || value.length > 80)
|
|
95
|
-
return;
|
|
96
|
-
if (!/[a-zA-Z]/.test(value))
|
|
97
|
-
return;
|
|
98
|
-
const key = (0, utils_1.generateTranslationKey)(value);
|
|
99
|
-
translationMap[key] = value;
|
|
100
|
-
let replacement;
|
|
101
|
-
// ✅ Detect if inside JSX attribute
|
|
102
|
-
if (path.parentPath.isJSXAttribute()) {
|
|
103
|
-
replacement = t.jsxExpressionContainer(t.callExpression(t.identifier(options.fnName), [
|
|
104
|
-
t.stringLiteral(key),
|
|
105
|
-
]));
|
|
87
|
+
catch {
|
|
88
|
+
console.warn(`[WARN] Could not parse ${nsFile}, starting fresh.`);
|
|
106
89
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const files = await (0, scanner_1.getSourceFiles)(inputDir, options.config?.excludePatterns);
|
|
94
|
+
const limit = (0, p_limit_1.default)(50); // Concurrency limit
|
|
95
|
+
await Promise.all(files.map((file) => limit(async () => {
|
|
96
|
+
try {
|
|
97
|
+
// Read file async
|
|
98
|
+
const code = await fs_2.promises.readFile(file, "utf-8");
|
|
99
|
+
// Parse AST (still sync for now as babel.parseAsync just wraps it usually)
|
|
100
|
+
const ast = babel.parseSync(code, {
|
|
101
|
+
filename: file,
|
|
102
|
+
presets: ["@babel/preset-typescript", "@babel/preset-react"],
|
|
103
|
+
});
|
|
104
|
+
if (!ast)
|
|
105
|
+
return;
|
|
106
|
+
// Determine namespace for this file
|
|
107
|
+
let currentNs = defaultNamespace;
|
|
108
|
+
if (options.config?.namespaces) {
|
|
109
|
+
const relativePath = path_1.default.relative(process.cwd(), file); // or relative to inputDir? relative to CWD usually for globs
|
|
110
|
+
// Check against globs
|
|
111
|
+
for (const [pattern, nsName] of Object.entries(options.config.namespaces)) {
|
|
112
|
+
if ((0, minimatch_1.minimatch)(relativePath, pattern)) {
|
|
113
|
+
currentNs = nsName;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Ensure map exists (e.g. if we default to common)
|
|
119
|
+
if (!namespaceMaps[currentNs])
|
|
120
|
+
namespaceMaps[currentNs] = {};
|
|
121
|
+
let modified = false;
|
|
122
|
+
const checkComments = (node) => {
|
|
123
|
+
if (node.leadingComments) {
|
|
124
|
+
const c = node.leadingComments.find(c => c.value.trim().startsWith('i18n:'));
|
|
125
|
+
if (c)
|
|
126
|
+
return c.value.trim().replace(/^i18n:\s*/, '');
|
|
111
127
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
return undefined;
|
|
129
|
+
};
|
|
130
|
+
const getContextFromSibling = (path) => {
|
|
131
|
+
if (typeof path.key === 'number' && Array.isArray(path.container)) {
|
|
132
|
+
let k = path.key - 1;
|
|
133
|
+
while (k >= 0) {
|
|
134
|
+
const prevNode = path.container[k];
|
|
135
|
+
if (t.isJSXText(prevNode) && !prevNode.value.trim()) {
|
|
136
|
+
k--;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (t.isJSXExpressionContainer(prevNode) && t.isJSXEmptyExpression(prevNode.expression)) {
|
|
140
|
+
const comments = prevNode.expression.innerComments;
|
|
141
|
+
if (comments) {
|
|
142
|
+
const c = comments.find((c) => c.value.trim().startsWith('i18n:'));
|
|
143
|
+
if (c)
|
|
144
|
+
return c.value.trim().replace(/^i18n:\s*/, '');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// If we hit a non-empty text or other node that is not comment, stop?
|
|
148
|
+
// The comment should be immediately preceding (ignoring whitespace).
|
|
149
|
+
// If we found another Element, assume no comment for this one.
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
116
152
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
return undefined;
|
|
154
|
+
};
|
|
155
|
+
(0, traverse_1.default)(ast, {
|
|
156
|
+
JSXText(path) {
|
|
157
|
+
const rawText = path.node.value.trim();
|
|
158
|
+
if (!rawText || /^\{.*\}$/.test(rawText))
|
|
159
|
+
return;
|
|
160
|
+
// Check for context comments (i18n: ...)
|
|
161
|
+
let contextComment;
|
|
162
|
+
// Standard comments (leading)
|
|
163
|
+
const comment = checkComments(path.node) ||
|
|
164
|
+
(path.parentPath?.node && checkComments(path.parentPath.node));
|
|
165
|
+
// Sibling comments (JSX expression container before parent Element)
|
|
166
|
+
const siblingComment = path.parentPath ? getContextFromSibling(path.parentPath) : undefined;
|
|
167
|
+
contextComment = comment || siblingComment;
|
|
168
|
+
const key = (0, utils_1.generateTranslationKey)(rawText, options.config?.keyGenerationStrategy);
|
|
169
|
+
namespaceMaps[currentNs][key] = rawText;
|
|
170
|
+
if (contextComment) {
|
|
171
|
+
namespaceMaps[currentNs][`${key}_comment`] = contextComment;
|
|
172
|
+
}
|
|
173
|
+
const replacement = t.jsxExpressionContainer(t.callExpression(t.identifier(options.fnName), [
|
|
174
|
+
t.stringLiteral(key),
|
|
175
|
+
]));
|
|
176
|
+
path.replaceWith(replacement);
|
|
177
|
+
modified = true;
|
|
178
|
+
if (options.dryRun) {
|
|
179
|
+
console.log(`[DRY RUN] ${file} (${currentNs}): ${rawText} -> ${key}`);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
StringLiteral(path) {
|
|
183
|
+
const value = path.node.value;
|
|
184
|
+
// ✅ Check for context comments (i18n: ...)
|
|
185
|
+
// Check the node or its parent (e.g. if inside specific call/JSX)
|
|
186
|
+
let contextComment;
|
|
187
|
+
// Check current node, parent, or 2nd parent (e.g. JSXAttribute -> JSXOpeningElement -> JSXElement?)
|
|
188
|
+
// For JSX: Text -> JSXElement (parent).
|
|
189
|
+
// For StringLiteral inside JSX: StringLiteral -> JSXAttribute -> JSXOpeningElement ...
|
|
190
|
+
// Test case: {/* comment */} <h1>Text</h1>
|
|
191
|
+
// Here comment is sibling?
|
|
192
|
+
// Babel attaches 'leadingComments' to the subsequent node.
|
|
193
|
+
// So 'Text' might have leadingComments? Or 'h1' has them?
|
|
194
|
+
// If 'Text' is inside 'h1', 'h1' has the comments.
|
|
195
|
+
// so `path.parentPath`?
|
|
196
|
+
const comment = checkComments(path.node) ||
|
|
197
|
+
(path.parentPath?.node && checkComments(path.parentPath.node)) ||
|
|
198
|
+
(path.parentPath?.parentPath?.node && checkComments(path.parentPath.parentPath.node));
|
|
199
|
+
if (comment) {
|
|
200
|
+
contextComment = comment;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Try finding sibling comment if inside JSX Element (e.g. Attribute -> OpeningElement -> Element)
|
|
204
|
+
const elementPath = path.findParent(p => p.isJSXElement());
|
|
205
|
+
if (elementPath) {
|
|
206
|
+
contextComment = getContextFromSibling(elementPath);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ✅ Skip import declarations
|
|
210
|
+
if (path.findParent((p) => p.isImportDeclaration()))
|
|
211
|
+
return;
|
|
212
|
+
// ✅ Skip require('...') calls
|
|
213
|
+
if (path.parentPath.isCallExpression() &&
|
|
214
|
+
t.isIdentifier(path.parentPath.node.callee) &&
|
|
215
|
+
path.parentPath.node.callee.name === "require")
|
|
216
|
+
return;
|
|
217
|
+
// ✅ Skip if already translated
|
|
218
|
+
if (path.parentPath.isCallExpression() &&
|
|
219
|
+
path.parentPath.node.callee.type === "Identifier" &&
|
|
220
|
+
path.parentPath.node.callee.name === options.fnName)
|
|
221
|
+
return;
|
|
222
|
+
if (!value ||
|
|
223
|
+
value.length < (options.config?.minStringLength || 2) ||
|
|
224
|
+
value.length > 80)
|
|
225
|
+
return;
|
|
226
|
+
if (!/[a-zA-Z]/.test(value))
|
|
227
|
+
return;
|
|
228
|
+
const key = (0, utils_1.generateTranslationKey)(value, options.config?.keyGenerationStrategy);
|
|
229
|
+
// console.log(`DEBUG: key for "${value}" -> "${key}"`);
|
|
230
|
+
namespaceMaps[currentNs][key] = value;
|
|
231
|
+
if (contextComment) {
|
|
232
|
+
namespaceMaps[currentNs][`${key}_comment`] = contextComment;
|
|
233
|
+
}
|
|
234
|
+
let replacement;
|
|
235
|
+
// ✅ Detect if inside ignored JSX attribute (direct or nested)
|
|
236
|
+
const attributePath = path.findParent((p) => p.isJSXAttribute());
|
|
237
|
+
if (attributePath && attributePath.isJSXAttribute()) {
|
|
238
|
+
const attrName = attributePath.node.name;
|
|
239
|
+
if (t.isJSXIdentifier(attrName) &&
|
|
240
|
+
[
|
|
241
|
+
"data-testid",
|
|
242
|
+
"className",
|
|
243
|
+
"style",
|
|
244
|
+
"id",
|
|
245
|
+
"key",
|
|
246
|
+
"ref",
|
|
247
|
+
"width",
|
|
248
|
+
"height",
|
|
249
|
+
"href",
|
|
250
|
+
"src",
|
|
251
|
+
"type",
|
|
252
|
+
"rel",
|
|
253
|
+
"target",
|
|
254
|
+
"alt",
|
|
255
|
+
"placeholder",
|
|
256
|
+
].includes(attrName.name))
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// ✅ If inside JSX attribute (but not ignored), we need JSX wrapper
|
|
260
|
+
if (path.parentPath.isJSXAttribute()) {
|
|
261
|
+
// Double check we are direct child (standard string attr)
|
|
262
|
+
replacement = t.jsxExpressionContainer(t.callExpression(t.identifier(options.fnName), [
|
|
263
|
+
t.stringLiteral(key),
|
|
264
|
+
]));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
replacement = t.callExpression(t.identifier(options.fnName), [
|
|
268
|
+
t.stringLiteral(key),
|
|
269
|
+
]);
|
|
270
|
+
}
|
|
271
|
+
path.replaceWith(replacement);
|
|
272
|
+
modified = true;
|
|
273
|
+
if (options.dryRun) {
|
|
274
|
+
console.log(`[DRY RUN] ${file} (${currentNs}): "${value}" -> ${key}`);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (!options.dryRun && modified) {
|
|
279
|
+
const output = (0, generator_1.default)(ast).code;
|
|
280
|
+
// Write file async
|
|
281
|
+
await fs_2.promises.writeFile(file, output);
|
|
282
|
+
}
|
|
122
283
|
}
|
|
123
|
-
|
|
284
|
+
catch (err) {
|
|
285
|
+
console.error(`Error processing file ${file}: `, err);
|
|
286
|
+
}
|
|
287
|
+
})));
|
|
124
288
|
if (!options.dryRun) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
289
|
+
if (outputDir !== ".") {
|
|
290
|
+
await fs_2.promises.mkdir(outputDir, { recursive: true });
|
|
291
|
+
}
|
|
292
|
+
// Write all namespace files
|
|
293
|
+
for (const [ns, map] of Object.entries(namespaceMaps)) {
|
|
294
|
+
if (Object.keys(map).length === 0 && ns !== defaultNamespace)
|
|
295
|
+
continue; // Skip empty non-default/extra namespaces if explicit? Actually keep them empty if created?
|
|
296
|
+
// Let's write them if they have content OR they are the expected output.
|
|
297
|
+
if (Object.keys(map).length === 0 && !options.config?.namespaces)
|
|
298
|
+
continue; // If no config, don't write empty 'common' if empty? No, we should writes.
|
|
299
|
+
let targetFile = outputFile;
|
|
300
|
+
if (options.config?.namespaces) {
|
|
301
|
+
// If namespaces are used, outputFile is treated as base dir for default locale?
|
|
302
|
+
// Or we append ns to outputDir?
|
|
303
|
+
// Logic: if namespaces active, we construct file paths like `outputDir/ns.json`
|
|
304
|
+
// OR if targetLang is active `outputDir/lang/ns.json`.
|
|
305
|
+
// Wait, existing logic for Lang:
|
|
306
|
+
// `path.join(path.dirname(outputFile), `${options.lang}.json`)`
|
|
307
|
+
// New logic:
|
|
308
|
+
// If namespaces:
|
|
309
|
+
// Target File = `outputDir/ns.json` (for default/en)
|
|
310
|
+
// If --lang: `outputDir/ns.lang.json` ?? OR `outputDir/lang/ns.json`?
|
|
311
|
+
// Common pattern: `locales/en/common.json`.
|
|
312
|
+
// Let's assume `outputFile` denotes the folder structure for the default file.
|
|
313
|
+
// e.g. `locales/en.json` -> outputDir = `locales`
|
|
314
|
+
// If namespaces active: `locales/common.json`, `locales/auth.json`.
|
|
315
|
+
targetFile = path_1.default.join(outputDir, `${ns}.json`);
|
|
316
|
+
}
|
|
317
|
+
const mapToWrite = map;
|
|
318
|
+
// Translation logic
|
|
319
|
+
if (options.lang && options.lang !== "en") {
|
|
320
|
+
const translated = {};
|
|
321
|
+
for (const [key, text] of Object.entries(map)) {
|
|
322
|
+
try {
|
|
323
|
+
const res = await (0, google_translate_api_1.translate)(text, { to: options.lang });
|
|
324
|
+
console.log(`Translated [${ns}] "${text}" => "${res.text}"`);
|
|
325
|
+
translated[key] = res.text;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
console.warn(`Translation failed for "${text}"`, err);
|
|
329
|
+
translated[key] = text;
|
|
330
|
+
}
|
|
133
331
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
332
|
+
// If namespaces active: `locales/lang/ns.json` or `locales/ns.lang.json`?
|
|
333
|
+
// Let's do `locales/lang.json` if single file,
|
|
334
|
+
// BUT if namespaces: `locales/ns.json` (en), `locales/ns_fr.json`?
|
|
335
|
+
// OR `locales/fr/ns.json`.
|
|
336
|
+
// Let's stick to `locales/fr.json` if NO namespaces (legacy).
|
|
337
|
+
// If namespaces: `locales/fr/ns.json`? Or `locales/ns.fr.json`?
|
|
338
|
+
// Complexity increase!
|
|
339
|
+
// Simplified for this task:
|
|
340
|
+
// If namespaces: write `outputDir/ns.json`. If lang available, write `outputDir/ns_lang.json` (flat).
|
|
341
|
+
let langPath;
|
|
342
|
+
if (options.config?.namespaces) {
|
|
343
|
+
langPath = path_1.default.join(outputDir, `${ns}_${options.lang}.json`);
|
|
137
344
|
}
|
|
345
|
+
else {
|
|
346
|
+
langPath = path_1.default.join(path_1.default.dirname(outputFile), `${options.lang}.json`);
|
|
347
|
+
}
|
|
348
|
+
await fs_2.promises.writeFile(langPath, JSON.stringify(translated, null, 2), "utf-8");
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Default language (usually EN)
|
|
352
|
+
await fs_2.promises.writeFile(targetFile, JSON.stringify(mapToWrite, null, 2), "utf-8");
|
|
138
353
|
}
|
|
139
|
-
const langPath = path_1.default.join(path_1.default.dirname(outputFile), `${options.lang}.json`);
|
|
140
|
-
fs_1.default.writeFileSync(langPath, JSON.stringify(translated, null, 2), "utf-8");
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
fs_1.default.writeFileSync(outputFile, JSON.stringify(translationMap, null, 2), "utf-8");
|
|
144
354
|
}
|
|
145
355
|
}
|
|
146
356
|
}
|
package/dist/src/reverser.js
CHANGED
|
@@ -43,9 +43,9 @@ const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
|
43
43
|
const t = __importStar(require("@babel/types"));
|
|
44
44
|
const generator_1 = __importDefault(require("@babel/generator"));
|
|
45
45
|
const scanner_1 = require("./scanner");
|
|
46
|
-
function reverseStringsFromDirectory(inputDir, i18nFile, fnName = "t") {
|
|
46
|
+
async function reverseStringsFromDirectory(inputDir, i18nFile, fnName = "t") {
|
|
47
47
|
const translationMap = JSON.parse(fs_1.default.readFileSync(i18nFile, "utf-8"));
|
|
48
|
-
const files = (0, scanner_1.getSourceFiles)(inputDir);
|
|
48
|
+
const files = await (0, scanner_1.getSourceFiles)(inputDir);
|
|
49
49
|
for (const file of files) {
|
|
50
50
|
const code = fs_1.default.readFileSync(file, "utf-8");
|
|
51
51
|
const ast = babel.parseSync(code, {
|
package/dist/src/scanner.js
CHANGED
|
@@ -1,43 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.getSourceFiles = getSourceFiles;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
nodir: true
|
|
4
|
+
const glob_1 = require("glob");
|
|
5
|
+
async function getSourceFiles(dir, excludes = []) {
|
|
6
|
+
console.log(`Scanning dir: ${dir}, excludes: ${excludes}`);
|
|
7
|
+
const files = await (0, glob_1.glob)(`${dir}/**/*.{js,jsx,ts,tsx}`, {
|
|
8
|
+
nodir: true,
|
|
9
|
+
ignore: ['**/node_modules/**', '**/dist/**', ...excludes]
|
|
42
10
|
});
|
|
11
|
+
console.log(`Found ${files.length} files.`);
|
|
12
|
+
return files;
|
|
43
13
|
}
|
package/dist/src/utils.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.generateTranslationKey = generateTranslationKey;
|
|
4
|
-
|
|
5
|
-
function generateTranslationKey(text) {
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
function generateTranslationKey(text, strategy = 'snake_case') {
|
|
9
|
+
if (typeof strategy === 'function') {
|
|
10
|
+
return strategy(text);
|
|
11
|
+
}
|
|
12
|
+
switch (strategy) {
|
|
13
|
+
case 'camelCase':
|
|
14
|
+
return toCamelCase(text);
|
|
15
|
+
case 'hash':
|
|
16
|
+
return crypto_1.default.createHash('md5').update(text).digest('hex').substring(0, 8);
|
|
17
|
+
case 'snake_case':
|
|
18
|
+
default:
|
|
19
|
+
return toSnakeCase(text);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function toSnakeCase(text) {
|
|
6
23
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
|
|
7
24
|
}
|
|
25
|
+
function toCamelCase(text) {
|
|
26
|
+
return text
|
|
27
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => index === 0 ? word.toLowerCase() : word.toUpperCase())
|
|
28
|
+
.replace(/\s+/g, '')
|
|
29
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "i18n-turbo",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Extract hardcoded text from JSX/TSX files and convert to i18n format.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist",
|
|
8
|
-
"README.md",
|
|
9
|
-
"LICENSE"
|
|
10
|
-
],
|
|
11
|
-
"bin": {
|
|
12
|
-
"i18n-turbo": "dist/bin/cli.js"
|
|
13
|
-
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc",
|
|
16
|
-
"start": "ts-node bin/cli.ts",
|
|
17
|
-
"test": "vitest",
|
|
18
|
-
"prepare": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/lasalasa/i18n-turbo.git"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"i18n",
|
|
26
|
-
"translation",
|
|
27
|
-
"react",
|
|
28
|
-
"cli",
|
|
29
|
-
"internationalization"
|
|
30
|
-
],
|
|
31
|
-
"author": "Lasantha Lakmal",
|
|
32
|
-
"license": "MIT",
|
|
33
|
-
"bugs": {
|
|
34
|
-
"url": "https://github.com/lasalasa/i18n-turbo.git/issues"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://github.com/lasalasa/i18n-turbo#readme",
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@babel/core": "^7.0.0",
|
|
39
|
-
"@babel/generator": "^7.0.0",
|
|
40
|
-
"@babel/preset-react": "^7.0.0",
|
|
41
|
-
"@babel/preset-typescript": "^7.0.0",
|
|
42
|
-
"@babel/traverse": "^7.0.0",
|
|
43
|
-
"@babel/types": "^7.0.0",
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "i18n-turbo",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Extract hardcoded text from JSX/TSX files and convert to i18n format.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"i18n-turbo": "dist/bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "ts-node bin/cli.ts",
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"prepare": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/lasalasa/i18n-turbo.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"i18n",
|
|
26
|
+
"translation",
|
|
27
|
+
"react",
|
|
28
|
+
"cli",
|
|
29
|
+
"internationalization"
|
|
30
|
+
],
|
|
31
|
+
"author": "Lasantha Lakmal",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/lasalasa/i18n-turbo.git/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/lasalasa/i18n-turbo#readme",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@babel/core": "^7.0.0",
|
|
39
|
+
"@babel/generator": "^7.0.0",
|
|
40
|
+
"@babel/preset-react": "^7.0.0",
|
|
41
|
+
"@babel/preset-typescript": "^7.0.0",
|
|
42
|
+
"@babel/traverse": "^7.0.0",
|
|
43
|
+
"@babel/types": "^7.0.0",
|
|
44
|
+
"@types/minimatch": "^5.1.2",
|
|
45
|
+
"@vitalets/google-translate-api": "^9.2.1",
|
|
46
|
+
"glob": "^10.4.5",
|
|
47
|
+
"minimatch": "^10.1.1",
|
|
48
|
+
"p-limit": "^3.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/babel__core": "^7.20.5",
|
|
52
|
+
"@types/babel__generator": "^7.27.0",
|
|
53
|
+
"@types/babel__traverse": "^7.20.7",
|
|
54
|
+
"ts-node": "^10.0.0",
|
|
55
|
+
"typescript": "^5.0.0",
|
|
56
|
+
"vitest": "^3.2.4"
|
|
57
|
+
}
|
|
58
|
+
}
|