bctranslate 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/generators/locales.js +63 -20
- package/src/index.js +5 -2
- package/src/parsers/vue.js +95 -31
- package/python/t2.py +0 -76
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Determine the locale directory path based on project type.
|
|
@@ -8,6 +8,7 @@ export function getLocaleDir(cwd, project) {
|
|
|
8
8
|
const candidates = [
|
|
9
9
|
join(cwd, 'src', 'locales'),
|
|
10
10
|
join(cwd, 'src', 'i18n', 'locales'),
|
|
11
|
+
join(cwd, 'src', 'i18n'),
|
|
11
12
|
join(cwd, 'locales'),
|
|
12
13
|
join(cwd, 'src', 'lang'),
|
|
13
14
|
join(cwd, 'public', 'locales'),
|
|
@@ -25,13 +26,53 @@ export function getLocaleDir(cwd, project) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
+
* Flatten a nested JSON object to dot-notation keys.
|
|
30
|
+
* { home: { notes: 'Notes' } } → { 'home.notes': 'Notes' }
|
|
31
|
+
*/
|
|
32
|
+
function flattenKeys(obj, prefix = '') {
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
35
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
36
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
|
+
Object.assign(result, flattenKeys(value, fullKey));
|
|
38
|
+
} else {
|
|
39
|
+
result[fullKey] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Unflatten dot-notation keys to a nested JSON object.
|
|
47
|
+
* { 'home.notes': 'Notes' } → { home: { notes: 'Notes' } }
|
|
48
|
+
*/
|
|
49
|
+
function unflattenKeys(flat) {
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const [dotKey, value] of Object.entries(flat)) {
|
|
52
|
+
const parts = dotKey.split('.');
|
|
53
|
+
let obj = result;
|
|
54
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
55
|
+
if (typeof obj[parts[i]] !== 'object' || obj[parts[i]] === null) {
|
|
56
|
+
obj[parts[i]] = {};
|
|
57
|
+
}
|
|
58
|
+
obj = obj[parts[i]];
|
|
59
|
+
}
|
|
60
|
+
obj[parts[parts.length - 1]] = value;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load an existing locale file and return flat dot-notation keys.
|
|
67
|
+
* Handles both nested JSON (vue-i18n standard) and legacy flat format.
|
|
29
68
|
*/
|
|
30
69
|
export function loadLocale(localeDir, langCode) {
|
|
31
70
|
const filePath = join(localeDir, `${langCode}.json`);
|
|
32
71
|
if (existsSync(filePath)) {
|
|
33
72
|
try {
|
|
34
|
-
|
|
73
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
74
|
+
// Flatten nested objects to dot-notation for internal use
|
|
75
|
+
return flattenKeys(raw);
|
|
35
76
|
} catch {
|
|
36
77
|
return {};
|
|
37
78
|
}
|
|
@@ -41,22 +82,24 @@ export function loadLocale(localeDir, langCode) {
|
|
|
41
82
|
|
|
42
83
|
/**
|
|
43
84
|
* Save a locale file, merging with existing keys.
|
|
85
|
+
* Writes nested JSON (standard for vue-i18n and react-i18next).
|
|
44
86
|
*/
|
|
45
|
-
export function saveLocale(localeDir, langCode, newEntries) {
|
|
46
|
-
mkdirSync(localeDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
const filePath = join(localeDir, `${langCode}.json`);
|
|
49
|
-
const existing = loadLocale(localeDir, langCode);
|
|
50
|
-
|
|
51
|
-
// Merge:
|
|
52
|
-
// but don't overwrite existing translations (idempotent)
|
|
87
|
+
export function saveLocale(localeDir, langCode, newEntries) {
|
|
88
|
+
mkdirSync(localeDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
const filePath = join(localeDir, `${langCode}.json`);
|
|
91
|
+
const existing = loadLocale(localeDir, langCode); // already flat
|
|
92
|
+
|
|
93
|
+
// Merge: don't overwrite existing translations
|
|
53
94
|
const merged = { ...existing };
|
|
54
|
-
for (const [key, value] of Object.entries(newEntries)) {
|
|
55
|
-
if (!(key in merged)) {
|
|
56
|
-
merged[key] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
for (const [key, value] of Object.entries(newEntries)) {
|
|
96
|
+
if (!(key in merged)) {
|
|
97
|
+
merged[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Write as nested JSON for i18n library compatibility
|
|
102
|
+
const nested = unflattenKeys(merged);
|
|
103
|
+
writeFileSync(filePath, JSON.stringify(nested, null, 2) + '\n', 'utf-8');
|
|
104
|
+
return filePath;
|
|
105
|
+
}
|
package/src/index.js
CHANGED
|
@@ -128,7 +128,7 @@ export async function writeFileResult(parseResult, translations, opts) {
|
|
|
128
128
|
export async function translateAllFiles(files, opts) {
|
|
129
129
|
const { from, to, cwd, project, verbose, localesDir } = opts;
|
|
130
130
|
|
|
131
|
-
const resolvedLocaleDir = localesDir || getLocaleDir(cwd, project);
|
|
131
|
+
const resolvedLocaleDir = opts.outdir ? join(opts.outdir, 'locales') : (localesDir || getLocaleDir(cwd, project));
|
|
132
132
|
const existingTarget = loadLocale(resolvedLocaleDir, to);
|
|
133
133
|
|
|
134
134
|
// Phase 1 — parse
|
|
@@ -165,6 +165,9 @@ export async function translateAllFiles(files, opts) {
|
|
|
165
165
|
const allTranslations = { ...existingTarget, ...newTranslations };
|
|
166
166
|
|
|
167
167
|
// Phase 4 — write files
|
|
168
|
+
if (!opts.dryRun) {
|
|
169
|
+
console.log(` → Locale dir: ${resolvedLocaleDir}`);
|
|
170
|
+
}
|
|
168
171
|
const results = [];
|
|
169
172
|
for (const parseResult of parsed) {
|
|
170
173
|
try {
|
|
@@ -174,7 +177,7 @@ export async function translateAllFiles(files, opts) {
|
|
|
174
177
|
});
|
|
175
178
|
results.push(r);
|
|
176
179
|
} catch (err) {
|
|
177
|
-
|
|
180
|
+
console.error(` ✗ Write error ${relative(cwd, parseResult.filePath)}: ${err.message}`);
|
|
178
181
|
results.push({ count: 0, skipped: 0, relativePath: relative(cwd, parseResult.filePath) });
|
|
179
182
|
}
|
|
180
183
|
}
|
package/src/parsers/vue.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as compiler from '@vue/compiler-dom';
|
|
2
|
+
import { parse as babelParse } from '@babel/parser';
|
|
3
|
+
import _traverse from '@babel/traverse';
|
|
4
|
+
const traverse = _traverse.default;
|
|
2
5
|
import MagicString from 'magic-string';
|
|
3
6
|
import { contextKey, isTranslatable } from '../utils.js';
|
|
4
7
|
|
|
@@ -30,14 +33,6 @@ const ATTR_WHITELIST = new Set([
|
|
|
30
33
|
* - Options API (no setup)
|
|
31
34
|
* → use $t('key') (global plugin property)
|
|
32
35
|
*/
|
|
33
|
-
function detectTStyle(source) {
|
|
34
|
-
const isSetup = /<script\b[^>]*\bsetup\b/i.test(source);
|
|
35
|
-
|
|
36
|
-
// Already has: const { t } = ... or const { t, ... } = ...
|
|
37
|
-
const hasT = /const\s*\{[^}]*\bt\b[^}]*\}\s*=/.test(source);
|
|
38
|
-
|
|
39
|
-
return { isSetup, hasT };
|
|
40
|
-
}
|
|
41
36
|
|
|
42
37
|
/**
|
|
43
38
|
* Parse a .vue file and extract translatable strings.
|
|
@@ -46,12 +41,9 @@ export function parseVue(source, filePath) {
|
|
|
46
41
|
const extracted = [];
|
|
47
42
|
const s = new MagicString(source);
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const tpl = (key) => (isSetup || hasT) ? `t('${key}')` : `$t('${key}')`;
|
|
53
|
-
// In scripts: use t() for setup, this.$t() for options
|
|
54
|
-
const scr = (key) => isSetup ? `t('${key}')` : `this.$t('${key}')`;
|
|
44
|
+
// Always use t() for consistency, as promised in the README.
|
|
45
|
+
const tpl = (key) => `t('${key}')`;
|
|
46
|
+
const scr = (key) => `t('${key}')`;
|
|
55
47
|
|
|
56
48
|
// ── Template ────────────────────────────────────────────────────────────────
|
|
57
49
|
const templateMatch = source.match(/<template\b[^>]*>([\s\S]*?)<\/template>/);
|
|
@@ -77,12 +69,13 @@ export function parseVue(source, filePath) {
|
|
|
77
69
|
extractScriptStrings(scriptContent, s, scriptOffset, extracted, filePath, scr);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
// ── Inject `const { t } = useI18n()` if
|
|
81
|
-
if (extracted.length > 0
|
|
72
|
+
// ── Inject `const { t } = useI18n()` if not yet declared ────────
|
|
73
|
+
if (extracted.length > 0) {
|
|
82
74
|
const scriptSetupMatch = source.match(/(<script\b[^>]*\bsetup\b[^>]*>)([\s\S]*?)<\/script>/i);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const hasT = /const\s*\{[^}]*\bt\b[^}]*\}\s*=/.test(source);
|
|
76
|
+
|
|
77
|
+
if (scriptSetupMatch && !hasT) {
|
|
78
|
+
const insertAt = source.indexOf(scriptSetupMatch[0]) + scriptSetupMatch[1].length;
|
|
86
79
|
const needsImport = !source.includes('useI18n');
|
|
87
80
|
const importLine = needsImport ? `import { useI18n } from 'vue-i18n';\n` : '';
|
|
88
81
|
s.appendRight(insertAt, `\n${importLine}const { t } = useI18n();\n`);
|
|
@@ -155,33 +148,104 @@ function walkTemplate(nodes, s, baseOffset, extracted, filePath, tpl) {
|
|
|
155
148
|
// ── Script string extractor ───────────────────────────────────────────────────
|
|
156
149
|
|
|
157
150
|
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
151
|
+
try {
|
|
152
|
+
const ast = babelParse(scriptContent, {
|
|
153
|
+
sourceType: 'module',
|
|
154
|
+
plugins: ['typescript', 'jsx'], // Enable TS and JSX support
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const replacements = [];
|
|
158
|
+
|
|
159
|
+
traverse(ast, {
|
|
160
|
+
enter(path) {
|
|
161
|
+
let text;
|
|
162
|
+
|
|
163
|
+
if (path.isStringLiteral()) {
|
|
164
|
+
text = path.node.value;
|
|
165
|
+
} else if (path.isTemplateLiteral()) {
|
|
166
|
+
if (path.node.expressions.length > 0 || path.node.quasis.length !== 1) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
text = path.node.quasis[0].value.cooked;
|
|
170
|
+
} else {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!isTranslatable(text)) return;
|
|
175
|
+
|
|
176
|
+
// --- Parent checks to avoid replacing the wrong strings ---
|
|
177
|
+
if (path.parent.type === 'CallExpression' && path.parent.callee.name === 't') return;
|
|
178
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(path.parent.type)) return;
|
|
179
|
+
if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) return;
|
|
180
|
+
if (path.parent.type === 'Property' && ['name'].includes(path.parent.key.name)) return;
|
|
181
|
+
|
|
182
|
+
// --- Passed all checks, schedule replacement ---
|
|
183
|
+
const key = contextKey(text, filePath);
|
|
184
|
+
const start = baseOffset + path.node.start;
|
|
185
|
+
const end = baseOffset + path.node.end;
|
|
186
|
+
replacements.push({ start, end, key });
|
|
187
|
+
extracted.push({ key, text, context: 'script' });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Apply replacements in reverse order to avoid offset issues
|
|
192
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
193
|
+
const { start, end, key } = replacements[i];
|
|
194
|
+
s.overwrite(start, end, scr(key));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error('Babel parsing failed:', e);
|
|
199
|
+
// If babel parsing fails, fallback to regex.
|
|
200
|
+
extractScriptStringsRegex(scriptContent, s, baseOffset, extracted, filePath, scr);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
function extractScriptStringsRegex(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
206
|
+
// This is the old regex-based implementation, kept as a fallback.
|
|
158
207
|
const patterns = [
|
|
159
208
|
/\b(alert|confirm|toast|notify|message\.(?:success|error|warning|info))\s*\(\s*(['"`])((?:(?!\2).)+)\2\s*\)/g,
|
|
160
|
-
/\b(title|label|placeholder|message|text|description|tooltip|hint|caption|header|subtitle|errorMessage|successMessage)\s*:\s*(['"`])((?:(?!\2).)+)\2/g,
|
|
209
|
+
/\b(title|label|placeholder|message|text|description|tooltip|hint|caption|header|subtitle|errorMessage|successMessage|emptyText|noData|loadingText|buttonText|confirmText|cancelText|successText|failText|warningText|helperText|hintText)\s*:\s*(['"`])((?:(?!\2).)+)\2/g,
|
|
210
|
+
/\bref\s*\(\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g,
|
|
211
|
+
/\bcomputed\s*\(\s*\(\s*\)\s*=>\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g,
|
|
161
212
|
];
|
|
162
213
|
|
|
163
214
|
for (const pattern of patterns) {
|
|
164
215
|
let match;
|
|
165
216
|
while ((match = pattern.exec(scriptContent)) !== null) {
|
|
166
|
-
const text = match[3];
|
|
167
|
-
if (isTranslatable(text)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
217
|
+
const text = match[3] || match[2];
|
|
218
|
+
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
219
|
+
|
|
220
|
+
const quoteChar = match[2] || match[1];
|
|
221
|
+
const innerStr = quoteChar + text + quoteChar;
|
|
222
|
+
const relPos = match.index + match[0].lastIndexOf(innerStr);
|
|
223
|
+
|
|
224
|
+
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
225
|
+
|
|
226
|
+
const key = contextKey(text, filePath);
|
|
227
|
+
const textStart = baseOffset + relPos;
|
|
228
|
+
const textEnd = textStart + innerStr.length;
|
|
229
|
+
|
|
230
|
+
s.overwrite(textStart, textEnd, scr(key));
|
|
231
|
+
extracted.push({ key, text, context: 'script-regex' });
|
|
175
232
|
}
|
|
176
233
|
}
|
|
177
234
|
}
|
|
178
235
|
|
|
236
|
+
function isAlreadyWrappedScript(scriptContent, pos) {
|
|
237
|
+
const before = scriptContent.slice(Math.max(0, pos - 30), pos);
|
|
238
|
+
return /\$?t\s*\(\s*$/.test(before);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
179
242
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
180
243
|
|
|
181
244
|
function isAlreadyWrapped(source, start, end) {
|
|
182
|
-
// Look back 25 chars for an open t(
|
|
245
|
+
// Look back 25 chars for an open t( call — node is inside an interpolation
|
|
183
246
|
const before = source.slice(Math.max(0, start - 25), start);
|
|
184
|
-
|
|
247
|
+
// Note: We only check for `t` now, not `$t`.
|
|
248
|
+
return /t\s*\(\s*['"]/.test(before);
|
|
185
249
|
}
|
|
186
250
|
|
|
187
251
|
function extractTemplateRegex(source, s, extracted, filePath, tpl) {
|
package/python/t2.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
# Suppress TensorFlow logs
|
|
6
|
-
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
import argostranslate.package
|
|
10
|
-
import argostranslate.translate
|
|
11
|
-
except ImportError:
|
|
12
|
-
print(json.dumps({"error": "argostranslate not found. Please run: pip install argostranslate"}), file=sys.stderr)
|
|
13
|
-
sys.exit(1)
|
|
14
|
-
|
|
15
|
-
def main():
|
|
16
|
-
if len(sys.argv) != 3:
|
|
17
|
-
print(json.dumps({"error": "Usage: python translator.py <from_code> <to_code>"}), file=sys.stderr)
|
|
18
|
-
sys.exit(1)
|
|
19
|
-
|
|
20
|
-
from_code = sys.argv[1]
|
|
21
|
-
to_code = sys.argv[2]
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
input_data = sys.stdin.read()
|
|
25
|
-
batch = json.loads(input_data)
|
|
26
|
-
except json.JSONDecodeError:
|
|
27
|
-
print(json.dumps({"error": "Invalid JSON input"}), file=sys.stderr)
|
|
28
|
-
sys.exit(1)
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
# 1. Find the installed languages
|
|
32
|
-
installed_languages = argostranslate.translate.get_installed_languages()
|
|
33
|
-
from_lang = next((lang for lang in installed_languages if lang.code == from_code), None)
|
|
34
|
-
to_lang = next((lang for lang in installed_languages if lang.code == to_code), None)
|
|
35
|
-
|
|
36
|
-
if not from_lang or not to_lang:
|
|
37
|
-
# This should ideally be handled by the check in the Node.js bridge,
|
|
38
|
-
# but as a fallback, we report it here too.
|
|
39
|
-
available_codes = [l.code for l in installed_languages]
|
|
40
|
-
print(json.dumps({
|
|
41
|
-
"error": f"Language pair not installed: {from_code}->{to_code}. Installed: {available_codes}"
|
|
42
|
-
}), file=sys.stderr)
|
|
43
|
-
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
# 2. Get the translation object
|
|
46
|
-
translation = from_lang.get_translation(to_lang)
|
|
47
|
-
if not translation:
|
|
48
|
-
# This may happen if the translation direction is not supported (e.g., en->en)
|
|
49
|
-
if from_code == to_code:
|
|
50
|
-
# If source and target are the same, just return the original text
|
|
51
|
-
print(json.dumps(batch))
|
|
52
|
-
sys.exit(0)
|
|
53
|
-
else:
|
|
54
|
-
print(json.dumps({"error": f"Translation from {from_code} to {to_code} is not supported by the installed model."}), file=sys.stderr)
|
|
55
|
-
sys.exit(1)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# 3. Translate texts
|
|
59
|
-
translated_batch = []
|
|
60
|
-
for item in batch:
|
|
61
|
-
original_text = item.get('text', '')
|
|
62
|
-
translated_text = translation.translate(original_text)
|
|
63
|
-
translated_batch.append({
|
|
64
|
-
"key": item.get('key'),
|
|
65
|
-
"text": translated_text
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
# 4. Output the result as a JSON array
|
|
69
|
-
print(json.dumps(translated_batch, ensure_ascii=False))
|
|
70
|
-
|
|
71
|
-
except Exception as e:
|
|
72
|
-
print(json.dumps({"error": f"An unexpected error occurred: {str(e)}"}), file=sys.stderr)
|
|
73
|
-
sys.exit(1)
|
|
74
|
-
|
|
75
|
-
if __name__ == "__main__":
|
|
76
|
-
main()
|