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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bctranslate",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "CLI to transform source files into i18n-ready code with automatic translation via argostranslate",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
- import { join, dirname } from 'path';
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
- * Load an existing locale file, or return empty object.
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
- return JSON.parse(readFileSync(filePath, 'utf-8'));
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: new entries take precedence for new keys,
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
- writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
61
- return filePath;
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
- if (verbose) console.error(` Write error ${parseResult.filePath}: ${err.message}`);
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
  }
@@ -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
- const patterns = [
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 patterns) {
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) && text.length > 1) {
168
- const key = contextKey(text, filePath);
169
- const quoteChar = match[2];
170
- const textStart = baseOffset + match.index + match[0].indexOf(quoteChar + text);
171
- const textEnd = textStart + text.length + 2;
172
- s.overwrite(textStart, textEnd, scr(key));
173
- extracted.push({ key, text, context: 'script' });
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 ───────────────────────────────────────────────────────────────────