diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md
index 30da7475..7c31e7a4 100644
--- a/claude-spec/01-architecture.md
+++ b/claude-spec/01-architecture.md
@@ -128,7 +128,7 @@ browser.messages.onNewMailReceived
newEmailListener (checks _process_incoming, which includes summarize_auto === 3)
↓
processEmails({ summarizeOnReceive: true })
- ↓ (single loop — shared with addTagsAuto / spamFilter)
+ ↓ (single loop — shared with addTagsAuto / spamFilter / translateOnReceive)
_generateSummaryForMessage(headerMessageId, null, { messageData })
← tabId is null → no UI messages sent, silent pre-cache
↓
@@ -137,6 +137,27 @@ taSummaryStore.saveSummary()
[later] user opens the message → initSummary → cache hit → showSummary instantly
```
+### Data Flow: Background Translation on Email Receive (translate_auto = 3)
+
+When `translate_auto = 3`, a translation is generated silently when a new email arrives. Mirrors the summarize on-receive flow:
+
+```
+New email arrives
+ ↓
+browser.messages.onNewMailReceived
+ ↓
+newEmailListener (checks _process_incoming, which includes translate_auto === 3)
+ ↓
+processEmails({ translateOnReceive: true })
+ ↓ (single loop — shared with addTagsAuto / spamFilter / summarizeOnReceive)
+_generateTranslationForMessage(headerMessageId, null, { messageData })
+ ← tabId is null → no UI messages sent, silent pre-cache
+ ↓
+taTranslationStore.saveTranslation()
+ ↓
+[later] user opens the message → initTranslation → cache hit → showTranslation instantly
+```
+
## Key Modules
| File | Role |
diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md
index f6b12bbe..06c0d250 100644
--- a/claude-spec/05-options.md
+++ b/claude-spec/05-options.md
@@ -99,8 +99,8 @@ These are generated programmatically at the bottom of `mzta-options-default.js`
| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` and `summarize_auto = 3` always use inline regardless of this setting. |
| `summarize_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
| `translate` | `true` | Enable email translation |
-| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open) |
-| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` always uses inline regardless of this setting. |
+| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) |
+| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` and `translate_auto = 3` always use inline regardless of this setting. |
| `translate_max_display_length` | `0` | Maximum characters shown in inline translation before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
| `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. |
diff --git a/mzta-background.js b/mzta-background.js
index ec44bd18..5aaad026 100644
--- a/mzta-background.js
+++ b/mzta-background.js
@@ -697,7 +697,9 @@ async function _generateSummaryForMessage(headerMessageId, tabId = null, options
}
}
-async function _generateTranslationForMessage(headerMessageId, tabId) {
+// tabId is optional — if null, runs silently (background pre-cache, no UI update)
+// options.messageData: { fullMessage } — pass pre-fetched data to avoid re-querying
+async function _generateTranslationForMessage(headerMessageId, tabId = null, options = {}) {
try {
let prefs = await browser.storage.sync.get({
connection_type: prefs_default.connection_type,
@@ -710,35 +712,39 @@ async function _generateTranslationForMessage(headerMessageId, tabId) {
let cachedTranslation = await translationStore.loadTranslation(headerMessageId);
if (cachedTranslation && !cachedTranslation.error) {
- browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } });
return;
}
if (await translationStore.isProcessing(headerMessageId)) {
- browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" });
return;
}
await translationStore.setProcessing(headerMessageId);
taWorkingStatus.startWorking();
- browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" });
- const messageResult = await browser.messages.query({ headerMessageId: headerMessageId });
- if (!messageResult || messageResult.messages.length === 0) {
- await translationStore.saveError(headerMessageId, "Message not found");
- browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: "Message not found" } });
- taWorkingStatus.stopWorking();
- return;
+ let fullMessage;
+ if (options.messageData) {
+ fullMessage = options.messageData.fullMessage;
+ } else {
+ const messageResult = await browser.messages.query({ headerMessageId: headerMessageId });
+ if (!messageResult || messageResult.messages.length === 0) {
+ await translationStore.saveError(headerMessageId, "Message not found");
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: "Message not found" } });
+ taWorkingStatus.stopWorking();
+ return;
+ }
+ fullMessage = await browser.messages.getFull(messageResult.messages[0].id);
}
- const fullMessage = await browser.messages.getFull(messageResult.messages[0].id);
-
const connectionType = getConnectionType(prefs, {}, 'translate');
if (connectionType === 'chatgpt_web') {
const errorMsg = browser.i18n.getMessage('translate_chatgpt_web_not_supported');
await translationStore.saveError(headerMessageId, errorMsg);
- browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } });
taWorkingStatus.stopWorking();
return;
}
@@ -762,13 +768,13 @@ async function _generateTranslationForMessage(headerMessageId, tabId) {
headerMessageId: headerMessageId
};
await translationStore.saveTranslation(translationData, headerMessageId);
- browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } });
taWorkingStatus.stopWorking();
} catch (error) {
console.error("[ThunderAI] Error generating translation:", error);
await translationStore.saveError(headerMessageId, error.message || String(error));
- browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: error.message || "Failed to generate translation" } });
+ if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: error.message || "Failed to generate translation" } });
taWorkingStatus.stopWorking();
}
}
@@ -1388,13 +1394,15 @@ async function reload_pref_init(){
spamfilter: prefs_default.spamfilter,
summarize: prefs_default.summarize,
summarize_auto: prefs_default.summarize_auto,
+ translate: prefs_default.translate,
+ translate_auto: prefs_default.translate_auto,
spamfilter_threshold: prefs_default.spamfilter_threshold,
spamfilter_show_msg_panel: prefs_default.spamfilter_show_msg_panel,
dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter,
chatgpt_win_save_position: prefs_default.chatgpt_win_save_position,
...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type'])
});
- _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3);
+ _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3) || (prefs_init.translate && prefs_init.translate_auto === 3);
_sparks_presence = await checkSparksPresence();
}
@@ -1622,7 +1630,8 @@ const newEmailListener = (folder, messagesList) => {
messages: messages,
addTagsAuto: add_tags_auto_enabled,
spamFilter: prefs_init.spamfilter,
- summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3
+ summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3,
+ translateOnReceive: prefs_init.translate && prefs_init.translate_auto === 3
});
if(prefs_init.spamfilter){
@@ -1656,15 +1665,16 @@ async function processEmails(args) {
addTagsAuto = false,
spamFilter = false,
summarize = false,
- summarizeOnReceive = false
+ summarizeOnReceive = false,
+ translateOnReceive = false
} = args;
taWorkingStatus.startWorking();
- // One loop handles addTagsAuto, spamFilter, and summarizeOnReceive (on email receive).
+ // One loop handles addTagsAuto, spamFilter, summarizeOnReceive, and translateOnReceive (on email receive).
// The separate summarize block below handles the context menu flow.
- if (addTagsAuto || spamFilter || summarizeOnReceive) {
+ if (addTagsAuto || spamFilter || summarizeOnReceive || translateOnReceive) {
let prefs_aats = await browser.storage.sync.get({
add_tags_maxnum: prefs_default.add_tags_maxnum,
connection_type: prefs_default.connection_type,
@@ -1769,6 +1779,16 @@ async function processEmails(args) {
messageData: { message, fullMessage: curr_fullMessage }
});
}
+
+ if (translateOnReceive) {
+ if (!curr_fullMessage) {
+ curr_fullMessage = await browser.messages.getFull(message.id);
+ }
+ taLog.log("[ThunderAI] Pre-caching translation on receive for: " + message.headerMessageId);
+ await _generateTranslationForMessage(message.headerMessageId, null, {
+ messageData: { fullMessage: curr_fullMessage }
+ });
+ }
}
}
diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js
index 0347bc82..17a7da3a 100644
--- a/options/mzta-options-default.js
+++ b/options/mzta-options-default.js
@@ -142,7 +142,7 @@ export const prefs_default = {
summarize_display_mode: 'inline', // 'inline' or 'webchat'
summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline
translate: true,
- translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic
+ translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive
translate_display_mode: 'inline', // 'inline' or 'webchat'
translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline
translate_lang: '', // target language, fallback on default_chatgpt_lang
diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html
index 891dc485..b424bd9f 100644
--- a/pages/translate/mzta-translate.html
+++ b/pages/translate/mzta-translate.html
@@ -34,6 +34,7 @@
+
__MSG_prefs_OptionText_translate_auto_Info__
diff --git a/pages/translate/mzta-translate.js b/pages/translate/mzta-translate.js
index 84e6953e..a4812808 100644
--- a/pages/translate/mzta-translate.js
+++ b/pages/translate/mzta-translate.js
@@ -74,6 +74,7 @@ document.addEventListener("DOMContentLoaded", async () => {
document.querySelectorAll(".option-input").forEach(element => {
element.addEventListener("change", saveOptions);
});
+ document.getElementById('translate_auto').addEventListener('change', updateDisplayModeConstraint);
let translate_textarea = document.getElementById("translate_prompt_text");
let translate_save_btn = document.getElementById("btn_save_prompt");
@@ -118,6 +119,21 @@ document.addEventListener("DOMContentLoaded", async () => {
// Methods to manage options, derived from: /options/mzta-options.js
+function updateDisplayModeConstraint() {
+ const translate_auto_el = document.getElementById('translate_auto');
+ const display_mode_el = document.getElementById('translate_display_mode');
+ const autoVal = String(translate_auto_el.value);
+ if (autoVal === '2' || autoVal === '3') {
+ display_mode_el.value = 'inline';
+ display_mode_el.disabled = true;
+ browser.storage.sync.set({ translate_display_mode: 'inline' });
+ } else if (autoVal === '0') {
+ display_mode_el.disabled = true;
+ } else {
+ display_mode_el.disabled = false;
+ }
+}
+
function saveOptions(e) {
e.preventDefault();
let options = {};
@@ -232,4 +248,5 @@ async function restoreOptions() {
}
setCurrentChoice(getting);
+ updateDisplayModeConstraint();
}