i18n-turbo 1.0.1 β 1.0.2
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/README.md +59 -41
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +2 -4
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +11 -16
- package/dist/src/config.d.ts +9 -0
- package/dist/src/config.js +5 -11
- package/dist/src/extractor.d.ts +11 -0
- package/dist/src/extractor.js +89 -89
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +12 -0
- package/dist/src/react.d.ts +106 -0
- package/dist/src/react.js +106 -0
- package/dist/src/reverser.d.ts +1 -0
- package/dist/src/reverser.js +59 -88
- package/dist/src/scanner.d.ts +1 -0
- package/dist/src/scanner.js +3 -6
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +3 -9
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -7,44 +7,89 @@
|
|
|
7
7
|
|
|
8
8
|
- β‘ **Blazing Fast**: Asynchronous, parallel file processing for large codebases.
|
|
9
9
|
- π― **Smart Extraction**: Detects strings in JSX text, attributes, and variables.
|
|
10
|
+
- βοΈ **React Integration**: Built-in `I18nProvider` and `useTranslation` hook for type-safe internalization.
|
|
10
11
|
- π **Namespaces**: Organize translations into multiple files (e.g., `auth.json`, `common.json`) based on file paths.
|
|
11
12
|
- π¬ **Context Support**: Extract comments (`i18n: ...`) as translation context for translators.
|
|
12
|
-
-
|
|
13
|
-
- π§ **Configs**: Flexible `i18n-turbo.config.js` for custom key strategies,
|
|
13
|
+
- π€ **Auto Translation**: Automatically translate missing keys to other languages (e.g. `--lang es`) using Google Translate.
|
|
14
|
+
- π§ **Configs**: Flexible `i18n-turbo.config.js` for custom key strategies, namespaces, and exclusions.
|
|
14
15
|
- π **Reverse Mode**: Revert `t('key')` back to original strings (great for refactoring).
|
|
15
16
|
|
|
16
17
|
## Install
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
|
-
npm install
|
|
20
|
+
npm install i18n-turbo
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
##
|
|
23
|
+
## React Integration
|
|
24
|
+
|
|
25
|
+
Wrap your app with `I18nProvider` and use the `useTranslation` hook.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// src/App.tsx
|
|
29
|
+
import { I18nProvider, useTranslation } from 'i18n-turbo';
|
|
30
|
+
import en from './locales/en.json';
|
|
31
|
+
import es from './locales/es.json';
|
|
32
|
+
|
|
33
|
+
const translations = { en, es };
|
|
34
|
+
|
|
35
|
+
export default function App() {
|
|
36
|
+
return (
|
|
37
|
+
<I18nProvider translations={translations} defaultLocale="en">
|
|
38
|
+
<MyComponent />
|
|
39
|
+
</I18nProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function MyComponent() {
|
|
44
|
+
const { t, lang, setLang } = useTranslation();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<h1>{t("hello_world")}</h1>
|
|
49
|
+
<button onClick={() => setLang('es')}>EspaΓ±ol</button>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CLI Usage
|
|
56
|
+
|
|
57
|
+
Run the CLI to extract strings and manage translations.
|
|
23
58
|
|
|
24
59
|
```bash
|
|
25
|
-
i18n-turbo <input-dir> <output-file> [options]
|
|
60
|
+
npx i18n-turbo <input-dir> <output-file> [options]
|
|
26
61
|
```
|
|
27
62
|
|
|
28
63
|
### Examples
|
|
29
64
|
|
|
30
65
|
**Basic Extraction:**
|
|
66
|
+
Extract strings from `./src` to `./locales/en.json`.
|
|
31
67
|
```bash
|
|
32
|
-
i18n-turbo ./src ./locales/en.json
|
|
68
|
+
npx i18n-turbo ./src ./locales/en.json
|
|
33
69
|
```
|
|
34
70
|
|
|
35
|
-
**
|
|
71
|
+
**Add a New Language:**
|
|
72
|
+
Translate extracted strings to French (`fr`).
|
|
36
73
|
```bash
|
|
37
|
-
i18n-turbo ./src ./locales/en.json --lang fr
|
|
74
|
+
npx i18n-turbo ./src ./locales/en.json --lang fr
|
|
38
75
|
```
|
|
39
76
|
|
|
40
|
-
**
|
|
77
|
+
**Update Translations:**
|
|
78
|
+
Merge new keys without overwriting existing manual translations.
|
|
41
79
|
```bash
|
|
42
|
-
i18n-turbo ./src ./locales/en.json --
|
|
80
|
+
npx i18n-turbo ./src ./locales/en.json --merge
|
|
43
81
|
```
|
|
44
82
|
|
|
45
|
-
**Update
|
|
83
|
+
**Force Update:**
|
|
84
|
+
Re-translate all keys (overwrite everything).
|
|
46
85
|
```bash
|
|
47
|
-
i18n-turbo ./src ./locales/en.json --
|
|
86
|
+
npx i18n-turbo ./src ./locales/en.json --force
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Reverse Extraction:**
|
|
90
|
+
Restore original text from keys (undo `t('key')` replacement).
|
|
91
|
+
```bash
|
|
92
|
+
npx i18n-turbo ./src ./locales/en.json --reverse
|
|
48
93
|
```
|
|
49
94
|
|
|
50
95
|
## Configuration
|
|
@@ -73,29 +118,13 @@ module.exports = {
|
|
|
73
118
|
'src/features/dashboard/**': 'dashboard',
|
|
74
119
|
'src/components/**': 'common',
|
|
75
120
|
},
|
|
76
|
-
|
|
77
|
-
// Default target language for machine translation
|
|
78
|
-
targetLang: 'es'
|
|
79
121
|
};
|
|
80
122
|
```
|
|
81
123
|
|
|
82
124
|
## Advanced Features
|
|
83
125
|
|
|
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
126
|
### Context Extraction
|
|
97
127
|
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
128
|
|
|
100
129
|
**Input:**
|
|
101
130
|
```tsx
|
|
@@ -116,7 +145,7 @@ const label = "Submit"; // i18n: Button label
|
|
|
116
145
|
```
|
|
117
146
|
|
|
118
147
|
### Pluralization
|
|
119
|
-
`i18n-turbo` detects simple
|
|
148
|
+
`i18n-turbo` detects simple ternary plurals.
|
|
120
149
|
|
|
121
150
|
**Input:**
|
|
122
151
|
```tsx
|
|
@@ -124,18 +153,7 @@ const label = "Submit"; // i18n: Button label
|
|
|
124
153
|
```
|
|
125
154
|
|
|
126
155
|
**Output:**
|
|
127
|
-
Replaces with `t('one_item')` and `t('many_items')
|
|
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
|
-
```
|
|
156
|
+
Replaces with `t('one_item')` and `t('many_items')`.
|
|
139
157
|
|
|
140
158
|
## License
|
|
141
159
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin/cli.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCLI(): Promise<void>;
|
package/dist/src/cli.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// bin/cli.ts or src/cli.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const extractor_1 = require("./extractor");
|
|
10
|
-
const reverser_1 = require("./reverser");
|
|
11
|
-
const config_1 = require("./config");
|
|
12
|
-
async function runCLI() {
|
|
13
|
-
const config = (0, config_1.loadConfig)();
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { extractStringsFromDirectory } from './extractor.js';
|
|
4
|
+
import { reverseStringsFromDirectory } from './reverser.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
export async function runCLI() {
|
|
7
|
+
const config = loadConfig();
|
|
14
8
|
const args = process.argv.slice(2);
|
|
15
9
|
const inputDir = args[0] || './src';
|
|
16
10
|
const outputFile = args[1] || './locales/en.json';
|
|
@@ -28,13 +22,14 @@ async function runCLI() {
|
|
|
28
22
|
dryRun: args.includes('--dry-run'),
|
|
29
23
|
merge: args.includes('--merge'),
|
|
30
24
|
lang,
|
|
25
|
+
force: args.includes('--force'),
|
|
31
26
|
config,
|
|
32
27
|
};
|
|
33
|
-
const resolvedInputDir =
|
|
34
|
-
const resolvedOutputFile =
|
|
28
|
+
const resolvedInputDir = path.resolve(inputDir);
|
|
29
|
+
const resolvedOutputFile = path.resolve(outputFile);
|
|
35
30
|
if (args.includes('--reverse')) {
|
|
36
|
-
await
|
|
31
|
+
await reverseStringsFromDirectory(resolvedInputDir, resolvedOutputFile, fnName);
|
|
37
32
|
process.exit(0);
|
|
38
33
|
}
|
|
39
|
-
await
|
|
34
|
+
await extractStringsFromDirectory(resolvedInputDir, resolvedOutputFile, options);
|
|
40
35
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface I18nTurboConfig {
|
|
2
|
+
translationFunction?: string;
|
|
3
|
+
minStringLength?: number;
|
|
4
|
+
excludePatterns?: string[];
|
|
5
|
+
keyGenerationStrategy?: 'snake_case' | 'camelCase' | 'hash' | ((text: string) => string);
|
|
6
|
+
targetLang?: string;
|
|
7
|
+
namespaces?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadConfig(cwd?: string): I18nTurboConfig;
|
package/dist/src/config.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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"));
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
9
3
|
const DEFAULT_CONFIG = {
|
|
10
4
|
translationFunction: 't',
|
|
11
5
|
minStringLength: 2,
|
|
12
6
|
excludePatterns: [],
|
|
13
7
|
keyGenerationStrategy: 'snake_case',
|
|
14
8
|
};
|
|
15
|
-
function loadConfig(cwd = process.cwd()) {
|
|
16
|
-
const configPath =
|
|
17
|
-
if (
|
|
9
|
+
export function loadConfig(cwd = process.cwd()) {
|
|
10
|
+
const configPath = path.join(cwd, 'i18n-turbo.config.js');
|
|
11
|
+
if (fs.existsSync(configPath)) {
|
|
18
12
|
try {
|
|
19
13
|
// Dynamic require is acceptable for a CLI tool reading local config
|
|
20
14
|
const userConfig = require(configPath);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { I18nTurboConfig } from "./config.js";
|
|
2
|
+
interface ExtractOptions {
|
|
3
|
+
fnName: string;
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
merge: boolean;
|
|
6
|
+
lang?: string;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
config?: I18nTurboConfig;
|
|
9
|
+
}
|
|
10
|
+
export declare function extractStringsFromDirectory(inputDir: string, outputFile: string, options: ExtractOptions): Promise<void>;
|
|
11
|
+
export {};
|
package/dist/src/extractor.js
CHANGED
|
@@ -1,56 +1,19 @@
|
|
|
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
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.extractStringsFromDirectory = extractStringsFromDirectory;
|
|
40
1
|
// src/extractor.ts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { promises as fsPromises } from "fs"; // Async FS
|
|
4
|
+
import path from "path";
|
|
5
|
+
import * as babel from "@babel/core";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const traverse = require("@babel/traverse").default;
|
|
9
|
+
const generate = require("@babel/generator").default;
|
|
10
|
+
import * as t from "@babel/types";
|
|
11
|
+
import { getSourceFiles } from "./scanner.js";
|
|
12
|
+
import { generateTranslationKey } from "./utils.js";
|
|
13
|
+
import { translate } from '@vitalets/google-translate-api';
|
|
14
|
+
import pLimit from "p-limit";
|
|
15
|
+
import { minimatch } from "minimatch";
|
|
16
|
+
export async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
54
17
|
// console.log('DEBUG: extractor options.config:', options.config);
|
|
55
18
|
// Map of namespace name -> translation map
|
|
56
19
|
const namespaceMaps = {};
|
|
@@ -74,15 +37,15 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
74
37
|
// Logic update: If namespaces are used, we need to load MULTIPLE files.
|
|
75
38
|
// For simplicity: If config.namespaces exists, we assume outputFile is a DIRECTORY or prefix base.
|
|
76
39
|
// But strictly, if user passes `locales/en.json`, we might treat `locales` as the dir.
|
|
77
|
-
const outputDir =
|
|
40
|
+
const outputDir = path.extname(outputFile) ? path.dirname(outputFile) : outputFile;
|
|
78
41
|
if (options.merge) {
|
|
79
42
|
for (const ns of Object.keys(namespaceMaps)) {
|
|
80
43
|
const nsFile = options.config?.namespaces
|
|
81
|
-
?
|
|
44
|
+
? path.join(outputDir, `${ns}.json`)
|
|
82
45
|
: outputFile; // If no namespaces, use the single output file
|
|
83
|
-
if (
|
|
46
|
+
if (fs.existsSync(nsFile)) {
|
|
84
47
|
try {
|
|
85
|
-
namespaceMaps[ns] = JSON.parse(
|
|
48
|
+
namespaceMaps[ns] = JSON.parse(fs.readFileSync(nsFile, "utf-8"));
|
|
86
49
|
}
|
|
87
50
|
catch {
|
|
88
51
|
console.warn(`[WARN] Could not parse ${nsFile}, starting fresh.`);
|
|
@@ -90,12 +53,12 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
90
53
|
}
|
|
91
54
|
}
|
|
92
55
|
}
|
|
93
|
-
const files = await
|
|
94
|
-
const limit = (
|
|
56
|
+
const files = await getSourceFiles(inputDir, options.config?.excludePatterns);
|
|
57
|
+
const limit = pLimit(50); // Concurrency limit
|
|
95
58
|
await Promise.all(files.map((file) => limit(async () => {
|
|
96
59
|
try {
|
|
97
60
|
// Read file async
|
|
98
|
-
const code = await
|
|
61
|
+
const code = await fsPromises.readFile(file, "utf-8");
|
|
99
62
|
// Parse AST (still sync for now as babel.parseAsync just wraps it usually)
|
|
100
63
|
const ast = babel.parseSync(code, {
|
|
101
64
|
filename: file,
|
|
@@ -106,10 +69,10 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
106
69
|
// Determine namespace for this file
|
|
107
70
|
let currentNs = defaultNamespace;
|
|
108
71
|
if (options.config?.namespaces) {
|
|
109
|
-
const relativePath =
|
|
72
|
+
const relativePath = path.relative(process.cwd(), file); // or relative to inputDir? relative to CWD usually for globs
|
|
110
73
|
// Check against globs
|
|
111
74
|
for (const [pattern, nsName] of Object.entries(options.config.namespaces)) {
|
|
112
|
-
if (
|
|
75
|
+
if (minimatch(relativePath, pattern)) {
|
|
113
76
|
currentNs = nsName;
|
|
114
77
|
break;
|
|
115
78
|
}
|
|
@@ -152,7 +115,7 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
152
115
|
}
|
|
153
116
|
return undefined;
|
|
154
117
|
};
|
|
155
|
-
(
|
|
118
|
+
traverse(ast, {
|
|
156
119
|
JSXText(path) {
|
|
157
120
|
const rawText = path.node.value.trim();
|
|
158
121
|
if (!rawText || /^\{.*\}$/.test(rawText))
|
|
@@ -165,7 +128,7 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
165
128
|
// Sibling comments (JSX expression container before parent Element)
|
|
166
129
|
const siblingComment = path.parentPath ? getContextFromSibling(path.parentPath) : undefined;
|
|
167
130
|
contextComment = comment || siblingComment;
|
|
168
|
-
const key =
|
|
131
|
+
const key = generateTranslationKey(rawText, options.config?.keyGenerationStrategy);
|
|
169
132
|
namespaceMaps[currentNs][key] = rawText;
|
|
170
133
|
if (contextComment) {
|
|
171
134
|
namespaceMaps[currentNs][`${key}_comment`] = contextComment;
|
|
@@ -219,13 +182,36 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
219
182
|
path.parentPath.node.callee.type === "Identifier" &&
|
|
220
183
|
path.parentPath.node.callee.name === options.fnName)
|
|
221
184
|
return;
|
|
185
|
+
// β
Skip common DOM identifiers
|
|
186
|
+
if (path.parentPath.isCallExpression()) {
|
|
187
|
+
const callee = path.parentPath.node.callee;
|
|
188
|
+
let fnName = '';
|
|
189
|
+
if (t.isIdentifier(callee)) {
|
|
190
|
+
fnName = callee.name;
|
|
191
|
+
}
|
|
192
|
+
else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
193
|
+
fnName = callee.property.name;
|
|
194
|
+
}
|
|
195
|
+
if ([
|
|
196
|
+
'getElementById',
|
|
197
|
+
'querySelector',
|
|
198
|
+
'querySelectorAll',
|
|
199
|
+
'addEventListener',
|
|
200
|
+
'removeEventListener',
|
|
201
|
+
'postMessage',
|
|
202
|
+
'setAttribute',
|
|
203
|
+
'getAttribute'
|
|
204
|
+
].includes(fnName)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
222
208
|
if (!value ||
|
|
223
209
|
value.length < (options.config?.minStringLength || 2) ||
|
|
224
210
|
value.length > 80)
|
|
225
211
|
return;
|
|
226
212
|
if (!/[a-zA-Z]/.test(value))
|
|
227
213
|
return;
|
|
228
|
-
const key =
|
|
214
|
+
const key = generateTranslationKey(value, options.config?.keyGenerationStrategy);
|
|
229
215
|
// console.log(`DEBUG: key for "${value}" -> "${key}"`);
|
|
230
216
|
namespaceMaps[currentNs][key] = value;
|
|
231
217
|
if (contextComment) {
|
|
@@ -251,8 +237,16 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
251
237
|
"type",
|
|
252
238
|
"rel",
|
|
253
239
|
"target",
|
|
254
|
-
"alt",
|
|
255
|
-
"placeholder",
|
|
240
|
+
// "alt", // Likely want to translate alt
|
|
241
|
+
// "placeholder", // Likely want to translate placeholder
|
|
242
|
+
"path", // React Router
|
|
243
|
+
"to", // Link
|
|
244
|
+
"element", // Route
|
|
245
|
+
"defaultLocale", // I18nProvider
|
|
246
|
+
"value", // Inputs
|
|
247
|
+
"name", // Inputs
|
|
248
|
+
"htmlFor", // Labels
|
|
249
|
+
"as" // Polymorphic
|
|
256
250
|
].includes(attrName.name))
|
|
257
251
|
return;
|
|
258
252
|
}
|
|
@@ -276,9 +270,9 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
276
270
|
},
|
|
277
271
|
});
|
|
278
272
|
if (!options.dryRun && modified) {
|
|
279
|
-
const output = (
|
|
273
|
+
const output = generate(ast).code;
|
|
280
274
|
// Write file async
|
|
281
|
-
await
|
|
275
|
+
await fsPromises.writeFile(file, output);
|
|
282
276
|
}
|
|
283
277
|
}
|
|
284
278
|
catch (err) {
|
|
@@ -287,7 +281,7 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
287
281
|
})));
|
|
288
282
|
if (!options.dryRun) {
|
|
289
283
|
if (outputDir !== ".") {
|
|
290
|
-
await
|
|
284
|
+
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
291
285
|
}
|
|
292
286
|
// Write all namespace files
|
|
293
287
|
for (const [ns, map] of Object.entries(namespaceMaps)) {
|
|
@@ -312,15 +306,37 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
312
306
|
// Let's assume `outputFile` denotes the folder structure for the default file.
|
|
313
307
|
// e.g. `locales/en.json` -> outputDir = `locales`
|
|
314
308
|
// If namespaces active: `locales/common.json`, `locales/auth.json`.
|
|
315
|
-
targetFile =
|
|
309
|
+
targetFile = path.join(outputDir, `${ns}.json`);
|
|
316
310
|
}
|
|
317
311
|
const mapToWrite = map;
|
|
318
312
|
// Translation logic
|
|
319
313
|
if (options.lang && options.lang !== "en") {
|
|
320
314
|
const translated = {};
|
|
315
|
+
let langPath;
|
|
316
|
+
if (options.config?.namespaces) {
|
|
317
|
+
langPath = path.join(outputDir, `${ns}_${options.lang}.json`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
langPath = path.join(path.dirname(outputFile), `${options.lang}.json`);
|
|
321
|
+
}
|
|
322
|
+
// Load existing translations for this language if available
|
|
323
|
+
let existingTranslations = {};
|
|
324
|
+
if (fs.existsSync(langPath)) {
|
|
325
|
+
try {
|
|
326
|
+
existingTranslations = JSON.parse(await fsPromises.readFile(langPath, 'utf-8'));
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
console.warn(`[WARN] Could not parse existing translation file ${langPath}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
321
332
|
for (const [key, text] of Object.entries(map)) {
|
|
333
|
+
// Check if exists
|
|
334
|
+
if (!options.force && existingTranslations[key]) {
|
|
335
|
+
translated[key] = existingTranslations[key];
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
322
338
|
try {
|
|
323
|
-
const res = await
|
|
339
|
+
const res = await translate(text, { to: options.lang });
|
|
324
340
|
console.log(`Translated [${ns}] "${text}" => "${res.text}"`);
|
|
325
341
|
translated[key] = res.text;
|
|
326
342
|
}
|
|
@@ -329,27 +345,11 @@ async function extractStringsFromDirectory(inputDir, outputFile, options) {
|
|
|
329
345
|
translated[key] = text;
|
|
330
346
|
}
|
|
331
347
|
}
|
|
332
|
-
|
|
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`);
|
|
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");
|
|
348
|
+
await fsPromises.writeFile(langPath, JSON.stringify(translated, null, 2), "utf-8");
|
|
349
349
|
}
|
|
350
350
|
else {
|
|
351
351
|
// Default language (usually EN)
|
|
352
|
-
await
|
|
352
|
+
await fsPromises.writeFile(targetFile, JSON.stringify(mapToWrite, null, 2), "utf-8");
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n-turbo - Internationalization toolkit for React
|
|
3
|
+
*
|
|
4
|
+
* This package provides:
|
|
5
|
+
* - React components (I18nProvider, useTranslation) for runtime i18n
|
|
6
|
+
* - CLI tool for extracting hardcoded strings from JSX/TSX files (run via CLI only)
|
|
7
|
+
*/
|
|
8
|
+
export { I18nProvider, useTranslation, createI18n, type I18nProviderProps, type I18nConfig, type I18nContextValue, type Translations, type TranslationDictionary, type InterpolationValues, } from './react';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n-turbo - Internationalization toolkit for React
|
|
3
|
+
*
|
|
4
|
+
* This package provides:
|
|
5
|
+
* - React components (I18nProvider, useTranslation) for runtime i18n
|
|
6
|
+
* - CLI tool for extracting hardcoded strings from JSX/TSX files (run via CLI only)
|
|
7
|
+
*/
|
|
8
|
+
// React components and hooks
|
|
9
|
+
export { I18nProvider, useTranslation, createI18n, } from './react';
|
|
10
|
+
// Note: CLI functionality (extractStringsFromDirectory) is only available
|
|
11
|
+
// via the command line tool, not as a module export, since it uses Node.js APIs
|
|
12
|
+
// that are not compatible with browser environments.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Translation dictionary type - maps keys to translated strings
|
|
4
|
+
*/
|
|
5
|
+
export type TranslationDictionary = Record<string, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Translations configuration - maps locale codes to translation dictionaries
|
|
8
|
+
*/
|
|
9
|
+
export type Translations = Record<string, TranslationDictionary>;
|
|
10
|
+
/**
|
|
11
|
+
* Interpolation values for translation strings
|
|
12
|
+
*/
|
|
13
|
+
export type InterpolationValues = Record<string, string | number>;
|
|
14
|
+
/**
|
|
15
|
+
* Configuration options for I18nProvider
|
|
16
|
+
*/
|
|
17
|
+
export interface I18nConfig {
|
|
18
|
+
/** Map of locale codes to translation dictionaries */
|
|
19
|
+
translations: Translations;
|
|
20
|
+
/** Default/initial locale */
|
|
21
|
+
defaultLocale?: string;
|
|
22
|
+
/** Fallback locale when translation is missing */
|
|
23
|
+
fallbackLocale?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Context value returned by useTranslation hook
|
|
27
|
+
*/
|
|
28
|
+
export interface I18nContextValue {
|
|
29
|
+
/** Translation function */
|
|
30
|
+
t: (key: string, values?: InterpolationValues) => string;
|
|
31
|
+
/** Current locale */
|
|
32
|
+
lang: string;
|
|
33
|
+
/** Set the current locale */
|
|
34
|
+
setLang: (lang: string) => void;
|
|
35
|
+
/** Available locales */
|
|
36
|
+
locales: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hook to access translation functions and locale state
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { t, lang, setLang } = useTranslation();
|
|
44
|
+
*
|
|
45
|
+
* return (
|
|
46
|
+
* <div>
|
|
47
|
+
* <h1>{t('hello')}</h1>
|
|
48
|
+
* <p>{t('welcome_message', { name: 'John' })}</p>
|
|
49
|
+
* <button onClick={() => setLang('es')}>EspaΓ±ol</button>
|
|
50
|
+
* </div>
|
|
51
|
+
* );
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function useTranslation(): I18nContextValue;
|
|
55
|
+
/**
|
|
56
|
+
* Props for I18nProvider component
|
|
57
|
+
*/
|
|
58
|
+
export interface I18nProviderProps {
|
|
59
|
+
/** Child components */
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
/** Translation dictionaries keyed by locale */
|
|
62
|
+
translations: Translations;
|
|
63
|
+
/** Default/initial locale (defaults to 'en') */
|
|
64
|
+
defaultLocale?: string;
|
|
65
|
+
/** Fallback locale when translation is missing (defaults to defaultLocale) */
|
|
66
|
+
fallbackLocale?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Provider component for internationalization context
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* import { I18nProvider } from 'i18n-turbo';
|
|
74
|
+
* import en from './locales/en.json';
|
|
75
|
+
* import es from './locales/es.json';
|
|
76
|
+
*
|
|
77
|
+
* const translations = { en, es };
|
|
78
|
+
*
|
|
79
|
+
* function App() {
|
|
80
|
+
* return (
|
|
81
|
+
* <I18nProvider translations={translations} defaultLocale="en">
|
|
82
|
+
* <MyApp />
|
|
83
|
+
* </I18nProvider>
|
|
84
|
+
* );
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function I18nProvider({ children, translations, defaultLocale, fallbackLocale, }: I18nProviderProps): React.ReactElement;
|
|
89
|
+
/**
|
|
90
|
+
* Factory function to create a configured I18n instance
|
|
91
|
+
* Useful for creating reusable configurations
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* const { Provider, useTranslation } = createI18n({
|
|
96
|
+
* translations: { en, es, fr },
|
|
97
|
+
* defaultLocale: 'en',
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function createI18n(config: I18nConfig): {
|
|
102
|
+
Provider: ({ children }: {
|
|
103
|
+
children: ReactNode;
|
|
104
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
105
|
+
useTranslation: typeof useTranslation;
|
|
106
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
const I18nContext = createContext({
|
|
4
|
+
t: (key) => key,
|
|
5
|
+
lang: 'en',
|
|
6
|
+
setLang: () => { },
|
|
7
|
+
locales: [],
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Hook to access translation functions and locale state
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { t, lang, setLang } = useTranslation();
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <div>
|
|
18
|
+
* <h1>{t('hello')}</h1>
|
|
19
|
+
* <p>{t('welcome_message', { name: 'John' })}</p>
|
|
20
|
+
* <button onClick={() => setLang('es')}>EspaΓ±ol</button>
|
|
21
|
+
* </div>
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function useTranslation() {
|
|
26
|
+
return useContext(I18nContext);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Interpolate values into a translation string
|
|
30
|
+
* Replaces {{key}} patterns with corresponding values
|
|
31
|
+
*/
|
|
32
|
+
function interpolate(template, values) {
|
|
33
|
+
if (!values)
|
|
34
|
+
return template;
|
|
35
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
36
|
+
const value = values[key];
|
|
37
|
+
return value !== undefined ? String(value) : `{{${key}}}`;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Provider component for internationalization context
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { I18nProvider } from 'i18n-turbo';
|
|
46
|
+
* import en from './locales/en.json';
|
|
47
|
+
* import es from './locales/es.json';
|
|
48
|
+
*
|
|
49
|
+
* const translations = { en, es };
|
|
50
|
+
*
|
|
51
|
+
* function App() {
|
|
52
|
+
* return (
|
|
53
|
+
* <I18nProvider translations={translations} defaultLocale="en">
|
|
54
|
+
* <MyApp />
|
|
55
|
+
* </I18nProvider>
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function I18nProvider({ children, translations, defaultLocale = 'en', fallbackLocale, }) {
|
|
61
|
+
const [lang, setLang] = useState(defaultLocale);
|
|
62
|
+
const effectiveFallback = fallbackLocale ?? defaultLocale;
|
|
63
|
+
const locales = useMemo(() => Object.keys(translations), [translations]);
|
|
64
|
+
const t = useCallback((key, values) => {
|
|
65
|
+
// Try current locale first
|
|
66
|
+
const currentDict = translations[lang];
|
|
67
|
+
if (currentDict?.[key]) {
|
|
68
|
+
return interpolate(currentDict[key], values);
|
|
69
|
+
}
|
|
70
|
+
// Try fallback locale
|
|
71
|
+
if (lang !== effectiveFallback) {
|
|
72
|
+
const fallbackDict = translations[effectiveFallback];
|
|
73
|
+
if (fallbackDict?.[key]) {
|
|
74
|
+
return interpolate(fallbackDict[key], values);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Return key as last resort
|
|
78
|
+
return interpolate(key, values);
|
|
79
|
+
}, [lang, translations, effectiveFallback]);
|
|
80
|
+
const contextValue = useMemo(() => ({
|
|
81
|
+
t,
|
|
82
|
+
lang,
|
|
83
|
+
setLang,
|
|
84
|
+
locales,
|
|
85
|
+
}), [t, lang, locales]);
|
|
86
|
+
return (_jsx(I18nContext.Provider, { value: contextValue, children: children }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Factory function to create a configured I18n instance
|
|
90
|
+
* Useful for creating reusable configurations
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const { Provider, useTranslation } = createI18n({
|
|
95
|
+
* translations: { en, es, fr },
|
|
96
|
+
* defaultLocale: 'en',
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function createI18n(config) {
|
|
101
|
+
const ConfiguredProvider = ({ children }) => (_jsx(I18nProvider, { translations: config.translations, defaultLocale: config.defaultLocale, fallbackLocale: config.fallbackLocale, children: children }));
|
|
102
|
+
return {
|
|
103
|
+
Provider: ConfiguredProvider,
|
|
104
|
+
useTranslation,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reverseStringsFromDirectory(inputDir: string, i18nFile: string, fnName?: string): Promise<void>;
|
package/dist/src/reverser.js
CHANGED
|
@@ -1,96 +1,67 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
47
|
-
const translationMap = JSON.parse(fs_1.default.readFileSync(i18nFile, "utf-8"));
|
|
48
|
-
const files = await (0, scanner_1.getSourceFiles)(inputDir);
|
|
49
|
-
for (const file of files) {
|
|
50
|
-
const code = fs_1.default.readFileSync(file, "utf-8");
|
|
51
|
-
const ast = babel.parseSync(code, {
|
|
52
|
-
filename: file,
|
|
53
|
-
presets: ["@babel/preset-typescript", "@babel/preset-react"],
|
|
54
|
-
});
|
|
55
|
-
if (!ast)
|
|
56
|
-
continue;
|
|
57
|
-
let modified = false;
|
|
58
|
-
(0, traverse_1.default)(ast, {
|
|
59
|
-
CallExpression(path) {
|
|
60
|
-
const callee = path.node.callee;
|
|
61
|
-
const args = path.node.arguments;
|
|
62
|
-
if (t.isIdentifier(callee, { name: fnName }) &&
|
|
63
|
-
args.length === 1 &&
|
|
64
|
-
t.isStringLiteral(args[0])) {
|
|
65
|
-
const key = args[0].value;
|
|
66
|
-
const originalText = translationMap[key];
|
|
67
|
-
if (typeof originalText === "string") {
|
|
68
|
-
const parent = path.parentPath;
|
|
69
|
-
if (parent?.isJSXExpressionContainer()) {
|
|
70
|
-
const containerParent = parent.parentPath;
|
|
71
|
-
if (containerParent?.isJSXAttribute() &&
|
|
72
|
-
containerParent.node.value === parent.node) {
|
|
73
|
-
// If the expression is inside an attribute, set attribute value to string literal
|
|
74
|
-
containerParent.node.value = t.stringLiteral(originalText);
|
|
75
|
-
parent.remove(); // Remove the expression container if still present
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import * as babel from "@babel/core";
|
|
3
|
+
import _traverse from "@babel/traverse";
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
const traverse = _traverse.default || _traverse;
|
|
6
|
+
import * as t from "@babel/types";
|
|
7
|
+
import _generate from "@babel/generator";
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const generate = _generate.default || _generate;
|
|
10
|
+
import { getSourceFiles } from "./scanner.js";
|
|
11
|
+
export async function reverseStringsFromDirectory(inputDir, i18nFile, fnName = "t") {
|
|
12
|
+
try {
|
|
13
|
+
const translationMap = JSON.parse(fs.readFileSync(i18nFile, "utf-8"));
|
|
14
|
+
const files = await getSourceFiles(inputDir);
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
17
|
+
const ast = babel.parseSync(code, {
|
|
18
|
+
filename: file,
|
|
19
|
+
presets: ["@babel/preset-typescript", "@babel/preset-react"],
|
|
20
|
+
});
|
|
21
|
+
if (!ast)
|
|
22
|
+
continue;
|
|
23
|
+
let modified = false;
|
|
24
|
+
traverse(ast, {
|
|
25
|
+
CallExpression(path) {
|
|
26
|
+
const callee = path.node.callee;
|
|
27
|
+
const args = path.node.arguments;
|
|
28
|
+
if (t.isIdentifier(callee, { name: fnName }) &&
|
|
29
|
+
args.length === 1 &&
|
|
30
|
+
t.isStringLiteral(args[0])) {
|
|
31
|
+
const key = args[0].value;
|
|
32
|
+
const originalText = translationMap[key];
|
|
33
|
+
if (typeof originalText === "string") {
|
|
34
|
+
const parent = path.parentPath;
|
|
35
|
+
if (parent?.isJSXExpressionContainer()) {
|
|
36
|
+
const containerParent = parent.parentPath;
|
|
37
|
+
if (containerParent?.isJSXAttribute() &&
|
|
38
|
+
containerParent.node.value === parent.node) {
|
|
39
|
+
// If the expression is inside an attribute, set attribute value to string literal
|
|
40
|
+
containerParent.node.value = t.stringLiteral(originalText);
|
|
41
|
+
parent.remove(); // Remove the expression container if still present
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Otherwise, it's a JSX child
|
|
45
|
+
parent.replaceWith(t.jsxText(originalText));
|
|
46
|
+
}
|
|
76
47
|
}
|
|
77
48
|
else {
|
|
78
|
-
//
|
|
79
|
-
|
|
49
|
+
// Not in JSX, just replace with a string literal
|
|
50
|
+
path.replaceWith(t.stringLiteral(originalText));
|
|
80
51
|
}
|
|
52
|
+
modified = true;
|
|
81
53
|
}
|
|
82
|
-
else {
|
|
83
|
-
// Not in JSX, just replace with a string literal
|
|
84
|
-
path.replaceWith(t.stringLiteral(originalText));
|
|
85
|
-
}
|
|
86
|
-
modified = true;
|
|
87
54
|
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (modified) {
|
|
58
|
+
const output = generate(ast).code;
|
|
59
|
+
fs.writeFileSync(file, output, "utf-8");
|
|
60
|
+
}
|
|
94
61
|
}
|
|
95
62
|
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error("[Reverser Error]", err);
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
96
67
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getSourceFiles(dir: string, excludes?: string[]): Promise<string[]>;
|
package/dist/src/scanner.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.getSourceFiles = getSourceFiles;
|
|
4
|
-
const glob_1 = require("glob");
|
|
5
|
-
async function getSourceFiles(dir, excludes = []) {
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
export async function getSourceFiles(dir, excludes = []) {
|
|
6
3
|
console.log(`Scanning dir: ${dir}, excludes: ${excludes}`);
|
|
7
|
-
const files = await
|
|
4
|
+
const files = await glob(`${dir}/**/*.{js,jsx,ts,tsx}`, {
|
|
8
5
|
nodir: true,
|
|
9
6
|
ignore: ['**/node_modules/**', '**/dist/**', ...excludes]
|
|
10
7
|
});
|
package/dist/src/utils.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.generateTranslationKey = generateTranslationKey;
|
|
7
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
-
function generateTranslationKey(text, strategy = 'snake_case') {
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export function generateTranslationKey(text, strategy = 'snake_case') {
|
|
9
3
|
if (typeof strategy === 'function') {
|
|
10
4
|
return strategy(text);
|
|
11
5
|
}
|
|
@@ -13,7 +7,7 @@ function generateTranslationKey(text, strategy = 'snake_case') {
|
|
|
13
7
|
case 'camelCase':
|
|
14
8
|
return toCamelCase(text);
|
|
15
9
|
case 'hash':
|
|
16
|
-
return
|
|
10
|
+
return crypto.createHash('md5').update(text).digest('hex').substring(0, 8);
|
|
17
11
|
case 'snake_case':
|
|
18
12
|
default:
|
|
19
13
|
return toSnakeCase(text);
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-turbo",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Extract hardcoded text from JSX/TSX files and convert to i18n format.",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
6
8
|
"files": [
|
|
7
9
|
"dist",
|
|
8
10
|
"README.md",
|
|
@@ -47,7 +49,20 @@
|
|
|
47
49
|
"minimatch": "^10.1.1",
|
|
48
50
|
"p-limit": "^3.1.0"
|
|
49
51
|
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
54
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"react": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"react-dom": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
50
64
|
"devDependencies": {
|
|
65
|
+
"@types/react": "^19.0.0",
|
|
51
66
|
"@types/babel__core": "^7.20.5",
|
|
52
67
|
"@types/babel__generator": "^7.27.0",
|
|
53
68
|
"@types/babel__traverse": "^7.20.7",
|