i18nizer 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/README.md +144 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/extract.js +98 -0
- package/dist/commands/hello/index.js +19 -0
- package/dist/commands/hello/world.js +14 -0
- package/dist/commands/keys.js +74 -0
- package/dist/core/ai/client.js +77 -0
- package/dist/core/ai/promt.js +30 -0
- package/dist/core/ast/extract-text.js +78 -0
- package/dist/core/ast/insert-user-translations.js +42 -0
- package/dist/core/ast/is-translatable.js +38 -0
- package/dist/core/ast/parse-file.js +7 -0
- package/dist/core/ast/replace-text-with-text.js +32 -0
- package/dist/core/i18n/parse-ai-json.js +12 -0
- package/dist/core/i18n/sace-source-file.js +3 -0
- package/dist/core/i18n/write-files.js +17 -0
- package/dist/index.js +1 -0
- package/oclif.manifest.json +165 -0
- package/package.json +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# i18nizer 🌍
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="https://img.shields.io/badge/Node.js-5FA04E?logo=nodedotjs&logoColor=fff&style=for-the-badge" alt="Node.js Badge">
|
|
7
|
+
<img src="https://img.shields.io/badge/oclif-000?logo=oclif&logoColor=fff&style=for-the-badge" alt="oclif Badge">
|
|
8
|
+
<img src="https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff&style=for-the-badge" alt="TypeScript Badge">
|
|
9
|
+
<img alt="Licence" src="https://img.shields.io/npm/dw/i18nizer.svg?style=for-the-badge">
|
|
10
|
+
<img alt="Licence" src="https://img.shields.io/github/license/yossTheDev/i18nizer?style=for-the-badge">
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
i18nizer is a practical CLI to **extract translatable texts from your TSX/JSX components** and automatically generate i18n JSON files.
|
|
14
|
+
It works with **Gemini** and **Hugging Face (DeepSeek)** AI providers to generate translations and prepares your components to use `t()` easily.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🚀 Installation
|
|
19
|
+
|
|
20
|
+
You can install the CLI globally using **npm**:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g i18nizer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> Requires Node.js 18+ and internet access to call the translation APIs.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🛠️ API Keys Configuration
|
|
31
|
+
|
|
32
|
+
Before generating translations, you need to set up your API keys:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
i18nizer keys --setGemini <YOUR_GEMINI_API_KEY>
|
|
36
|
+
i18nizer keys --setHF <YOUR_HUGGING_FACE_API_KEY>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
* Keys are stored securely in your **global user folder** (`~/.mycli/api-keys.json` on Linux/Mac or `C:\Users\<User>\.mycli\api-keys.json` on Windows).
|
|
40
|
+
* To see which keys are set (partially masked):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
i18nizer keys --show
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ⚡ Main Commands
|
|
49
|
+
|
|
50
|
+
### 1. Extract and Generate Translations
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
i18nizer extract <file-path>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Available flags:**
|
|
57
|
+
|
|
58
|
+
* `-l, --locales` → List of locales to generate (default: `en,es`)
|
|
59
|
+
* `-p, --provider` → AI provider to generate translations (optional, default: `huggingface`). Options: `gemini`, `huggingface`
|
|
60
|
+
|
|
61
|
+
**Example:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
i18nizer extract src/components/Button.tsx --locales en,es,fr --provider gemini
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**What this command does:**
|
|
68
|
+
|
|
69
|
+
* Extracts all translatable texts from your JSX/TSX file.
|
|
70
|
+
* Generates translations using the selected AI provider.
|
|
71
|
+
* Automatically inserts `t()` calls in the component.
|
|
72
|
+
* Saves JSON files in `messages/<locale>/<component>.json`.
|
|
73
|
+
* Logs the paths of generated files.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 2. Manage API Keys
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
i18nizer keys
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Available flags:**
|
|
84
|
+
|
|
85
|
+
* `--setGemini` → Set your Gemini key
|
|
86
|
+
* `--setHF` → Set your Hugging Face key
|
|
87
|
+
* `--show` → Show stored keys (masked)
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 💡 Current Features
|
|
92
|
+
|
|
93
|
+
* Compatible with **JSX and TSX**
|
|
94
|
+
* Works with AI providers: **Gemini**, **Hugging Face (DeepSeek)**
|
|
95
|
+
* Automatically inserts `t()` calls in components
|
|
96
|
+
* Generates JSON files for each locale
|
|
97
|
+
* Spinner and colored logs with emojis for better UX
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 🔮 Future Ideas
|
|
102
|
+
|
|
103
|
+
* Configurable **output directory** for JSON files
|
|
104
|
+
* Support for other frameworks and file types (Vue, Svelte, etc.)
|
|
105
|
+
* Better integration with `next-intl` or `react-i18next`
|
|
106
|
+
* Keychain / Credential Manager support for storing API keys securely
|
|
107
|
+
* Multi-project support and locale presets
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📂 Output Structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
messages/
|
|
115
|
+
├─ en/
|
|
116
|
+
│ └─ Button.json
|
|
117
|
+
├─ es/
|
|
118
|
+
│ └─ Button.json
|
|
119
|
+
└─ fr/
|
|
120
|
+
└─ Button.json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Each JSON contains a namespace with AI-generated keys:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"button": {
|
|
128
|
+
"submitText": "Submit",
|
|
129
|
+
"cancelText": "Cancel"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## ⚠️ Notes
|
|
137
|
+
|
|
138
|
+
* For the first version, JSON files are saved **inside the project** under `messages/<locale>/`.
|
|
139
|
+
* Make sure **not to commit your API keys** to the repository.
|
|
140
|
+
* The CLI is designed to be simple and functional, ideal for starting automatic i18n in React projects.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
> Made with ❤️ by Yoannis Sánchez Soto
|
package/bin/dev.cmd
ADDED
package/bin/dev.js
ADDED
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { generateTranslations } from "../core/ai/client.js";
|
|
6
|
+
import { buildPrompt } from "../core/ai/promt.js";
|
|
7
|
+
import { extractTexts } from "../core/ast/extract-text.js";
|
|
8
|
+
import { insertUseTranslations } from "../core/ast/insert-user-translations.js";
|
|
9
|
+
import { parseFile } from "../core/ast/parse-file.js";
|
|
10
|
+
import { replaceTempKeysWithT } from "../core/ast/replace-text-with-text.js";
|
|
11
|
+
import { parseAiJson } from "../core/i18n/parse-ai-json.js";
|
|
12
|
+
import { saveSourceFile } from "../core/i18n/sace-source-file.js";
|
|
13
|
+
import { writeLocaleFiles } from "../core/i18n/write-files.js";
|
|
14
|
+
const VALID_PROVIDERS = ["gemini", "huggingface", "openai"];
|
|
15
|
+
export default class Extract extends Command {
|
|
16
|
+
static args = {
|
|
17
|
+
file: Args.string({
|
|
18
|
+
description: "Path to the TSX/JSX file",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static description = "🌍 Extract translatable strings from a TSX/JSX file and generate i18n JSON";
|
|
23
|
+
static flags = {
|
|
24
|
+
locales: Flags.string({
|
|
25
|
+
char: "l",
|
|
26
|
+
default: "en,es",
|
|
27
|
+
description: "Locales to generate",
|
|
28
|
+
}),
|
|
29
|
+
provider: Flags.string({
|
|
30
|
+
char: "p",
|
|
31
|
+
description: "AI provider (gemini | huggingface), optional",
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(Extract);
|
|
36
|
+
this.log(chalk.cyan("📄 File:"), args.file);
|
|
37
|
+
this.log(chalk.cyan("🌐 Locales:"), flags.locales);
|
|
38
|
+
let provider = "huggingface";
|
|
39
|
+
if (flags.provider) {
|
|
40
|
+
const p = flags.provider.toLowerCase();
|
|
41
|
+
if (!VALID_PROVIDERS.includes(p)) {
|
|
42
|
+
this.error(`❌ Invalid provider: ${flags.provider}. Valid options: ${VALID_PROVIDERS.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
provider = p;
|
|
45
|
+
}
|
|
46
|
+
this.log(chalk.cyan("🤖 Provider:"), provider);
|
|
47
|
+
// Parse file
|
|
48
|
+
const sourceFile = parseFile(args.file);
|
|
49
|
+
const texts = extractTexts(sourceFile);
|
|
50
|
+
if (texts.length === 0) {
|
|
51
|
+
this.log(chalk.yellow("⚠️ No translatable texts found."));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.log(`🔍 Found ${chalk.green(texts.length)} translatable texts`);
|
|
55
|
+
const componentName = path
|
|
56
|
+
.basename(args.file)
|
|
57
|
+
.replace(/\.(tsx|jsx)$/, "");
|
|
58
|
+
const locales = flags.locales.split(",");
|
|
59
|
+
const spinner = ora(`💬 Generating translations with ${provider}...`).start();
|
|
60
|
+
try {
|
|
61
|
+
const prompt = buildPrompt({
|
|
62
|
+
componentName,
|
|
63
|
+
locales,
|
|
64
|
+
texts: texts.map((t) => t.text),
|
|
65
|
+
});
|
|
66
|
+
// Pasamos el provider a la función
|
|
67
|
+
const raw = await generateTranslations(prompt, provider);
|
|
68
|
+
if (!raw)
|
|
69
|
+
throw new Error("AI did not return any data");
|
|
70
|
+
const json = parseAiJson(raw);
|
|
71
|
+
writeLocaleFiles(componentName, json, locales);
|
|
72
|
+
spinner.succeed(`✅ Translations generated with ${provider}`);
|
|
73
|
+
const namespace = componentName.toLowerCase();
|
|
74
|
+
const aiGeneratedKeys = Object.keys(json[namespace] || {});
|
|
75
|
+
const mapped = texts.map((e, i) => ({
|
|
76
|
+
key: aiGeneratedKeys[i],
|
|
77
|
+
node: e.node,
|
|
78
|
+
placeholders: e.placeholders,
|
|
79
|
+
tempKey: e.tempKey,
|
|
80
|
+
}));
|
|
81
|
+
this.log(`🔗 Mapped ${chalk.green(mapped.length)} texts to keys`);
|
|
82
|
+
insertUseTranslations(sourceFile, componentName);
|
|
83
|
+
replaceTempKeysWithT(mapped);
|
|
84
|
+
saveSourceFile(sourceFile);
|
|
85
|
+
this.log(chalk.green("✨ Component rewritten with t() calls"));
|
|
86
|
+
this.log(chalk.green(`🌍 JSON files generated using ${provider}`));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
spinner.fail(`❌ Failed to generate translations with ${provider}`);
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
this.error(error.message);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.error("An unknown error occurred");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
export default class Hello extends Command {
|
|
3
|
+
static args = {
|
|
4
|
+
person: Args.string({ description: 'Person to say hello to', required: true }),
|
|
5
|
+
};
|
|
6
|
+
static description = 'Say hello';
|
|
7
|
+
static examples = [
|
|
8
|
+
`<%= config.bin %> <%= command.id %> friend --from oclif
|
|
9
|
+
hello friend from oclif! (./src/commands/hello/index.ts)
|
|
10
|
+
`,
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
from: Flags.string({ char: 'f', description: 'Who is saying hello', required: true }),
|
|
14
|
+
};
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(Hello);
|
|
17
|
+
this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class World extends Command {
|
|
3
|
+
static args = {};
|
|
4
|
+
static description = 'Say hello world';
|
|
5
|
+
static examples = [
|
|
6
|
+
`<%= config.bin %> <%= command.id %>
|
|
7
|
+
hello world! (./src/commands/hello/world.ts)
|
|
8
|
+
`,
|
|
9
|
+
];
|
|
10
|
+
static flags = {};
|
|
11
|
+
async run() {
|
|
12
|
+
this.log('hello world! (./src/commands/hello/world.ts)');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), ".18nizer");
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "api-keys.json");
|
|
9
|
+
export default class Keys extends Command {
|
|
10
|
+
static description = "Manage API keys for your CLI";
|
|
11
|
+
static flags = {
|
|
12
|
+
setGemini: Flags.string({
|
|
13
|
+
char: "g",
|
|
14
|
+
description: "Set Google Gemini API key",
|
|
15
|
+
}),
|
|
16
|
+
setHF: Flags.string({
|
|
17
|
+
char: "h",
|
|
18
|
+
description: "Set Hugging Face API key",
|
|
19
|
+
}),
|
|
20
|
+
setOpenAI: Flags.string({
|
|
21
|
+
char: "o",
|
|
22
|
+
description: "Set OpenAI API key",
|
|
23
|
+
}),
|
|
24
|
+
show: Flags.boolean({
|
|
25
|
+
char: "s",
|
|
26
|
+
description: "Show saved keys (masked)",
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
async run() {
|
|
30
|
+
const { flags } = await this.parse(Keys);
|
|
31
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
32
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
let keys = {};
|
|
35
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
36
|
+
try {
|
|
37
|
+
keys = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
this.error("❌ Could not read existing keys file.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Set keys
|
|
44
|
+
if (flags.setGemini) {
|
|
45
|
+
keys.gemini = flags.setGemini;
|
|
46
|
+
this.log(chalk.green("✅ Gemini API key set"));
|
|
47
|
+
}
|
|
48
|
+
if (flags.setHF) {
|
|
49
|
+
keys.huggingface = flags.setHF;
|
|
50
|
+
this.log(chalk.green("✅ Hugging Face API key set"));
|
|
51
|
+
}
|
|
52
|
+
if (flags.setOpenAI) {
|
|
53
|
+
keys.openai = flags.setOpenAI;
|
|
54
|
+
this.log(chalk.green("✅ OpenAI API key set"));
|
|
55
|
+
}
|
|
56
|
+
// Save keys
|
|
57
|
+
if (flags.setGemini || flags.setHF || flags.setOpenAI) {
|
|
58
|
+
const spinner = ora("Saving keys...").start();
|
|
59
|
+
try {
|
|
60
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
|
61
|
+
spinner.succeed(`💾 Keys saved successfully at ${CONFIG_FILE}`);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
spinner.fail("❌ Failed to save keys");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Show keys (masked)
|
|
68
|
+
if (flags.show) {
|
|
69
|
+
this.log("🔑 Saved API keys:");
|
|
70
|
+
this.log("Gemini: ", keys.gemini ? keys.gemini.slice(0, 4) + "****" : chalk.yellow("not set"));
|
|
71
|
+
this.log("Hugging Face: ", keys.huggingface ? keys.huggingface.slice(0, 4) + "****" : chalk.yellow("not set"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import { InferenceClient as HFClient } from "@huggingface/inference";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import OpenAI from "openai";
|
|
7
|
+
const CONFIG_FILE = path.join(os.homedir(), ".18nizer", "api-keys.json");
|
|
8
|
+
function loadApiKeys() {
|
|
9
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
10
|
+
console.warn(`⚠️ API keys file not found: ${CONFIG_FILE}`);
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const keys = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
15
|
+
return keys;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error("❌ Error reading API keys:", error);
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function generateTranslations(prompt, provider = "huggingface") {
|
|
23
|
+
const keys = loadApiKeys();
|
|
24
|
+
switch (provider) {
|
|
25
|
+
case "gemini": {
|
|
26
|
+
const apiKey = keys.gemini;
|
|
27
|
+
if (!apiKey)
|
|
28
|
+
throw new Error("Gemini API key is not set.");
|
|
29
|
+
console.log("🤖 Using Google Gemini...");
|
|
30
|
+
const gemini = new GoogleGenAI({ apiKey });
|
|
31
|
+
const result = await gemini.models.generateContent({
|
|
32
|
+
contents: prompt,
|
|
33
|
+
model: "gemini-2.5-flash",
|
|
34
|
+
});
|
|
35
|
+
return result.text;
|
|
36
|
+
}
|
|
37
|
+
case "huggingface": {
|
|
38
|
+
const apiKey = keys.huggingface;
|
|
39
|
+
if (!apiKey)
|
|
40
|
+
throw new Error("Hugging Face API key is not set.");
|
|
41
|
+
console.log("🤖 Using Hugging Face (DeepSeek-V3.2)...");
|
|
42
|
+
const hfClient = new HFClient(apiKey);
|
|
43
|
+
try {
|
|
44
|
+
const chatCompletion = await hfClient.chatCompletion({
|
|
45
|
+
messages: [{ content: prompt, role: "user" }],
|
|
46
|
+
model: "deepseek-ai/DeepSeek-V3.2",
|
|
47
|
+
});
|
|
48
|
+
return chatCompletion.choices?.[0]?.message?.content || (typeof chatCompletion.output_text === "string" ? chatCompletion.output_text : undefined);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error("❌ Error calling Hugging Face:", error);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
case "openai": {
|
|
56
|
+
const apiKey = keys.openai;
|
|
57
|
+
if (!apiKey)
|
|
58
|
+
throw new Error("OpenAI API key is not set.");
|
|
59
|
+
console.log("🤖 Using OpenAI...");
|
|
60
|
+
const openai = new OpenAI({ apiKey });
|
|
61
|
+
try {
|
|
62
|
+
const completion = await openai.chat.completions.create({
|
|
63
|
+
messages: [{ content: prompt, role: "user" }],
|
|
64
|
+
model: "gpt-4o-mini",
|
|
65
|
+
});
|
|
66
|
+
return completion.choices?.[0]?.message?.content || "";
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("❌ Error calling OpenAI:", error);
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
throw new Error("Provider not supported");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function buildPrompt({ componentName, locales, texts, }) {
|
|
2
|
+
return `
|
|
3
|
+
You are an i18n automation tool.
|
|
4
|
+
|
|
5
|
+
TASK:
|
|
6
|
+
Generate translation keys and translations for a React component.
|
|
7
|
+
|
|
8
|
+
RULES:
|
|
9
|
+
- Keys must be camelCase
|
|
10
|
+
- Namespace must be "${componentName}"
|
|
11
|
+
- Languages: ${locales.join(", ")}
|
|
12
|
+
- Do NOT invent or modify meaning
|
|
13
|
+
- Do NOT add explanations
|
|
14
|
+
- Do NOT add markdown
|
|
15
|
+
- Output ONLY valid JSON
|
|
16
|
+
|
|
17
|
+
FORMAT EXACTLY:
|
|
18
|
+
{
|
|
19
|
+
"${componentName}": {
|
|
20
|
+
"keyName": {
|
|
21
|
+
"${locales[0]}": "...",
|
|
22
|
+
"${locales[1]}": "..."
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TEXTS:
|
|
28
|
+
${texts.map((t) => `"${t}"`).join("\n")}
|
|
29
|
+
`.trim();
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Node } from "ts-morph";
|
|
2
|
+
let tempIdCounter = 0;
|
|
3
|
+
const allowedFunctions = new Set(["alert", "confirm", "prompt"]);
|
|
4
|
+
function processTemplateLiteral(node) {
|
|
5
|
+
if (Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
6
|
+
return { placeholders: [], text: node.getLiteralText() };
|
|
7
|
+
}
|
|
8
|
+
if (Node.isTemplateExpression(node)) {
|
|
9
|
+
let text = node.getHead().getLiteralText();
|
|
10
|
+
const placeholders = [];
|
|
11
|
+
for (const span of node.getTemplateSpans()) {
|
|
12
|
+
const exprText = span.getExpression().getText();
|
|
13
|
+
const literalText = span.getLiteral().getLiteralText();
|
|
14
|
+
text += `{${exprText}}${literalText}`;
|
|
15
|
+
placeholders.push(exprText);
|
|
16
|
+
}
|
|
17
|
+
return { placeholders, text };
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function extractTexts(sourceFile) {
|
|
22
|
+
const results = [];
|
|
23
|
+
sourceFile.forEachDescendant((node) => {
|
|
24
|
+
// JSX TEXT simple
|
|
25
|
+
if (Node.isJsxText(node)) {
|
|
26
|
+
const text = node.getText().trim();
|
|
27
|
+
if (text) {
|
|
28
|
+
const tempKey = `i$fdw_${tempIdCounter++}`;
|
|
29
|
+
results.push({ node, placeholders: [], tempKey, text });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// JSX Expression con TemplateLiteral
|
|
33
|
+
if (Node.isJsxExpression(node)) {
|
|
34
|
+
const expr = node.getExpression();
|
|
35
|
+
if (expr && (Node.isTemplateExpression(expr) || Node.isNoSubstitutionTemplateLiteral(expr))) {
|
|
36
|
+
const processed = processTemplateLiteral(expr);
|
|
37
|
+
if (processed) {
|
|
38
|
+
const tempKey = `i$fdw_${tempIdCounter++}`;
|
|
39
|
+
results.push({ node: expr, placeholders: processed.placeholders, tempKey, text: processed.text });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// STRING LITERALS
|
|
44
|
+
if (Node.isStringLiteral(node)) {
|
|
45
|
+
const text = node.getLiteralText();
|
|
46
|
+
const parent = node.getParent();
|
|
47
|
+
// JSX Attributes permitidos
|
|
48
|
+
const allowedProps = ["placeholder", "title", "alt", "aria-label"];
|
|
49
|
+
if (Node.isJsxAttribute(parent) && allowedProps.includes(parent.getNameNode().getText())) {
|
|
50
|
+
const tempKey = `i$fdw_${tempIdCounter++}`;
|
|
51
|
+
results.push({ node, placeholders: [], tempKey, text });
|
|
52
|
+
}
|
|
53
|
+
// Funciones tipo alert, confirm, prompt
|
|
54
|
+
if (Node.isCallExpression(parent)) {
|
|
55
|
+
const fnName = parent.getExpression().getText();
|
|
56
|
+
if (allowedFunctions.has(fnName)) {
|
|
57
|
+
const tempKey = `i$fdw_${tempIdCounter++}`;
|
|
58
|
+
results.push({ node, placeholders: [], tempKey, text });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// TEMPLATE LITERALS para alert/confirm/prompt
|
|
63
|
+
if (Node.isTemplateExpression(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
64
|
+
const parent = node.getParent();
|
|
65
|
+
if (Node.isCallExpression(parent)) {
|
|
66
|
+
const fnName = parent.getExpression().getText();
|
|
67
|
+
if (allowedFunctions.has(fnName)) {
|
|
68
|
+
const processed = processTemplateLiteral(node);
|
|
69
|
+
if (processed) {
|
|
70
|
+
const tempKey = `i$fdw_${tempIdCounter++}`;
|
|
71
|
+
results.push({ node, placeholders: processed.placeholders, tempKey, text: processed.text });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SyntaxKind, VariableDeclarationKind } from "ts-morph";
|
|
2
|
+
export function insertUseTranslations(sourceFile, namespace) {
|
|
3
|
+
const defaultExport = sourceFile.getDefaultExportSymbol();
|
|
4
|
+
if (!defaultExport)
|
|
5
|
+
return;
|
|
6
|
+
const declarations = defaultExport.getDeclarations();
|
|
7
|
+
if (declarations.length === 0)
|
|
8
|
+
return;
|
|
9
|
+
const decl = declarations[0];
|
|
10
|
+
// Function Component tradicional
|
|
11
|
+
if (decl.getKind() === SyntaxKind.FunctionDeclaration) {
|
|
12
|
+
const body = decl.asKind(SyntaxKind.FunctionDeclaration)?.getBody();
|
|
13
|
+
if (!body)
|
|
14
|
+
return;
|
|
15
|
+
if (!body.getVariableStatements().some(vs => vs.getDeclarations().some(d => d.getName() === "t"))) {
|
|
16
|
+
body.insertVariableStatement(0, {
|
|
17
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
18
|
+
declarations: [{ initializer: `useTranslations("${namespace}")`, name: "t" }],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Arrow Function Component
|
|
24
|
+
if (decl.getKind() === SyntaxKind.VariableDeclaration) {
|
|
25
|
+
const initializer = decl.getInitializer();
|
|
26
|
+
if (!initializer)
|
|
27
|
+
return;
|
|
28
|
+
// Si es arrow function
|
|
29
|
+
if (initializer.getKind() === SyntaxKind.ArrowFunction) {
|
|
30
|
+
const body = initializer.asKind(SyntaxKind.ArrowFunction)?.getBody();
|
|
31
|
+
if (!body)
|
|
32
|
+
return;
|
|
33
|
+
// Si es un bloque {}
|
|
34
|
+
if (body.getKind() === SyntaxKind.Block && !body.getVariableStatements().some(vs => vs.getDeclarations().some(d => d.getName() === "t"))) {
|
|
35
|
+
body.insertVariableStatement(0, {
|
|
36
|
+
declarationKind: VariableDeclarationKind.Const, declarations: [{ initializer: `useTranslations("${namespace}")`, name: "t" }],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
decl.setInitializer(`() => { const t = useTranslations("${namespace}"); return ${body.getText()}; }`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Node } from "ts-morph";
|
|
2
|
+
const IGNORED_PROPS = new Set([
|
|
3
|
+
"class",
|
|
4
|
+
"className",
|
|
5
|
+
"id",
|
|
6
|
+
"key",
|
|
7
|
+
]);
|
|
8
|
+
export function isTranslatableString(node, text) {
|
|
9
|
+
const parent = node.getParent();
|
|
10
|
+
// use client / use server
|
|
11
|
+
if (text === "use client" || text === "use server") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
// imports
|
|
15
|
+
if (Node.isImportDeclaration(parent))
|
|
16
|
+
return false;
|
|
17
|
+
// JSX attributes (className, id, etc)
|
|
18
|
+
if (Node.isJsxAttribute(parent)) {
|
|
19
|
+
const name = parent.getNameNode().getText();
|
|
20
|
+
if (IGNORED_PROPS.has(name)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Tailwind heurística (simple y efectiva)
|
|
25
|
+
if (/^(flex|grid|gap-|bg-|text-|p-|m-|w-|h-)/.test(text)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// paths
|
|
29
|
+
if (text.startsWith("/") || text.startsWith("./")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// console.*
|
|
33
|
+
if (Node.isCallExpression(parent) &&
|
|
34
|
+
parent.getExpression().getText().startsWith("console.")) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Node } from "ts-morph";
|
|
2
|
+
const allowedProps = new Set(["alt", "aria-label", "placeholder", "title"]);
|
|
3
|
+
const allowedFunctions = new Set(["alert", "confirm", "prompt"]);
|
|
4
|
+
export function replaceTempKeysWithT(mapped) {
|
|
5
|
+
for (const { key, node, placeholders = [] } of mapped) {
|
|
6
|
+
const placeholdersText = placeholders.length > 0
|
|
7
|
+
? `{ ${placeholders.map(p => `${p}: ${p}`).join(", ")} }`
|
|
8
|
+
: "";
|
|
9
|
+
if (Node.isJsxText(node)) {
|
|
10
|
+
// JSXText → {t("key")}
|
|
11
|
+
node.replaceWithText(`{t("${key}"${placeholdersText ? `, ${placeholdersText}` : ""})}`);
|
|
12
|
+
}
|
|
13
|
+
else if (Node.isStringLiteral(node)) {
|
|
14
|
+
const parent = node.getParent();
|
|
15
|
+
if (Node.isJsxAttribute(parent) && allowedProps.has(parent.getNameNode().getText())) {
|
|
16
|
+
// Props de JSX → {t("key")}
|
|
17
|
+
node.replaceWithText(`{t("${key}"${placeholdersText ? `, ${placeholdersText}` : ""})}`);
|
|
18
|
+
}
|
|
19
|
+
else if (Node.isCallExpression(parent) && allowedFunctions.has(parent.getExpression().getText())) {
|
|
20
|
+
// Literal dentro de alert/confirm/prompt → t("key", { ... })
|
|
21
|
+
node.replaceWithText(`t("${key}"${placeholdersText ? `, ${placeholdersText}` : ""})`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (Node.isNoSubstitutionTemplateLiteral(node) || Node.isTemplateExpression(node)) {
|
|
25
|
+
const parent = node.getParent();
|
|
26
|
+
if (Node.isCallExpression(parent) && allowedFunctions.has(parent.getExpression().getText())) {
|
|
27
|
+
// Template literal dentro de alert/confirm/prompt
|
|
28
|
+
node.replaceWithText(`t("${key}"${placeholdersText ? `, ${placeholdersText}` : ""})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function writeLocaleFiles(namespace, data, locales) {
|
|
5
|
+
for (const locale of locales) {
|
|
6
|
+
const content = {};
|
|
7
|
+
content[namespace] = {};
|
|
8
|
+
for (const key of Object.keys(data[namespace])) {
|
|
9
|
+
content[namespace][key] = data[namespace][key][locale];
|
|
10
|
+
}
|
|
11
|
+
const dir = path.join(process.cwd(), "messages", locale);
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
const filePath = path.join(dir, `${namespace}.json`);
|
|
14
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
|
|
15
|
+
console.log(chalk.green(`💾 Locale file saved: ${filePath}`));
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commands": {
|
|
3
|
+
"extract": {
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"args": {
|
|
6
|
+
"file": {
|
|
7
|
+
"description": "Path to the TSX/JSX file",
|
|
8
|
+
"name": "file",
|
|
9
|
+
"required": true
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"description": "🌍 Extract translatable strings from a TSX/JSX file and generate i18n JSON",
|
|
13
|
+
"flags": {
|
|
14
|
+
"locales": {
|
|
15
|
+
"char": "l",
|
|
16
|
+
"description": "Locales to generate",
|
|
17
|
+
"name": "locales",
|
|
18
|
+
"default": "en,es",
|
|
19
|
+
"hasDynamicHelp": false,
|
|
20
|
+
"multiple": false,
|
|
21
|
+
"type": "option"
|
|
22
|
+
},
|
|
23
|
+
"provider": {
|
|
24
|
+
"char": "p",
|
|
25
|
+
"description": "AI provider (gemini | huggingface), optional",
|
|
26
|
+
"name": "provider",
|
|
27
|
+
"hasDynamicHelp": false,
|
|
28
|
+
"multiple": false,
|
|
29
|
+
"type": "option"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"hasDynamicHelp": false,
|
|
33
|
+
"hiddenAliases": [],
|
|
34
|
+
"id": "extract",
|
|
35
|
+
"pluginAlias": "i18nizer",
|
|
36
|
+
"pluginName": "i18nizer",
|
|
37
|
+
"pluginType": "core",
|
|
38
|
+
"strict": true,
|
|
39
|
+
"enableJsonFlag": false,
|
|
40
|
+
"isESM": true,
|
|
41
|
+
"relativePath": [
|
|
42
|
+
"dist",
|
|
43
|
+
"commands",
|
|
44
|
+
"extract.js"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"keys": {
|
|
48
|
+
"aliases": [],
|
|
49
|
+
"args": {},
|
|
50
|
+
"description": "Manage API keys for your CLI",
|
|
51
|
+
"flags": {
|
|
52
|
+
"setGemini": {
|
|
53
|
+
"char": "g",
|
|
54
|
+
"description": "Set Google Gemini API key",
|
|
55
|
+
"name": "setGemini",
|
|
56
|
+
"hasDynamicHelp": false,
|
|
57
|
+
"multiple": false,
|
|
58
|
+
"type": "option"
|
|
59
|
+
},
|
|
60
|
+
"setHF": {
|
|
61
|
+
"char": "h",
|
|
62
|
+
"description": "Set Hugging Face API key",
|
|
63
|
+
"name": "setHF",
|
|
64
|
+
"hasDynamicHelp": false,
|
|
65
|
+
"multiple": false,
|
|
66
|
+
"type": "option"
|
|
67
|
+
},
|
|
68
|
+
"setOpenAI": {
|
|
69
|
+
"char": "o",
|
|
70
|
+
"description": "Set OpenAI API key",
|
|
71
|
+
"name": "setOpenAI",
|
|
72
|
+
"hasDynamicHelp": false,
|
|
73
|
+
"multiple": false,
|
|
74
|
+
"type": "option"
|
|
75
|
+
},
|
|
76
|
+
"show": {
|
|
77
|
+
"char": "s",
|
|
78
|
+
"description": "Show saved keys (masked)",
|
|
79
|
+
"name": "show",
|
|
80
|
+
"allowNo": false,
|
|
81
|
+
"type": "boolean"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"hasDynamicHelp": false,
|
|
85
|
+
"hiddenAliases": [],
|
|
86
|
+
"id": "keys",
|
|
87
|
+
"pluginAlias": "i18nizer",
|
|
88
|
+
"pluginName": "i18nizer",
|
|
89
|
+
"pluginType": "core",
|
|
90
|
+
"strict": true,
|
|
91
|
+
"enableJsonFlag": false,
|
|
92
|
+
"isESM": true,
|
|
93
|
+
"relativePath": [
|
|
94
|
+
"dist",
|
|
95
|
+
"commands",
|
|
96
|
+
"keys.js"
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"hello": {
|
|
100
|
+
"aliases": [],
|
|
101
|
+
"args": {
|
|
102
|
+
"person": {
|
|
103
|
+
"description": "Person to say hello to",
|
|
104
|
+
"name": "person",
|
|
105
|
+
"required": true
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"description": "Say hello",
|
|
109
|
+
"examples": [
|
|
110
|
+
"<%= config.bin %> <%= command.id %> friend --from oclif\nhello friend from oclif! (./src/commands/hello/index.ts)\n"
|
|
111
|
+
],
|
|
112
|
+
"flags": {
|
|
113
|
+
"from": {
|
|
114
|
+
"char": "f",
|
|
115
|
+
"description": "Who is saying hello",
|
|
116
|
+
"name": "from",
|
|
117
|
+
"required": true,
|
|
118
|
+
"hasDynamicHelp": false,
|
|
119
|
+
"multiple": false,
|
|
120
|
+
"type": "option"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"hasDynamicHelp": false,
|
|
124
|
+
"hiddenAliases": [],
|
|
125
|
+
"id": "hello",
|
|
126
|
+
"pluginAlias": "i18nizer",
|
|
127
|
+
"pluginName": "i18nizer",
|
|
128
|
+
"pluginType": "core",
|
|
129
|
+
"strict": true,
|
|
130
|
+
"enableJsonFlag": false,
|
|
131
|
+
"isESM": true,
|
|
132
|
+
"relativePath": [
|
|
133
|
+
"dist",
|
|
134
|
+
"commands",
|
|
135
|
+
"hello",
|
|
136
|
+
"index.js"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"hello:world": {
|
|
140
|
+
"aliases": [],
|
|
141
|
+
"args": {},
|
|
142
|
+
"description": "Say hello world",
|
|
143
|
+
"examples": [
|
|
144
|
+
"<%= config.bin %> <%= command.id %>\nhello world! (./src/commands/hello/world.ts)\n"
|
|
145
|
+
],
|
|
146
|
+
"flags": {},
|
|
147
|
+
"hasDynamicHelp": false,
|
|
148
|
+
"hiddenAliases": [],
|
|
149
|
+
"id": "hello:world",
|
|
150
|
+
"pluginAlias": "i18nizer",
|
|
151
|
+
"pluginName": "i18nizer",
|
|
152
|
+
"pluginType": "core",
|
|
153
|
+
"strict": true,
|
|
154
|
+
"enableJsonFlag": false,
|
|
155
|
+
"isESM": true,
|
|
156
|
+
"relativePath": [
|
|
157
|
+
"dist",
|
|
158
|
+
"commands",
|
|
159
|
+
"hello",
|
|
160
|
+
"world.js"
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"version": "0.1.0"
|
|
165
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "i18nizer",
|
|
3
|
+
"description": "CLI to extract texts from JSX/TSX and generate i18n JSON with AI translations",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": "Yoannis Sanchez Soto",
|
|
6
|
+
"bin": "./bin/run.js",
|
|
7
|
+
"bugs": "https://github.com/tools/i18nizer/issues",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@google/genai": "^1.34.0",
|
|
10
|
+
"@huggingface/inference": "^4.13.5",
|
|
11
|
+
"@oclif/core": "^4",
|
|
12
|
+
"@oclif/plugin-help": "^6",
|
|
13
|
+
"@oclif/plugin-plugins": "^5",
|
|
14
|
+
"chalk": "^5.6.2",
|
|
15
|
+
"node-fetch": "^3.3.2",
|
|
16
|
+
"openai": "^6.15.0",
|
|
17
|
+
"ora": "^9.0.0",
|
|
18
|
+
"ts-morph": "^27.0.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/compat": "^1",
|
|
22
|
+
"@oclif/prettier-config": "^0.2.1",
|
|
23
|
+
"@oclif/test": "^4",
|
|
24
|
+
"@types/chai": "^4",
|
|
25
|
+
"@types/mocha": "^10",
|
|
26
|
+
"@types/node": "^18",
|
|
27
|
+
"chai": "^4",
|
|
28
|
+
"eslint": "^9",
|
|
29
|
+
"eslint-config-oclif": "^6",
|
|
30
|
+
"eslint-config-prettier": "^10",
|
|
31
|
+
"mocha": "^10",
|
|
32
|
+
"oclif": "^4",
|
|
33
|
+
"shx": "^0.3.3",
|
|
34
|
+
"ts-node": "^10",
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"./bin",
|
|
42
|
+
"./dist",
|
|
43
|
+
"./oclif.manifest.json"
|
|
44
|
+
],
|
|
45
|
+
"homepage": "https://github.com/tools/i18nizer#readme",
|
|
46
|
+
"keywords": [
|
|
47
|
+
"oclif",
|
|
48
|
+
"i18n",
|
|
49
|
+
"cli",
|
|
50
|
+
"translation",
|
|
51
|
+
"ai",
|
|
52
|
+
"generative-ai",
|
|
53
|
+
"google"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"main": "dist/index.js",
|
|
57
|
+
"type": "module",
|
|
58
|
+
"oclif": {
|
|
59
|
+
"bin": "i18nizer",
|
|
60
|
+
"dirname": "i18nizer",
|
|
61
|
+
"commands": "./dist/commands",
|
|
62
|
+
"plugins": [
|
|
63
|
+
"@oclif/plugin-help",
|
|
64
|
+
"@oclif/plugin-plugins"
|
|
65
|
+
],
|
|
66
|
+
"topicSeparator": " ",
|
|
67
|
+
"topics": {
|
|
68
|
+
"hello": {
|
|
69
|
+
"description": "Say hello to the world and others"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"repository": "tools/i18nizer",
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "shx rm -rf dist && tsc -b",
|
|
76
|
+
"lint": "eslint",
|
|
77
|
+
"postpack": "shx rm -f oclif.manifest.json",
|
|
78
|
+
"posttest": "yarn lint",
|
|
79
|
+
"prepack": "oclif manifest && oclif readme",
|
|
80
|
+
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
81
|
+
"version": "oclif readme && git add README.md"
|
|
82
|
+
},
|
|
83
|
+
"types": "dist/index.d.ts",
|
|
84
|
+
"packageManager": "yarn@4.12.0"
|
|
85
|
+
}
|