i18nizer 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands/extract.js
CHANGED
|
@@ -8,6 +8,8 @@ import { extractTexts } from "../core/ast/extract-text.js";
|
|
|
8
8
|
import { insertUseTranslations } from "../core/ast/insert-user-translations.js";
|
|
9
9
|
import { parseFile } from "../core/ast/parse-file.js";
|
|
10
10
|
import { replaceTempKeysWithT } from "../core/ast/replace-text-with-text.js";
|
|
11
|
+
import { TranslationCache } from "../core/cache/translation-cache.js";
|
|
12
|
+
import { Deduplicator } from "../core/deduplication/deduplicator.js";
|
|
11
13
|
import { parseAiJson } from "../core/i18n/parse-ai-json.js";
|
|
12
14
|
import { saveSourceFile } from "../core/i18n/sace-source-file.js";
|
|
13
15
|
import { writeLocaleFiles } from "../core/i18n/write-files.js";
|
|
@@ -30,6 +32,11 @@ export default class Extract extends Command {
|
|
|
30
32
|
char: "p",
|
|
31
33
|
description: "AI provider (gemini | huggingface), optional",
|
|
32
34
|
}),
|
|
35
|
+
"use-ai-keys": Flags.boolean({
|
|
36
|
+
allowNo: true,
|
|
37
|
+
default: true,
|
|
38
|
+
description: "Use AI to generate human-readable keys (default: true)",
|
|
39
|
+
}),
|
|
33
40
|
};
|
|
34
41
|
async run() {
|
|
35
42
|
const { args, flags } = await this.parse(Extract);
|
|
@@ -51,17 +58,58 @@ export default class Extract extends Command {
|
|
|
51
58
|
this.log(chalk.yellow("⚠️ No translatable texts found."));
|
|
52
59
|
return;
|
|
53
60
|
}
|
|
54
|
-
this.log(`🔍 Found ${chalk.green(texts.length)} translatable texts`);
|
|
55
61
|
const componentName = path
|
|
56
62
|
.basename(args.file)
|
|
57
63
|
.replace(/\.(tsx|jsx)$/, "");
|
|
58
64
|
const locales = flags.locales.split(",");
|
|
65
|
+
// Count unique strings
|
|
66
|
+
const uniqueTexts = new Set(texts.map((t) => t.text));
|
|
67
|
+
this.log(`🔍 Extracting strings... (${chalk.green(texts.length)} found, ${chalk.green(uniqueTexts.size)} unique)`);
|
|
68
|
+
// Initialize cache and deduplicator
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const projectDir = path.join(cwd, ".i18nizer");
|
|
71
|
+
const cache = new TranslationCache(projectDir);
|
|
72
|
+
const deduplicator = new Deduplicator(cache, flags["use-ai-keys"], provider);
|
|
73
|
+
// Step 1: Generate keys (batch, cache-first)
|
|
74
|
+
let keySpinner;
|
|
75
|
+
if (flags["use-ai-keys"]) {
|
|
76
|
+
keySpinner = ora("🧠 Generating keys (batch)...").start();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.log("🔑 Generating keys (deterministic)...");
|
|
80
|
+
}
|
|
81
|
+
const textList = texts.map((t) => t.text);
|
|
82
|
+
const deduplicationResults = await deduplicator.deduplicateBatch(textList, componentName, false // Standalone extraction: generate fresh keys without reusing from cache
|
|
83
|
+
);
|
|
84
|
+
const stats = deduplicator.getStats();
|
|
85
|
+
if (keySpinner) {
|
|
86
|
+
keySpinner.succeed(`🧠 Generated ${chalk.green(uniqueTexts.size)} keys`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.log(`✅ Generated ${chalk.green(uniqueTexts.size)} keys (deterministic)`);
|
|
90
|
+
}
|
|
91
|
+
this.log(`💾 Cache hits: ${chalk.green(stats.cacheHits)}`);
|
|
92
|
+
this.log(`🤖 AI requests used: ${chalk.green(stats.aiRequestsUsed)}`);
|
|
93
|
+
// Map results back to texts
|
|
94
|
+
const mappedTexts = texts.map((t) => {
|
|
95
|
+
const result = deduplicationResults.get(t.text);
|
|
96
|
+
return {
|
|
97
|
+
isCached: result.isCached,
|
|
98
|
+
key: result.key,
|
|
99
|
+
node: t.node,
|
|
100
|
+
placeholders: t.placeholders,
|
|
101
|
+
tempKey: t.tempKey,
|
|
102
|
+
text: t.text,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
// Step 2: Generate translations (batch, separate from key generation)
|
|
59
106
|
const spinner = ora(`💬 Generating translations with ${provider}...`).start();
|
|
60
107
|
try {
|
|
108
|
+
// Build prompt with the keys we already generated
|
|
61
109
|
const prompt = buildPrompt({
|
|
62
110
|
componentName,
|
|
63
111
|
locales,
|
|
64
|
-
texts:
|
|
112
|
+
texts: mappedTexts.map((t) => ({ tempKey: t.tempKey, text: t.text })),
|
|
65
113
|
});
|
|
66
114
|
const raw = await generateTranslations(prompt, provider);
|
|
67
115
|
if (!raw)
|
|
@@ -70,32 +118,37 @@ export default class Extract extends Command {
|
|
|
70
118
|
const namespace = componentName;
|
|
71
119
|
const jsonNamespace = json[namespace] || {};
|
|
72
120
|
const i18nJson = {};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
121
|
+
// Use our generated keys, not the AI's keys
|
|
122
|
+
for (const mapped of mappedTexts) {
|
|
123
|
+
const translations = jsonNamespace[mapped.tempKey];
|
|
124
|
+
if (!translations) {
|
|
125
|
+
throw new Error(`No translations found for tempKey: ${mapped.tempKey}`);
|
|
126
|
+
}
|
|
127
|
+
i18nJson[mapped.key] = {};
|
|
76
128
|
for (const locale of locales) {
|
|
77
|
-
i18nJson[key][locale] = translations[locale];
|
|
129
|
+
i18nJson[mapped.key][locale] = translations[locale];
|
|
78
130
|
}
|
|
131
|
+
// Update cache
|
|
132
|
+
cache.set({
|
|
133
|
+
componentName,
|
|
134
|
+
key: mapped.key,
|
|
135
|
+
locales: i18nJson[mapped.key],
|
|
136
|
+
text: mapped.text,
|
|
137
|
+
});
|
|
79
138
|
}
|
|
80
139
|
writeLocaleFiles(componentName, { [componentName]: i18nJson }, locales);
|
|
81
140
|
spinner.succeed(`✅ Translations generated with ${provider}`);
|
|
82
|
-
|
|
83
|
-
const mapped = texts.map(t => {
|
|
84
|
-
const translations = jsonNamespace[t.tempKey];
|
|
85
|
-
if (!translations) {
|
|
86
|
-
throw new Error(`No translations found for tempKey: ${t.tempKey}`);
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
key: translations.key,
|
|
90
|
-
node: t.node,
|
|
91
|
-
placeholders: t.placeholders,
|
|
92
|
-
tempKey: t.tempKey
|
|
93
|
-
};
|
|
94
|
-
});
|
|
95
|
-
this.log(`🔗 Mapped ${chalk.green(mapped.length)} texts to keys`);
|
|
141
|
+
this.log(`🔗 Mapped ${chalk.green(mappedTexts.length)} texts to keys`);
|
|
96
142
|
insertUseTranslations(sourceFile, componentName);
|
|
97
|
-
replaceTempKeysWithT(
|
|
143
|
+
replaceTempKeysWithT(mappedTexts.map((m) => ({
|
|
144
|
+
key: m.key,
|
|
145
|
+
node: m.node,
|
|
146
|
+
placeholders: m.placeholders,
|
|
147
|
+
tempKey: m.tempKey,
|
|
148
|
+
})));
|
|
98
149
|
saveSourceFile(sourceFile);
|
|
150
|
+
// Save cache
|
|
151
|
+
cache.save();
|
|
99
152
|
this.log(chalk.green("✨ Component rewritten with t() calls"));
|
|
100
153
|
this.log(chalk.green(`🌍 JSON files generated using ${provider}`));
|
|
101
154
|
}
|
|
@@ -123,10 +123,13 @@ export default class Translate extends Command {
|
|
|
123
123
|
continue;
|
|
124
124
|
}
|
|
125
125
|
totalExtracted += texts.length;
|
|
126
|
-
// Deduplicate and assign keys
|
|
126
|
+
// Deduplicate and assign keys using batch processing
|
|
127
|
+
const textList = texts.map((t) => t.text);
|
|
127
128
|
// eslint-disable-next-line no-await-in-loop
|
|
128
|
-
const
|
|
129
|
-
|
|
129
|
+
const deduplicationResults = await deduplicator.deduplicateBatch(textList, componentName, config.behavior.detectDuplicates);
|
|
130
|
+
// Map results back to texts
|
|
131
|
+
const mappedTexts = texts.map((t) => {
|
|
132
|
+
const result = deduplicationResults.get(t.text);
|
|
130
133
|
if (result.isReused)
|
|
131
134
|
totalReused++;
|
|
132
135
|
if (result.isCached)
|
|
@@ -139,7 +142,7 @@ export default class Translate extends Command {
|
|
|
139
142
|
tempKey: t.tempKey,
|
|
140
143
|
text: t.text,
|
|
141
144
|
};
|
|
142
|
-
})
|
|
145
|
+
});
|
|
143
146
|
// Build translations JSON
|
|
144
147
|
const i18nJson = {};
|
|
145
148
|
for (const mapped of mappedTexts) {
|
|
@@ -241,14 +244,18 @@ export default class Translate extends Command {
|
|
|
241
244
|
if (!flags["dry-run"]) {
|
|
242
245
|
cache.save();
|
|
243
246
|
}
|
|
247
|
+
// Get statistics from deduplicator
|
|
248
|
+
const stats = deduplicator.getStats();
|
|
244
249
|
// Summary
|
|
245
250
|
this.log("");
|
|
246
251
|
this.log(chalk.green("🎉 Translation complete!"));
|
|
247
252
|
this.log(chalk.cyan("📊 Summary:"));
|
|
248
253
|
this.log(` Files processed: ${chalk.bold(filesToProcess.length)}`);
|
|
249
254
|
this.log(` Strings extracted: ${chalk.bold(totalExtracted)}`);
|
|
255
|
+
this.log(` Unique strings: ${chalk.bold(stats.uniqueStrings)}`);
|
|
250
256
|
this.log(` Keys reused: ${chalk.bold(totalReused)}`);
|
|
251
257
|
this.log(` Cached translations: ${chalk.bold(totalCached)}`);
|
|
258
|
+
this.log(` AI requests (keys): ${chalk.bold(stats.aiRequestsUsed)}`);
|
|
252
259
|
this.log("");
|
|
253
260
|
}
|
|
254
261
|
}
|
|
@@ -1,7 +1,77 @@
|
|
|
1
1
|
import { generateTranslations } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate English camelCase keys for multiple texts in a single AI request
|
|
4
|
+
* This is the preferred method for efficiency
|
|
5
|
+
*
|
|
6
|
+
* @param texts - Array of texts to generate keys for
|
|
7
|
+
* @param provider - AI provider to use
|
|
8
|
+
* @returns Map of text to generated key, or empty map on failure
|
|
9
|
+
*/
|
|
10
|
+
export async function generateEnglishKeysBatch(texts, provider = "huggingface") {
|
|
11
|
+
if (texts.length === 0) {
|
|
12
|
+
return new Map();
|
|
13
|
+
}
|
|
14
|
+
const prompt = `
|
|
15
|
+
You are an i18n key generator. Generate concise, meaningful camelCase keys in English for the following texts.
|
|
16
|
+
|
|
17
|
+
RULES:
|
|
18
|
+
- Output ONLY a JSON object mapping each text to its camelCase key
|
|
19
|
+
- Keys should be 2-4 words maximum
|
|
20
|
+
- Keys must be in English
|
|
21
|
+
- Keys should describe the content/purpose
|
|
22
|
+
- Do NOT include explanations, comments, or any other text
|
|
23
|
+
- Output format: { "original text": "camelCaseKey" }
|
|
24
|
+
|
|
25
|
+
TEXTS:
|
|
26
|
+
${texts.map((t, i) => `${i + 1}. ${JSON.stringify(t)}`).join("\n")}
|
|
27
|
+
|
|
28
|
+
EXAMPLES:
|
|
29
|
+
{ "Bienvenido de nuevo": "welcomeBack", "Por favor inicia sesión": "pleaseSignIn" }
|
|
30
|
+
|
|
31
|
+
OUTPUT (JSON only):`.trim();
|
|
32
|
+
try {
|
|
33
|
+
const response = await generateTranslations(prompt, provider);
|
|
34
|
+
if (!response)
|
|
35
|
+
return new Map();
|
|
36
|
+
// Extract JSON from response (handle cases where AI adds markdown or extra text)
|
|
37
|
+
let jsonText = response.trim();
|
|
38
|
+
// Remove markdown code blocks if present
|
|
39
|
+
if (jsonText.startsWith("```")) {
|
|
40
|
+
jsonText = jsonText.replace(/^```(?:json)?\s*\n/, "").replace(/\n```\s*$/, "");
|
|
41
|
+
}
|
|
42
|
+
const parsed = JSON.parse(jsonText);
|
|
43
|
+
const result = new Map();
|
|
44
|
+
// Validate and add each key
|
|
45
|
+
for (const [text, key] of Object.entries(parsed)) {
|
|
46
|
+
// Validate it's a valid camelCase identifier
|
|
47
|
+
if (typeof key === "string" && /^[a-z][a-zA-Z0-9]*$/.test(key)) {
|
|
48
|
+
result.set(text, key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error("❌ Error generating English keys with AI:", error);
|
|
55
|
+
return new Map();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
2
58
|
/**
|
|
3
59
|
* Generate an English camelCase key from text using AI
|
|
4
60
|
* This ensures keys are always in English regardless of source language
|
|
61
|
+
*
|
|
62
|
+
* @deprecated Use generateEnglishKeysBatch for better efficiency. This method
|
|
63
|
+
* will be removed in a future version. To migrate, collect all texts and call
|
|
64
|
+
* generateEnglishKeysBatch once:
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Before:
|
|
68
|
+
* const key1 = await generateEnglishKey(text1);
|
|
69
|
+
* const key2 = await generateEnglishKey(text2);
|
|
70
|
+
*
|
|
71
|
+
* // After:
|
|
72
|
+
* const keyMap = await generateEnglishKeysBatch([text1, text2]);
|
|
73
|
+
* const key1 = keyMap.get(text1);
|
|
74
|
+
* const key2 = keyMap.get(text2);
|
|
5
75
|
*/
|
|
6
76
|
export async function generateEnglishKey(text, provider = "huggingface") {
|
|
7
77
|
const prompt = `
|
|
@@ -14,7 +84,7 @@ RULES:
|
|
|
14
84
|
- Key should describe the content/purpose
|
|
15
85
|
- Do NOT include quotes, explanations, or any other text
|
|
16
86
|
|
|
17
|
-
TEXT:
|
|
87
|
+
TEXT: ${JSON.stringify(text)}
|
|
18
88
|
|
|
19
89
|
EXAMPLES:
|
|
20
90
|
TEXT: "Bienvenido de nuevo" -> welcomeBack
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateEnglishKeysBatch } from "../ai/generate-english-key.js";
|
|
2
2
|
import { generateKey, generateUniqueKey } from "../i18n/generate-key.js";
|
|
3
3
|
/**
|
|
4
4
|
* Deduplicate strings and assign deterministic keys
|
|
@@ -8,45 +8,96 @@ export class Deduplicator {
|
|
|
8
8
|
provider;
|
|
9
9
|
useAiForKeys;
|
|
10
10
|
usedKeys = new Set();
|
|
11
|
+
seenTexts = new Set(); // Track all texts seen across all batches
|
|
12
|
+
stats = {
|
|
13
|
+
aiRequestsUsed: 0,
|
|
14
|
+
cacheHits: 0,
|
|
15
|
+
totalStrings: 0,
|
|
16
|
+
uniqueStrings: 0,
|
|
17
|
+
};
|
|
11
18
|
constructor(cache, useAiForKeys = true, provider = "huggingface") {
|
|
12
19
|
this.cache = cache;
|
|
13
20
|
this.provider = provider;
|
|
14
21
|
this.useAiForKeys = useAiForKeys;
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
|
-
* Process
|
|
18
|
-
*
|
|
24
|
+
* Process multiple texts in batch and return deterministic keys
|
|
25
|
+
* This is the preferred method for efficiency with AI
|
|
19
26
|
*/
|
|
20
|
-
async
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
async deduplicateBatch(texts, componentName, detectDuplicates) {
|
|
28
|
+
this.stats.totalStrings += texts.length;
|
|
29
|
+
const results = new Map();
|
|
30
|
+
const uncachedTexts = [];
|
|
31
|
+
// First pass: check cache and collect uncached texts
|
|
32
|
+
for (const text of texts) {
|
|
33
|
+
const cached = this.cache.get(text);
|
|
34
|
+
if (cached && detectDuplicates) {
|
|
35
|
+
// Reuse existing key from cache
|
|
36
|
+
this.usedKeys.add(cached.key);
|
|
37
|
+
results.set(text, {
|
|
38
|
+
isCached: true,
|
|
39
|
+
isReused: true,
|
|
40
|
+
key: cached.key,
|
|
41
|
+
});
|
|
42
|
+
this.stats.cacheHits++;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
uncachedTexts.push(text);
|
|
46
|
+
}
|
|
31
47
|
}
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
// Track unique strings across all batches
|
|
49
|
+
for (const text of texts) {
|
|
50
|
+
if (!this.seenTexts.has(text)) {
|
|
51
|
+
this.seenTexts.add(text);
|
|
52
|
+
this.stats.uniqueStrings++;
|
|
53
|
+
}
|
|
38
54
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
// Second pass: generate keys for uncached texts
|
|
56
|
+
if (uncachedTexts.length > 0) {
|
|
57
|
+
let aiKeyMap = new Map();
|
|
58
|
+
if (this.useAiForKeys) {
|
|
59
|
+
// Batch AI request for all uncached texts
|
|
60
|
+
aiKeyMap = await generateEnglishKeysBatch(uncachedTexts, this.provider);
|
|
61
|
+
if (aiKeyMap.size > 0) {
|
|
62
|
+
this.stats.aiRequestsUsed++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Generate keys for remaining uncached texts
|
|
66
|
+
for (const text of uncachedTexts) {
|
|
67
|
+
const aiKey = aiKeyMap.get(text);
|
|
68
|
+
const baseKey = aiKey ?? generateKey(text);
|
|
69
|
+
const uniqueKey = generateUniqueKey(baseKey, this.usedKeys);
|
|
70
|
+
this.usedKeys.add(uniqueKey);
|
|
71
|
+
results.set(text, {
|
|
72
|
+
isCached: false,
|
|
73
|
+
isReused: false,
|
|
74
|
+
key: uniqueKey,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
42
77
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Process a single text and return a deterministic key
|
|
82
|
+
* Reuses existing keys if the text was seen before
|
|
83
|
+
*
|
|
84
|
+
* @deprecated Use deduplicateBatch for better efficiency. This method will be
|
|
85
|
+
* removed in v1.0. To migrate, collect texts and call deduplicateBatch once:
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Before:
|
|
89
|
+
* const result1 = await deduplicator.deduplicate(text1, comp, true);
|
|
90
|
+
* const result2 = await deduplicator.deduplicate(text2, comp, true);
|
|
91
|
+
*
|
|
92
|
+
* // After:
|
|
93
|
+
* const results = await deduplicator.deduplicateBatch([text1, text2], comp, true);
|
|
94
|
+
* const result1 = results.get(text1);
|
|
95
|
+
* const result2 = results.get(text2);
|
|
96
|
+
*/
|
|
97
|
+
async deduplicate(text, componentName, detectDuplicates) {
|
|
98
|
+
// Use batch method with single item for consistency
|
|
99
|
+
const results = await this.deduplicateBatch([text], componentName, detectDuplicates);
|
|
100
|
+
return results.get(text);
|
|
50
101
|
}
|
|
51
102
|
/**
|
|
52
103
|
* Get all used keys
|
|
@@ -54,4 +105,22 @@ export class Deduplicator {
|
|
|
54
105
|
getUsedKeys() {
|
|
55
106
|
return new Set(this.usedKeys);
|
|
56
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Get deduplication statistics
|
|
110
|
+
*/
|
|
111
|
+
getStats() {
|
|
112
|
+
return { ...this.stats };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reset statistics
|
|
116
|
+
*/
|
|
117
|
+
resetStats() {
|
|
118
|
+
this.stats = {
|
|
119
|
+
aiRequestsUsed: 0,
|
|
120
|
+
cacheHits: 0,
|
|
121
|
+
totalStrings: 0,
|
|
122
|
+
uniqueStrings: 0,
|
|
123
|
+
};
|
|
124
|
+
this.seenTexts.clear();
|
|
125
|
+
}
|
|
57
126
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
"hasDynamicHelp": false,
|
|
28
28
|
"multiple": false,
|
|
29
29
|
"type": "option"
|
|
30
|
+
},
|
|
31
|
+
"use-ai-keys": {
|
|
32
|
+
"description": "Use AI to generate human-readable keys (default: true)",
|
|
33
|
+
"name": "use-ai-keys",
|
|
34
|
+
"allowNo": true,
|
|
35
|
+
"type": "boolean"
|
|
30
36
|
}
|
|
31
37
|
},
|
|
32
38
|
"hasDynamicHelp": false,
|
|
@@ -301,5 +307,5 @@
|
|
|
301
307
|
]
|
|
302
308
|
}
|
|
303
309
|
},
|
|
304
|
-
"version": "0.5.
|
|
310
|
+
"version": "0.5.1"
|
|
305
311
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18nizer",
|
|
3
3
|
"description": "CLI to extract texts from JSX/TSX and generate i18n JSON with AI translations",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"author": "Yoannis Sanchez Soto",
|
|
6
6
|
"bin": "./bin/run.js",
|
|
7
7
|
"bugs": "https://github.com/yossTheDev/i18nizer/issues",
|