i18n-turbo 0.1.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/LICENSE ADDED
@@ -0,0 +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.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # i18n-turbo
2
+
3
+ > Extract hardcoded strings from JSX/TSX/JS/TS files and export them to an i18n JSON format — with optional reverse transformation support.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g i18n-turbo
9
+ ````
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ i18n-turbo <input-dir> <output-en-json> [--dry-run] [--merge] [--lang <code>] [--fn <name>] [--reverse]
15
+ ```
16
+
17
+ ## Options
18
+
19
+ * `--dry-run` — Show what would be replaced, but don’t modify files.
20
+ * `--merge` — Merge new keys with existing `en.json` entries.
21
+ * `--lang <code>` — Automatically translate to the specified language (e.g., `fr`, `es`) and generate `<lang>.json`.
22
+ * `--fn <name>` — Set the i18n function name (default is `t`).
23
+ * `--reverse` — Revert translated keys (e.g., `t("hello_world")`) back to the original hardcoded strings using the translation file.
24
+
25
+ ## Features
26
+
27
+ * Scans `.jsx`, `.tsx`, `.ts`, `.js` files
28
+ * Extracts static text and replaces with `t('key')`
29
+ * Outputs i18n JSON files (e.g., `en.json`, `fr.json`)
30
+ * Reverses translated keys back to raw strings
31
+ * Preserves original file structure
32
+ * Supports dry-run, merge, and auto-translate
33
+
34
+ ## Examples
35
+
36
+ ### Extract mode
37
+
38
+ **Dry run only (preview):**
39
+
40
+ ```bash
41
+ i18n-turbo ./src ./locales/en.json --dry-run
42
+ ```
43
+
44
+ **Translate to French and output `fr.json`:**
45
+
46
+ ```bash
47
+ i18n-turbo ./examples ./locales/en.json --lang fr
48
+ ```
49
+
50
+ **Use a custom function name (e.g. `i18n` instead of `t`):**
51
+
52
+ ```bash
53
+ i18n-turbo ./src ./locales/en.json --fn i18n
54
+ ```
55
+
56
+ ### Reverse mode
57
+
58
+ **Revert translations (e.g. `t("hello_world")`) back to original strings:**
59
+
60
+ ```bash
61
+ i18n-turbo ./examples ./locales/en.json --reverse
62
+ ```
63
+
64
+ **Reverse using a custom i18n function name:**
65
+
66
+ ```bash
67
+ i18n-turbo ./src ./locales/en.json --reverse --fn i18n
68
+ ```
69
+
70
+ > 🔁 The reverse command reads keys from the translation file (e.g. `en.json`) and replaces `t("key")` with the original hardcoded string like `"Hello world"`.
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // bin/cli.ts
4
+ const cli_1 = require("../src/cli");
5
+ (0, cli_1.runCLI)();
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // bin/cli.ts or src/cli.ts
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.runCLI = runCLI;
8
+ const path_1 = __importDefault(require("path"));
9
+ const extractor_1 = require("./extractor");
10
+ const reverser_1 = require("./reverser");
11
+ async function runCLI() {
12
+ const args = process.argv.slice(2);
13
+ const inputDir = args[0] || './src';
14
+ const outputFile = args[1] || './locales/en.json';
15
+ const fnNameIndex = args.indexOf('--fn');
16
+ const fnName = fnNameIndex !== -1 && args[fnNameIndex + 1] ? args[fnNameIndex + 1] : 't';
17
+ const langIndex = args.indexOf('--lang');
18
+ const lang = langIndex !== -1 && args[langIndex + 1] ? args[langIndex + 1] : undefined;
19
+ const options = {
20
+ fnName,
21
+ dryRun: args.includes('--dry-run'),
22
+ merge: args.includes('--merge'),
23
+ lang,
24
+ };
25
+ const resolvedInputDir = path_1.default.resolve(inputDir);
26
+ const resolvedOutputFile = path_1.default.resolve(outputFile);
27
+ if (args.includes('--reverse')) {
28
+ await (0, reverser_1.reverseStringsFromDirectory)(resolvedInputDir, resolvedOutputFile, fnName);
29
+ process.exit(0);
30
+ }
31
+ await (0, extractor_1.extractStringsFromDirectory)(resolvedInputDir, resolvedOutputFile, options);
32
+ }
@@ -0,0 +1,146 @@
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
+ // src/extractor.ts
41
+ const fs_1 = __importDefault(require("fs"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const babel = __importStar(require("@babel/core"));
44
+ const traverse_1 = __importDefault(require("@babel/traverse"));
45
+ const generator_1 = __importDefault(require("@babel/generator"));
46
+ const t = __importStar(require("@babel/types"));
47
+ const scanner_1 = require("./scanner");
48
+ const utils_1 = require("./utils");
49
+ const google_translate_api_1 = require("@vitalets/google-translate-api");
50
+ async function extractStringsFromDirectory(inputDir, outputFile, options) {
51
+ let translationMap = {};
52
+ if (options.merge && fs_1.default.existsSync(outputFile)) {
53
+ try {
54
+ translationMap = JSON.parse(fs_1.default.readFileSync(outputFile, "utf-8"));
55
+ }
56
+ catch {
57
+ console.warn(`[WARN] Could not parse ${outputFile}, starting fresh.`);
58
+ }
59
+ }
60
+ const files = (0, scanner_1.getSourceFiles)(inputDir);
61
+ for (const file of files) {
62
+ const code = fs_1.default.readFileSync(file, "utf-8");
63
+ const ast = babel.parseSync(code, {
64
+ filename: file,
65
+ presets: ["@babel/preset-typescript", "@babel/preset-react"],
66
+ });
67
+ if (!ast)
68
+ continue;
69
+ let modified = false;
70
+ (0, traverse_1.default)(ast, {
71
+ JSXText(path) {
72
+ const rawText = path.node.value.trim();
73
+ if (!rawText || /^\{.*\}$/.test(rawText))
74
+ return;
75
+ const key = (0, utils_1.generateTranslationKey)(rawText);
76
+ translationMap[key] = rawText;
77
+ const replacement = t.jsxExpressionContainer(t.callExpression(t.identifier(options.fnName), [t.stringLiteral(key)]));
78
+ path.replaceWith(replacement);
79
+ modified = true;
80
+ if (options.dryRun) {
81
+ console.log(`[DRY RUN] ${file}: ${rawText} -> ${key}`);
82
+ }
83
+ },
84
+ StringLiteral(path) {
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
+ ]));
106
+ }
107
+ else {
108
+ replacement = t.callExpression(t.identifier(options.fnName), [
109
+ t.stringLiteral(key),
110
+ ]);
111
+ }
112
+ path.replaceWith(replacement);
113
+ modified = true;
114
+ if (options.dryRun) {
115
+ console.log(`[DRY RUN] ${file}: "${value}" -> ${key}`);
116
+ }
117
+ },
118
+ });
119
+ if (!options.dryRun && modified) {
120
+ const output = (0, generator_1.default)(ast).code;
121
+ fs_1.default.writeFileSync(file, output);
122
+ }
123
+ }
124
+ if (!options.dryRun) {
125
+ fs_1.default.mkdirSync(path_1.default.dirname(outputFile), { recursive: true });
126
+ if (options.lang && options.lang !== "en") {
127
+ const translated = {};
128
+ for (const [key, text] of Object.entries(translationMap)) {
129
+ try {
130
+ const res = await (0, google_translate_api_1.translate)(text, { to: options.lang });
131
+ console.log(`Translated "${text}" => "${res.text}"`);
132
+ translated[key] = res.text;
133
+ }
134
+ catch (err) {
135
+ console.warn(`Translation failed for "${text}"`, err);
136
+ translated[key] = text;
137
+ }
138
+ }
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
+ }
145
+ }
146
+ }
@@ -0,0 +1,96 @@
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.reverseStringsFromDirectory = reverseStringsFromDirectory;
40
+ const fs_1 = __importDefault(require("fs"));
41
+ const babel = __importStar(require("@babel/core"));
42
+ const traverse_1 = __importDefault(require("@babel/traverse"));
43
+ const t = __importStar(require("@babel/types"));
44
+ const generator_1 = __importDefault(require("@babel/generator"));
45
+ const scanner_1 = require("./scanner");
46
+ function reverseStringsFromDirectory(inputDir, i18nFile, fnName = "t") {
47
+ const translationMap = JSON.parse(fs_1.default.readFileSync(i18nFile, "utf-8"));
48
+ const files = (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
76
+ }
77
+ else {
78
+ // Otherwise, it's a JSX child
79
+ parent.replaceWith(t.jsxText(originalText));
80
+ }
81
+ }
82
+ else {
83
+ // Not in JSX, just replace with a string literal
84
+ path.replaceWith(t.stringLiteral(originalText));
85
+ }
86
+ modified = true;
87
+ }
88
+ }
89
+ },
90
+ });
91
+ if (modified) {
92
+ const output = (0, generator_1.default)(ast).code;
93
+ fs_1.default.writeFileSync(file, output, "utf-8");
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,43 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getSourceFiles = getSourceFiles;
37
+ // src/scanner.ts
38
+ const glob = __importStar(require("glob"));
39
+ function getSourceFiles(dir) {
40
+ return glob.sync(`${dir}/**/*.{js,jsx,ts,tsx}`, {
41
+ nodir: true
42
+ });
43
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateTranslationKey = generateTranslationKey;
4
+ // src/utils.ts
5
+ function generateTranslationKey(text) {
6
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
7
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "i18n-turbo",
3
+ "version": "0.1.0",
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
+ "keywords": [
21
+ "i18n",
22
+ "translation",
23
+ "react",
24
+ "cli",
25
+ "internationalization"
26
+ ],
27
+ "author": "Lasantha Lakmal",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@babel/core": "^7.0.0",
31
+ "@babel/generator": "^7.0.0",
32
+ "@babel/preset-react": "^7.0.0",
33
+ "@babel/preset-typescript": "^7.0.0",
34
+ "@babel/traverse": "^7.0.0",
35
+ "@babel/types": "^7.0.0",
36
+ "@vitalets/google-translate-api": "^9.2.1",
37
+ "glob": "^10.4.5"
38
+ },
39
+ "devDependencies": {
40
+ "@types/babel__core": "^7.20.5",
41
+ "@types/babel__generator": "^7.27.0",
42
+ "@types/babel__traverse": "^7.20.7",
43
+ "ts-node": "^10.0.0",
44
+ "typescript": "^5.0.0",
45
+ "vitest": "^3.2.4"
46
+ }
47
+ }