bctranslate 1.0.0 → 1.0.2

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.
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
+ import { shieldInterpolations, unshieldInterpolations } from '../utils.js';
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
@@ -8,24 +9,20 @@ const PYTHON_SCRIPT = join(__dirname, '..', '..', 'python', 'translator.py');
8
9
 
9
10
  let cachedPythonCmd = null;
10
11
 
11
- /**
12
- * Find the correct python command on this system.
13
- */
14
12
  async function findPython() {
15
13
  if (cachedPythonCmd) return cachedPythonCmd;
16
-
17
14
  for (const cmd of ['python3', 'python']) {
18
15
  try {
19
- const result = await execSimple(cmd, ['--version']);
20
- if (result.includes('Python 3')) {
16
+ const out = await execSimple(cmd, ['--version']);
17
+ if (out.includes('Python 3')) {
21
18
  cachedPythonCmd = cmd;
22
19
  return cmd;
23
20
  }
24
21
  } catch { /* try next */ }
25
22
  }
26
23
  throw new Error(
27
- 'Python 3 not found. Install Python 3.8+ and ensure it is on your PATH.\n' +
28
- ' Also install argostranslate: pip install argostranslate'
24
+ 'Python 3 not found. Install Python 3.8+ and add it to your PATH.\n' +
25
+ ' Then run: pip install argostranslate'
29
26
  );
30
27
  }
31
28
 
@@ -45,13 +42,21 @@ function execSimple(cmd, args) {
45
42
  }
46
43
 
47
44
  /**
48
- * Check that Python and argostranslate are available,
49
- * and that the required language pair is installed.
45
+ * Install argostranslate via pip.
46
+ * Called from `init` and from checkPythonBridge when the package is missing.
47
+ */
48
+ export async function installArgostranslate() {
49
+ const py = await findPython();
50
+ return execSimple(py, ['-m', 'pip', 'install', '--quiet', 'argostranslate']);
51
+ }
52
+
53
+ /**
54
+ * Check Python + argostranslate availability and download the language pair
55
+ * model if not already installed.
50
56
  */
51
57
  export async function checkPythonBridge(from, to) {
52
58
  const py = await findPython();
53
59
 
54
- // Check argostranslate is installed and language pair available
55
60
  const checkScript = `
56
61
  import sys, json
57
62
  try:
@@ -62,41 +67,34 @@ except ImportError:
62
67
  sys.exit(0)
63
68
 
64
69
  from_code = "${from}"
65
- to_code = "${to}"
70
+ to_code = "${to}"
66
71
 
67
72
  installed = argostranslate.translate.get_installed_languages()
68
- from_lang = None
69
- to_lang = None
70
- for lang in installed:
71
- if lang.code == from_code:
72
- from_lang = lang
73
- if lang.code == to_code:
74
- to_lang = lang
75
-
76
- if not from_lang or not to_lang:
77
- available_codes = [l.code for l in installed]
78
- # Try to auto-download
73
+ from_lang = next((l for l in installed if l.code == from_code), None)
74
+ to_lang = next((l for l in installed if l.code == to_code), None)
75
+
76
+ if from_lang and to_lang and from_lang.get_translation(to_lang):
77
+ print(json.dumps({"status": "ready"}))
78
+ else:
79
79
  try:
80
80
  argostranslate.package.update_package_index()
81
- available_packages = argostranslate.package.get_available_packages()
82
- pkg = next((p for p in available_packages if p.from_code == from_code and p.to_code == to_code), None)
81
+ available = argostranslate.package.get_available_packages()
82
+ pkg = next((p for p in available if p.from_code == from_code and p.to_code == to_code), None)
83
83
  if pkg:
84
84
  print(json.dumps({"status": "downloading", "pair": f"{from_code}->{to_code}"}))
85
85
  argostranslate.package.install_from_path(pkg.download())
86
86
  print(json.dumps({"status": "ready"}))
87
87
  else:
88
+ available_codes = [l.code for l in installed]
88
89
  print(json.dumps({"error": f"Language pair {from_code}->{to_code} not available. Installed: {available_codes}"}))
89
90
  except Exception as e:
90
- print(json.dumps({"error": f"Failed to download language pair: {str(e)}. Installed: {available_codes}"}))
91
- else:
92
- print(json.dumps({"status": "ready"}))
91
+ print(json.dumps({"error": f"Failed to download language pair: {str(e)}"}))
93
92
  `;
94
93
 
95
94
  const result = await execSimple(py, ['-c', checkScript]);
96
95
 
97
- // Parse last JSON line (might have multiple from downloading)
98
- const lines = result.split('\n').filter(l => l.trim());
99
- for (const line of lines.reverse()) {
96
+ const lines = result.split('\n').filter((l) => l.trim());
97
+ for (const line of [...lines].reverse()) {
100
98
  try {
101
99
  const parsed = JSON.parse(line);
102
100
  if (parsed.error) throw new Error(parsed.error);
@@ -105,31 +103,38 @@ else:
105
103
  if (e.message && !e.message.includes('Unexpected')) throw e;
106
104
  }
107
105
  }
108
-
109
106
  return true;
110
107
  }
111
108
 
112
109
  /**
113
110
  * Translate a batch of strings using argostranslate via Python.
114
- * Sends all strings at once for efficiency (model loads once).
115
111
  *
116
- * @param {Array<{key: string, text: string}>} batch - Strings to translate
117
- * @param {string} from - Source language code
118
- * @param {string} to - Target language code
119
- * @returns {Promise<Object<string, string>>} Map of key -> translated text
112
+ * Interpolation variables like {{ name }}, {count}, ${val} are shielded
113
+ * before sending and restored after Argos never sees them.
114
+ *
115
+ * @param {Array<{key: string, text: string}>} batch
116
+ * @param {string} from Source language code
117
+ * @param {string} to Target language code
118
+ * @returns {Promise<Record<string, string>>} key → translated text
120
119
  */
121
120
  export async function translateBatch(batch, from, to) {
122
121
  if (batch.length === 0) return {};
123
122
 
124
123
  const py = await findPython();
125
124
 
125
+ // ── Shield interpolations so Argos never mangles {name} / {{ expr }} ────────
126
+ const shielded = batch.map((item) => {
127
+ const { shielded: text, tokens } = shieldInterpolations(item.text);
128
+ return { key: item.key, text, tokens };
129
+ });
130
+
126
131
  return new Promise((resolve, reject) => {
127
132
  const proc = spawn(py, [PYTHON_SCRIPT, from, to], {
128
133
  stdio: ['pipe', 'pipe', 'pipe'],
129
134
  env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
130
135
  });
131
136
 
132
- const input = JSON.stringify(batch);
137
+ const input = JSON.stringify(shielded.map(({ key, text }) => ({ key, text })));
133
138
  let stdout = '';
134
139
  let stderr = '';
135
140
 
@@ -142,34 +147,35 @@ export async function translateBatch(batch, from, to) {
142
147
  }
143
148
 
144
149
  try {
145
- // Find the JSON output line (ignore any debug/warning output)
150
+ // Find the JSON output line (skip debug/warning lines)
146
151
  const lines = stdout.split('\n');
147
- let result = null;
152
+ let raw = null;
148
153
  for (let i = lines.length - 1; i >= 0; i--) {
149
154
  const line = lines[i].trim();
150
- if (line.startsWith('{') || line.startsWith('[')) {
151
- try {
152
- result = JSON.parse(line);
153
- break;
154
- } catch { /* try previous line */ }
155
+ if (line.startsWith('[') || line.startsWith('{')) {
156
+ try { raw = JSON.parse(line); break; } catch { /* try previous */ }
155
157
  }
156
158
  }
157
159
 
158
- if (!result) {
160
+ if (!raw) {
159
161
  return reject(new Error(`No valid JSON in Python output: ${stdout.slice(0, 500)}`));
160
162
  }
161
163
 
162
- // Convert array to map
163
- const map = {};
164
- if (Array.isArray(result)) {
165
- for (const item of result) {
166
- map[item.key] = item.text;
167
- }
164
+ // Convert array map, then unshield interpolations
165
+ const rawMap = {};
166
+ if (Array.isArray(raw)) {
167
+ for (const item of raw) rawMap[item.key] = item.text;
168
168
  } else {
169
- Object.assign(map, result);
169
+ Object.assign(rawMap, raw);
170
170
  }
171
171
 
172
- resolve(map);
172
+ const result = {};
173
+ for (const { key, tokens } of shielded) {
174
+ const translated = rawMap[key] ?? batch.find((b) => b.key === key)?.text ?? '';
175
+ result[key] = unshieldInterpolations(translated, tokens);
176
+ }
177
+
178
+ resolve(result);
173
179
  } catch (err) {
174
180
  reject(new Error(`Failed to parse Python output: ${err.message}\nOutput: ${stdout.slice(0, 500)}`));
175
181
  }
@@ -180,4 +186,4 @@ export async function translateBatch(batch, from, to) {
180
186
  proc.stdin.write(input);
181
187
  proc.stdin.end();
182
188
  });
183
- }
189
+ }
package/src/index.js CHANGED
@@ -8,148 +8,209 @@ import { parseJson, applyJsonTranslations } from './parsers/json.js';
8
8
  import { translateBatch } from './bridges/python.js';
9
9
  import { getLocaleDir, saveLocale, loadLocale } from './generators/locales.js';
10
10
 
11
- /**
12
- * Process a single file: extract strings, translate only untranslated ones,
13
- * write results. Smart: skips strings already present in the target locale.
14
- */
15
- export async function translateFile(filePath, opts) {
16
- const { from, to, dryRun, outdir, project, cwd, verbose, jsonMode } = opts;
17
- const ext = extname(filePath).toLowerCase();
18
- const source = readFileSync(filePath, 'utf-8');
19
- const relativePath = relative(cwd, filePath);
11
+ // ── Route source file to the correct parser ──────────────────────────────────
20
12
 
21
- // Route to appropriate parser
22
- let result;
13
+ function routeParser(ext, source, filePath, project, jsonMode) {
23
14
  switch (ext) {
24
- case '.vue':
25
- result = parseVue(source, filePath);
26
- break;
15
+ case '.vue': return parseVue(source, filePath);
27
16
  case '.jsx':
28
- case '.tsx':
29
- result = parseReact(source, filePath);
30
- break;
17
+ case '.tsx': return parseReact(source, filePath);
31
18
  case '.html':
32
- case '.htm':
33
- result = parseHtml(source, filePath, project);
34
- break;
19
+ case '.htm': return parseHtml(source, filePath, project);
35
20
  case '.js':
36
21
  case '.ts':
37
22
  if (source.includes('React') || source.includes('jsx') || /<\w+[\s>]/.test(source)) {
38
- result = parseReact(source, filePath);
39
- } else {
40
- result = parseJs(source, filePath, project);
23
+ return parseReact(source, filePath);
41
24
  }
42
- break;
43
- case '.json':
44
- result = parseJson(source, filePath, { jsonMode });
45
- break;
46
- default:
47
- return { count: 0, skipped: 0, relativePath };
25
+ return parseJs(source, filePath, project);
26
+ case '.json': return parseJson(source, filePath, { jsonMode });
27
+ default: return null;
48
28
  }
29
+ }
30
+
31
+ /**
32
+ * Parse a file — no translation, no disk writes.
33
+ * Returns extracted strings + modified source for later application.
34
+ */
35
+ export function parseFileOnly(filePath, opts) {
36
+ const { project, jsonMode = 'values' } = opts;
37
+ const ext = extname(filePath).toLowerCase();
38
+ const source = readFileSync(filePath, 'utf-8');
49
39
 
50
- if (!result.extracted || result.extracted.length === 0) {
51
- return { count: 0, skipped: 0, relativePath };
40
+ const result = routeParser(ext, source, filePath, project, jsonMode);
41
+ if (!result || !result.extracted?.length) {
42
+ return { source, modified: source, extracted: [], ext, filePath };
52
43
  }
44
+ return {
45
+ source,
46
+ modified: result.source,
47
+ extracted: result.extracted,
48
+ ext,
49
+ filePath,
50
+ jsonData: result.jsonData,
51
+ };
52
+ }
53
53
 
54
- // Deduplicate by key (same text = same hash key)
55
- const seen = new Set();
56
- const uniqueBatch = result.extracted
57
- .map((item) => ({ key: item.key, text: item.text }))
58
- .filter((item) => {
59
- if (seen.has(item.key)) return false;
60
- seen.add(item.key);
61
- return true;
62
- });
54
+ /**
55
+ * Apply a completed translations map to one parsed file and write to disk.
56
+ * The original file is NOT touched until an atomic rename succeeds.
57
+ */
58
+ export async function writeFileResult(parseResult, translations, opts) {
59
+ const { source, modified, extracted, ext, filePath, jsonData } = parseResult;
60
+ const { from, to, dryRun, outdir, project, cwd, verbose, localesDir } = opts;
63
61
 
64
- // ── Smart half-translation: load existing locale, skip already-done keys ──
65
- const localeDir = opts.localesDir || getLocaleDir(cwd, project);
66
- const existingTarget = loadLocale(localeDir, to);
62
+ const relativePath = relative(cwd, filePath);
63
+ if (!extracted?.length) return { count: 0, skipped: 0, relativePath };
67
64
 
68
- const needsTranslation = uniqueBatch.filter((item) => !(item.key in existingTarget));
69
- const skippedCount = uniqueBatch.length - needsTranslation.length;
65
+ // Deduplicate extracted items by key
66
+ const seen = new Set();
67
+ const unique = extracted.filter(({ key }) => {
68
+ if (seen.has(key)) return false;
69
+ seen.add(key);
70
+ return true;
71
+ });
70
72
 
71
- // Start with existing translations
72
- let translations = { ...existingTarget };
73
+ const resolvedLocaleDir = localesDir || getLocaleDir(cwd, project);
74
+ const existingTarget = loadLocale(resolvedLocaleDir, to);
73
75
 
74
- if (needsTranslation.length > 0) {
75
- try {
76
- const newTranslations = await translateBatch(needsTranslation, from, to);
77
- Object.assign(translations, newTranslations);
78
- } catch (err) {
79
- if (verbose) {
80
- console.error(` Translation error: ${err.message}`);
81
- }
82
- // Fallback: use original text
83
- for (const item of needsTranslation) {
84
- translations[item.key] = item.text;
85
- }
86
- }
87
- }
76
+ const newCount = unique.filter(({ key }) => !(key in existingTarget)).length;
77
+ const skipped = unique.length - newCount;
88
78
 
89
- // Build locale entries for all extracted strings
79
+ // Build locale entry maps
90
80
  const fromEntries = {};
91
- const toEntries = {};
92
- for (const item of uniqueBatch) {
93
- fromEntries[item.key] = item.text;
94
- toEntries[item.key] = translations[item.key] || item.text;
81
+ const toEntries = {};
82
+ for (const { key, text } of unique) {
83
+ fromEntries[key] = text;
84
+ toEntries[key] = translations[key] ?? existingTarget[key] ?? text;
95
85
  }
96
86
 
97
87
  if (!dryRun) {
98
- // Write locale files (merge with existing — won't overwrite manual edits)
99
- saveLocale(localeDir, from, fromEntries);
100
- saveLocale(localeDir, to, toEntries);
88
+ saveLocale(resolvedLocaleDir, from, fromEntries);
89
+ saveLocale(resolvedLocaleDir, to, toEntries);
101
90
 
102
91
  if (ext === '.json') {
103
- const translatedData = applyJsonTranslations(result.jsonData, translations);
92
+ const translatedData = applyJsonTranslations(jsonData, toEntries);
104
93
  const outName = basename(filePath, ext) + `.${to}${ext}`;
105
94
  const outPath = outdir
106
95
  ? join(outdir, outName)
107
96
  : join(dirname(filePath), outName);
108
-
109
97
  mkdirSync(dirname(outPath), { recursive: true });
110
98
  writeFileSync(outPath, JSON.stringify(translatedData, null, 2) + '\n', 'utf-8');
111
- } else {
112
- // Only rewrite source if it actually changed
113
- if (result.source !== source) {
114
- const outPath = outdir ? join(outdir, basename(filePath)) : filePath;
115
- if (outdir) mkdirSync(outdir, { recursive: true });
116
-
117
- const tmpPath = outPath + '.bctmp';
118
- writeFileSync(tmpPath, result.source, 'utf-8');
119
- const { renameSync } = await import('fs');
120
- renameSync(tmpPath, outPath);
121
- }
99
+
100
+ } else if (modified !== source) {
101
+ // Only rewrite source when content actually changed
102
+ // Atomic: write .bctmp first, then rename — original untouched until swap
103
+ const outPath = outdir ? join(outdir, basename(filePath)) : filePath;
104
+ const tmpPath = outPath + '.bctmp';
105
+ if (outdir) mkdirSync(outdir, { recursive: true });
106
+ writeFileSync(tmpPath, modified, 'utf-8');
107
+ const { renameSync } = await import('fs');
108
+ renameSync(tmpPath, outPath);
122
109
  }
123
110
  }
124
111
 
125
- // count = newly translated strings; skipped = already had translations
126
112
  return {
127
- count: needsTranslation.length,
128
- skipped: skippedCount,
113
+ count: newCount,
114
+ skipped,
129
115
  relativePath,
130
- diff: dryRun ? generateDiff(source, result.source) : null,
116
+ diff: dryRun || verbose ? generateDiff(source, modified, relativePath) : null,
131
117
  };
132
118
  }
133
119
 
134
120
  /**
135
- * Simple line-diff preview for dry-run mode.
121
+ * Translate ALL files in one Python invocation — model loads exactly once.
122
+ *
123
+ * Phase 1: Parse every file (pure CPU — no network/Python)
124
+ * Phase 2: Collect all unique keys not already in the target locale
125
+ * Phase 3: translateBatch() once → Python starts, model loads, all strings translated
126
+ * Phase 4: Write each file
127
+ */
128
+ export async function translateAllFiles(files, opts) {
129
+ const { from, to, cwd, project, verbose, localesDir } = opts;
130
+
131
+ const resolvedLocaleDir = localesDir || getLocaleDir(cwd, project);
132
+ const existingTarget = loadLocale(resolvedLocaleDir, to);
133
+
134
+ // Phase 1 — parse
135
+ const parsed = [];
136
+ for (const file of files) {
137
+ try {
138
+ const result = parseFileOnly(file, opts);
139
+ if (result.extracted.length > 0) parsed.push(result);
140
+ } catch (err) {
141
+ if (verbose) console.error(` Parse error ${file}: ${err.message}`);
142
+ }
143
+ }
144
+
145
+ if (parsed.length === 0) return [];
146
+
147
+ // Phase 2 — deduplicate across all files, skip already-translated keys
148
+ const seenKeys = new Set(Object.keys(existingTarget));
149
+ const needed = [];
150
+ for (const { extracted } of parsed) {
151
+ for (const { key, text } of extracted) {
152
+ if (!seenKeys.has(key)) {
153
+ seenKeys.add(key);
154
+ needed.push({ key, text });
155
+ }
156
+ }
157
+ }
158
+
159
+ // Phase 3 — translate once (Python spawned exactly once, model loads once)
160
+ let newTranslations = {};
161
+ if (needed.length > 0) {
162
+ newTranslations = await translateBatch(needed, from, to);
163
+ }
164
+
165
+ const allTranslations = { ...existingTarget, ...newTranslations };
166
+
167
+ // Phase 4 — write files
168
+ const results = [];
169
+ for (const parseResult of parsed) {
170
+ try {
171
+ const r = await writeFileResult(parseResult, allTranslations, {
172
+ ...opts,
173
+ localesDir: resolvedLocaleDir,
174
+ });
175
+ results.push(r);
176
+ } catch (err) {
177
+ if (verbose) console.error(` Write error ${parseResult.filePath}: ${err.message}`);
178
+ results.push({ count: 0, skipped: 0, relativePath: relative(cwd, parseResult.filePath) });
179
+ }
180
+ }
181
+
182
+ return results;
183
+ }
184
+
185
+ /**
186
+ * Single-file convenience wrapper (for backward compatibility).
136
187
  */
137
- function generateDiff(original, modified) {
138
- if (original === modified) return '';
188
+ export async function translateFile(filePath, opts) {
189
+ const results = await translateAllFiles([filePath], opts);
190
+ return results[0] ?? { count: 0, skipped: 0, relativePath: relative(opts.cwd, filePath) };
191
+ }
192
+
193
+ // ── Diff display ─────────────────────────────────────────────────────────────
194
+
195
+ export function generateDiff(original, modified, label = '') {
196
+ if (!modified || original === modified) return '';
139
197
 
140
198
  const origLines = original.split('\n');
141
- const modLines = modified.split('\n');
142
- const diffs = [];
199
+ const modLines = modified.split('\n');
200
+ const out = label ? [`--- ${label} ---`] : [];
143
201
 
144
202
  const maxLines = Math.max(origLines.length, modLines.length);
145
203
  for (let i = 0; i < maxLines; i++) {
146
- const orig = origLines[i] || '';
147
- const mod = modLines[i] || '';
148
- if (orig !== mod) {
149
- diffs.push(` ${i + 1}: - ${orig.trim()}`);
150
- diffs.push(` ${i + 1}: + ${mod.trim()}`);
204
+ const a = origLines[i] ?? '';
205
+ const b = modLines[i] ?? '';
206
+ if (a !== b) {
207
+ out.push(` ${String(i + 1).padStart(4)} - ${a.trimEnd()}`);
208
+ out.push(` ${String(i + 1).padStart(4)} + ${b.trimEnd()}`);
151
209
  }
152
210
  }
153
211
 
154
- return diffs.slice(0, 40).join('\n') + (diffs.length > 40 ? '\n ... (truncated)' : '');
212
+ if (out.length > 80) {
213
+ return out.slice(0, 80).join('\n') + `\n ... (${out.length - 80} more lines truncated)`;
214
+ }
215
+ return out.join('\n');
155
216
  }
@@ -1,6 +1,6 @@
1
1
  import MagicString from 'magic-string';
2
2
  import { parse, NodeTypes } from '@vue/compiler-dom';
3
- import { hashKey, isTranslatable } from '../utils.js';
3
+ import { contextKey, isTranslatable } from '../utils.js';
4
4
 
5
5
  const ATTR_WHITELIST = new Set([
6
6
  'title',
@@ -82,7 +82,7 @@ export function parseHtml(source, filePath, project) {
82
82
  const marker = `data-i18n-${name}`;
83
83
  if (openTagText.includes(marker)) continue;
84
84
 
85
- const key = hashKey(value);
85
+ const key = contextKey(value, filePath);
86
86
  s.appendRight(prop.loc.end.offset, ` ${marker}="${key}"`);
87
87
  extracted.push({ key, text: value, context: `html-attr-${name}` });
88
88
  }
@@ -98,7 +98,7 @@ export function parseHtml(source, filePath, project) {
98
98
 
99
99
  const marker = tag === 'title' ? 'data-i18n-title' : 'data-i18n';
100
100
  if (!openTagText.includes(marker) && isTranslatable(plain)) {
101
- const key = hashKey(text);
101
+ const key = contextKey(text, filePath);
102
102
  s.appendLeft(openTagEnd, ` ${marker}="${key}"`);
103
103
  extracted.push({ key, text, context: `html-inner-${tag}` });
104
104
  }
package/src/parsers/js.js CHANGED
@@ -2,7 +2,7 @@ import * as babelParser from '@babel/parser';
2
2
  import _traverse from '@babel/traverse';
3
3
  import * as t from '@babel/types';
4
4
  import MagicString from 'magic-string';
5
- import { hashKey, isTranslatable } from '../utils.js';
5
+ import { contextKey, isTranslatable } from '../utils.js';
6
6
 
7
7
  const traverse = _traverse.default || _traverse;
8
8
 
@@ -55,7 +55,7 @@ export function parseJs(source, filePath, project) {
55
55
 
56
56
  if (!translatableKeys.has(keyName)) return;
57
57
 
58
- const key = hashKey(valueNode.value);
58
+ const key = contextKey(valueNode.value, filePath);
59
59
  const tFunc = project.type === 'vue' ? `this.$t('${key}')` : `t('${key}')`;
60
60
 
61
61
  s.overwrite(valueNode.start, valueNode.end, tFunc);
@@ -80,7 +80,7 @@ export function parseJs(source, filePath, project) {
80
80
  if (['alert', 'confirm', 'prompt'].includes(calleeName)) {
81
81
  const arg = path.node.arguments[0];
82
82
  if (arg && arg.type === 'StringLiteral' && isTranslatable(arg.value)) {
83
- const key = hashKey(arg.value);
83
+ const key = contextKey(arg.value, filePath);
84
84
  const tFunc = project.type === 'vue' ? `this.$t('${key}')` : `t('${key}')`;
85
85
  s.overwrite(arg.start, arg.end, tFunc);
86
86
  extracted.push({ key, text: arg.value, context: 'js-call' });
@@ -106,7 +106,7 @@ export function parseJs(source, filePath, project) {
106
106
  ]);
107
107
 
108
108
  if (domProps.has(propName)) {
109
- const key = hashKey(right.value);
109
+ const key = contextKey(right.value, filePath);
110
110
  const tFunc = project.type === 'vanilla'
111
111
  ? `i18n.t('${key}')`
112
112
  : project.type === 'vue'
@@ -1,4 +1,4 @@
1
- import { hashKey, isTranslatable } from '../utils.js';
1
+ import { isTranslatable } from '../utils.js';
2
2
 
3
3
  /**
4
4
  * Parse a JSON file and extract translatable string values.