bctranslate 1.0.2 → 1.0.3
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 +4 -1
- package/src/parsers/vue.js +57 -11
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
|
@@ -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
|
@@ -155,25 +155,71 @@ function walkTemplate(nodes, s, baseOffset, extracted, filePath, tpl) {
|
|
|
155
155
|
// ── Script string extractor ───────────────────────────────────────────────────
|
|
156
156
|
|
|
157
157
|
function extractScriptStrings(scriptContent, s, baseOffset, extracted, filePath, scr) {
|
|
158
|
-
|
|
158
|
+
// Pattern A: alert/confirm/toast/notify calls → match[2]=quote, match[3]=text
|
|
159
|
+
// Pattern B: UI object property string values → match[2]=quote, match[3]=text
|
|
160
|
+
const patternsAB = [
|
|
159
161
|
/\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,
|
|
162
|
+
/\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,
|
|
161
163
|
];
|
|
162
164
|
|
|
163
|
-
for (const pattern of
|
|
165
|
+
for (const pattern of patternsAB) {
|
|
164
166
|
let match;
|
|
165
167
|
while ((match = pattern.exec(scriptContent)) !== null) {
|
|
166
168
|
const text = match[3];
|
|
167
|
-
if (isTranslatable(text)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
170
|
+
const quoteChar = match[2];
|
|
171
|
+
const relPos = match.index + match[0].indexOf(quoteChar + text);
|
|
172
|
+
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
173
|
+
const key = contextKey(text, filePath);
|
|
174
|
+
const textStart = baseOffset + relPos;
|
|
175
|
+
const textEnd = textStart + text.length + 2;
|
|
176
|
+
s.overwrite(textStart, textEnd, scr(key));
|
|
177
|
+
extracted.push({ key, text, context: 'script' });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Pattern C: ref('string') / ref("string") → match[1]=quote, match[2]=text
|
|
182
|
+
const refPattern = /\bref\s*\(\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g;
|
|
183
|
+
{
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = refPattern.exec(scriptContent)) !== null) {
|
|
186
|
+
const text = match[2];
|
|
187
|
+
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
188
|
+
const quoteChar = match[1];
|
|
189
|
+
const innerStr = quoteChar + text + quoteChar;
|
|
190
|
+
const relPos = match.index + match[0].indexOf(innerStr);
|
|
191
|
+
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
192
|
+
const key = contextKey(text, filePath);
|
|
193
|
+
const textStart = baseOffset + relPos;
|
|
194
|
+
const textEnd = textStart + innerStr.length;
|
|
195
|
+
s.overwrite(textStart, textEnd, scr(key));
|
|
196
|
+
extracted.push({ key, text, context: 'script-ref' });
|
|
175
197
|
}
|
|
176
198
|
}
|
|
199
|
+
|
|
200
|
+
// Pattern D: computed(() => 'string') → match[1]=quote, match[2]=text
|
|
201
|
+
const computedPattern = /\bcomputed\s*\(\s*\(\s*\)\s*=>\s*(['"`])((?:(?!\1)[^\\]|\\.)+)\1\s*\)/g;
|
|
202
|
+
{
|
|
203
|
+
let match;
|
|
204
|
+
while ((match = computedPattern.exec(scriptContent)) !== null) {
|
|
205
|
+
const text = match[2];
|
|
206
|
+
if (!isTranslatable(text) || text.length <= 1) continue;
|
|
207
|
+
const quoteChar = match[1];
|
|
208
|
+
const innerStr = quoteChar + text + quoteChar;
|
|
209
|
+
const relPos = match.index + match[0].indexOf(innerStr);
|
|
210
|
+
if (isAlreadyWrappedScript(scriptContent, relPos)) continue;
|
|
211
|
+
const key = contextKey(text, filePath);
|
|
212
|
+
const textStart = baseOffset + relPos;
|
|
213
|
+
const textEnd = textStart + innerStr.length;
|
|
214
|
+
s.overwrite(textStart, textEnd, scr(key));
|
|
215
|
+
extracted.push({ key, text, context: 'script-computed' });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isAlreadyWrappedScript(scriptContent, pos) {
|
|
221
|
+
const before = scriptContent.slice(Math.max(0, pos - 30), pos);
|
|
222
|
+
return /\$?t\s*\(\s*$/.test(before);
|
|
177
223
|
}
|
|
178
224
|
|
|
179
225
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|