From f10beb809771e0e3b19a4a18241107a648443dbb Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:55:06 +0100 Subject: [PATCH] onreceive translate added. see #247 #721 --- claude-spec/01-architecture.md | 23 ++++++++++- claude-spec/05-options.md | 4 +- mzta-background.js | 60 +++++++++++++++++++---------- options/mzta-options-default.js | 2 +- pages/translate/mzta-translate.html | 1 + pages/translate/mzta-translate.js | 17 ++++++++ 6 files changed, 83 insertions(+), 24 deletions(-) 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(); }