i18n-turbo 1.0.0 β†’ 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 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
- - πŸ”’ **Pluralization**: Automatically detects singular/plural patterns in ternary operators.
13
- - πŸ”§ **Configs**: Flexible `i18n-turbo.config.js` for custom key strategies, exclusions, and more.
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 -g i18n-turbo
20
+ npm install i18n-turbo
20
21
  ```
21
22
 
22
- ## Usage
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
- **Translate to French:**
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
- **Dry Run (Preview changes):**
77
+ **Update Translations:**
78
+ Merge new keys without overwriting existing manual translations.
41
79
  ```bash
42
- i18n-turbo ./src ./locales/en.json --dry-run
80
+ npx i18n-turbo ./src ./locales/en.json --merge
43
81
  ```
44
82
 
45
- **Update existing translations:**
83
+ **Force Update:**
84
+ Re-translate all keys (overwrite everything).
46
85
  ```bash
47
- i18n-turbo ./src ./locales/en.json --merge
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 pluralization patterns in your code.
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')` (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
- ```
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
@@ -1,5 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
1
  // bin/cli.ts
4
- const cli_1 = require("../src/cli");
5
- (0, cli_1.runCLI)();
2
+ import { runCLI } from '../src/cli.js';
3
+ runCLI();
@@ -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
- 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
- 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 = path_1.default.resolve(inputDir);
34
- const resolvedOutputFile = path_1.default.resolve(outputFile);
28
+ const resolvedInputDir = path.resolve(inputDir);
29
+ const resolvedOutputFile = path.resolve(outputFile);
35
30
  if (args.includes('--reverse')) {
36
- await (0, reverser_1.reverseStringsFromDirectory)(resolvedInputDir, resolvedOutputFile, fnName);
31
+ await reverseStringsFromDirectory(resolvedInputDir, resolvedOutputFile, fnName);
37
32
  process.exit(0);
38
33
  }
39
- await (0, extractor_1.extractStringsFromDirectory)(resolvedInputDir, resolvedOutputFile, options);
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;
@@ -1,20 +1,14 @@
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"));
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 = path_1.default.join(cwd, 'i18n-turbo.config.js');
17
- if (fs_1.default.existsSync(configPath)) {
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 {};
@@ -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
- const fs_1 = __importDefault(require("fs"));
42
- const fs_2 = require("fs"); // Async FS
43
- const path_1 = __importDefault(require("path"));
44
- const babel = __importStar(require("@babel/core"));
45
- const traverse_1 = __importDefault(require("@babel/traverse"));
46
- const generator_1 = __importDefault(require("@babel/generator"));
47
- const t = __importStar(require("@babel/types"));
48
- const scanner_1 = require("./scanner");
49
- const utils_1 = require("./utils");
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");
53
- async function extractStringsFromDirectory(inputDir, outputFile, options) {
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 = path_1.default.extname(outputFile) ? path_1.default.dirname(outputFile) : outputFile;
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
- ? path_1.default.join(outputDir, `${ns}.json`)
44
+ ? path.join(outputDir, `${ns}.json`)
82
45
  : outputFile; // If no namespaces, use the single output file
83
- if (fs_1.default.existsSync(nsFile)) {
46
+ if (fs.existsSync(nsFile)) {
84
47
  try {
85
- namespaceMaps[ns] = JSON.parse(fs_1.default.readFileSync(nsFile, "utf-8"));
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 (0, scanner_1.getSourceFiles)(inputDir, options.config?.excludePatterns);
94
- const limit = (0, p_limit_1.default)(50); // Concurrency 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 fs_2.promises.readFile(file, "utf-8");
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 = path_1.default.relative(process.cwd(), file); // or relative to inputDir? relative to CWD usually for globs
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 ((0, minimatch_1.minimatch)(relativePath, pattern)) {
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
- (0, traverse_1.default)(ast, {
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 = (0, utils_1.generateTranslationKey)(rawText, options.config?.keyGenerationStrategy);
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 = (0, utils_1.generateTranslationKey)(value, options.config?.keyGenerationStrategy);
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 = (0, generator_1.default)(ast).code;
273
+ const output = generate(ast).code;
280
274
  // Write file async
281
- await fs_2.promises.writeFile(file, output);
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 fs_2.promises.mkdir(outputDir, { recursive: true });
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 = path_1.default.join(outputDir, `${ns}.json`);
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 (0, google_translate_api_1.translate)(text, { to: options.lang });
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
- // 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`);
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 fs_2.promises.writeFile(targetFile, JSON.stringify(mapToWrite, null, 2), "utf-8");
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>;
@@ -1,96 +1,67 @@
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
- async function reverseStringsFromDirectory(inputDir, i18nFile, fnName = "t") {
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
- // Otherwise, it's a JSX child
79
- parent.replaceWith(t.jsxText(originalText));
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
- if (modified) {
92
- const output = (0, generator_1.default)(ast).code;
93
- fs_1.default.writeFileSync(file, output, "utf-8");
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[]>;
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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 (0, glob_1.glob)(`${dir}/**/*.{js,jsx,ts,tsx}`, {
4
+ const files = await glob(`${dir}/**/*.{js,jsx,ts,tsx}`, {
8
5
  nodir: true,
9
6
  ignore: ['**/node_modules/**', '**/dist/**', ...excludes]
10
7
  });
@@ -0,0 +1,2 @@
1
+ export type KeyStrategy = 'snake_case' | 'camelCase' | 'hash';
2
+ export declare function generateTranslationKey(text: string, strategy?: KeyStrategy | ((text: string) => string)): string;
package/dist/src/utils.js CHANGED
@@ -1,11 +1,5 @@
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.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 crypto_1.default.createHash('md5').update(text).digest('hex').substring(0, 8);
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.0",
3
+ "version": "1.0.2",
4
4
  "description": "Extract hardcoded text from JSX/TSX files and convert to i18n format.",
5
- "main": "dist/index.js",
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",