diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 12bdb601..e36db6ab 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2149,6 +2149,14 @@ "message": "Comma-separated list of language codes (e.g., en, fr, it) to skip for automatic translation. If the email is in one of these languages, it won't be translated automatically or the manual button won't be shown.", "description": "" }, + "prefs_OptionText_translate_summary_translation": { + "message": "Summary Translation", + "description": "" + }, + "prefs_OptionText_translate_summary_translation_Info": { + "message": "When translating, also translate the existing summary. When summarizing, use the existing translation as input.", + "description": "" + }, "prefs_OptionText_Translate_main_prompt": { "message": "The prompt describing the translation task:", "description": "" diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 06c0d250..760534e8 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -103,6 +103,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `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. | +| `translate_summary_translation` | `false` | Links translation and summary features. When translating, also translates the existing cached summary. When summarizing, uses the existing cached translation as input instead of the original email body. | ### Summarize Settings Page (`pages/summarize/`) @@ -141,6 +142,7 @@ The translate settings page provides: 4. **Max display length** (`translate_max_display_length`) — number input, limits inline translation text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended. 5. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. 6. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. +7. **Summary Translation checkbox** (`translate_summary_translation`) — links translation and summary features. When enabled: translating an email also translates the cached summary (if present), and summarizing an email uses the cached translation (if present) as input instead of the original email body. ## Adding a New Preference diff --git a/js/mzta-utils-prompt.js b/js/mzta-utils-prompt.js index b123ff29..c8646fbf 100644 --- a/js/mzta-utils-prompt.js +++ b/js/mzta-utils-prompt.js @@ -132,7 +132,7 @@ export const taPromptUtils = { }, - async buildSummaryPrompt(messageDataArray) { + async buildSummaryPrompt(messageDataArray, overrideBodyText = null) { const specialPrompts = await getSpecialPrompts(); const prompt = specialPrompts.find(p => p.id === 'prompt_summarize'); const prompt_email = specialPrompts.find(p => p.id === 'prompt_summarize_email_template'); @@ -151,10 +151,17 @@ export const taPromptUtils = { const messages_list = []; for (let entry of messageDataArray) { - const bodyHtml = getMailBody(entry.fullMessage); - let bodyText = htmlBodyToPlainText(bodyHtml.html); - if (bodyText.length === 0) { - bodyText = bodyHtml.text || ''; + let bodyText; + let bodyHtml; + if (overrideBodyText && messageDataArray.length === 1) { + bodyText = overrideBodyText; + bodyHtml = { html: '', text: overrideBodyText }; + } else { + bodyHtml = getMailBody(entry.fullMessage); + bodyText = htmlBodyToPlainText(bodyHtml.html); + if (bodyText.length === 0) { + bodyText = bodyHtml.text || ''; + } } messages_list.push(await taPromptUtils.preparePrompt({ @@ -173,7 +180,7 @@ export const taPromptUtils = { return { promptText, promptInfo: prompt }; }, - async buildTranslationPrompt(fullMessage, lang) { + async buildTranslationPrompt(fullMessage, lang, overrideBodyText = null) { const specialPrompts = await getSpecialPrompts(); const prompt = specialPrompts.find(p => p.id === 'prompt_translate_this'); @@ -182,10 +189,15 @@ export const taPromptUtils = { promptText = browser.i18n.getMessage('prompt_translate_this_full_text'); } - const bodyHtml = getMailBody(fullMessage); - let bodyText = htmlBodyToPlainText(bodyHtml.html); - if (bodyText.length === 0) { - bodyText = bodyHtml.text || ''; + let bodyText; + if (overrideBodyText) { + bodyText = overrideBodyText; + } else { + const bodyHtml = getMailBody(fullMessage); + bodyText = htmlBodyToPlainText(bodyHtml.html); + if (bodyText.length === 0) { + bodyText = bodyHtml.text || ''; + } } const fullPrompt = promptText + " " + lang + ". \"" + bodyText + "\""; diff --git a/mzta-background.js b/mzta-background.js index ffb87c08..fe7e56b5 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -643,6 +643,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId = null, options do_debug: prefs_default.do_debug, default_chatgpt_lang: prefs_default.default_chatgpt_lang, summarize_max_display_length: prefs_default.summarize_max_display_length, + translate_summary_translation: prefs_default.translate_summary_translation, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); @@ -687,7 +688,16 @@ async function _generateSummaryForMessage(headerMessageId, tabId = null, options return; } - const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message, fullMessage }]); + // Summary Translation: if enabled and a cached translation exists, use it as input + let overrideBodyText = null; + if (prefs.translate_summary_translation) { + let cachedTranslation = await translationStore.loadTranslation(headerMessageId); + if (cachedTranslation && !cachedTranslation.error && cachedTranslation.translated_text) { + overrideBodyText = cachedTranslation.translated_text; + } + } + + const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message, fullMessage }], overrideBodyText); const cmd = new mzta_specialCommand({ prompt: promptText, @@ -730,6 +740,8 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt default_chatgpt_lang: prefs_default.default_chatgpt_lang, translate_lang: prefs_default.translate_lang, translate_max_display_length: prefs_default.translate_max_display_length, + translate_summary_translation: prefs_default.translate_summary_translation, + summarize_max_display_length: prefs_default.summarize_max_display_length, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); @@ -796,6 +808,38 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt }; await translationStore.saveTranslation(translationData, headerMessageId); if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } }); + + // Summary Translation: if enabled and a cached summary exists, also translate the summary + if (prefs.translate_summary_translation) { + let cachedSummary = await summaryStore.loadSummary(headerMessageId); + if (cachedSummary && !cachedSummary.error && cachedSummary.summary) { + try { + const { promptText: summaryTranslationPrompt } = await taPromptUtils.buildTranslationPrompt(null, lang, cachedSummary.summary); + const cmdSummary = new mzta_specialCommand({ + prompt: summaryTranslationPrompt, + llm: connectionType, + do_debug: prefs.do_debug, + config: {} + }); + await cmdSummary.initWorker(); + const translatedSummary = await cmdSummary.sendPrompt(); + let cleanedSummary = cleanSummaryText(translatedSummary); + const md = window.markdownit(); + let summaryHtml = md.render(translatedSummary); + const summaryData = { + summary: cleanedSummary, + summary_html: summaryHtml, + summary_date: new Date(), + headerMessageId: headerMessageId + }; + await summaryStore.saveSummary(summaryData, headerMessageId); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); + } catch (summaryError) { + console.error("[ThunderAI] Error translating summary:", summaryError); + } + } + } + taWorkingStatus.stopWorking(); } catch (error) { @@ -935,7 +979,8 @@ async function _openSummaryWebchat(headerMessageId, tabId) { const curr_message = messageResult.messages[0]; const curr_message_full = await browser.messages.getFull(curr_message.id); - const connectionType = getConnectionType(await browser.storage.sync.get(prefs_default), {}, 'summarize'); + const prefs = await browser.storage.sync.get(prefs_default); + const connectionType = getConnectionType(prefs, {}, 'summarize'); if (connectionType === 'chatgpt_web') { const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported'); await summaryStore.saveError(headerMessageId, errorMsg); @@ -943,7 +988,16 @@ async function _openSummaryWebchat(headerMessageId, tabId) { return; } - const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }]); + // Summary Translation: if enabled and a cached translation exists, use it as input + let overrideBodyText = null; + if (prefs.translate_summary_translation) { + let cachedTranslation = await translationStore.loadTranslation(headerMessageId); + if (cachedTranslation && !cachedTranslation.error && cachedTranslation.translated_text) { + overrideBodyText = cachedTranslation.translated_text; + } + } + + const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }], overrideBodyText); promptInfo.headerMessageId = headerMessageId; promptInfo.summaryTabId = tabId; diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c1015507..be910084 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -147,6 +147,7 @@ export const prefs_default = { translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang translate_exclude_lang: '', // languages to do not translate + translate_summary_translation: false, // link translation and summary features spamfilter_show_msg_panel: true, ...generated_prefs } diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html index dc7a4e9d..7a3fa5a0 100644 --- a/pages/translate/mzta-translate.html +++ b/pages/translate/mzta-translate.html @@ -81,6 +81,15 @@ +