How FluentRead Implements Context Menu Integration for Translations
FluentRead registers two browser context menu items—"全文翻译" (Translate Full Page) and "撤销翻译" (Restore Original)—in the background script, routes user clicks through the browser.contextMenus API to content scripts, and maintains per-tab translation state using a Map to dynamically enable or disable menu items based on the current page status.
The open-source browser extension FluentRead (bistutu/fluentread) provides seamless context menu integration for translations, allowing users to translate entire web pages or revert to original content directly from the right-click menu. This functionality leverages the standard WebExtension browser.contextMenus API while implementing a robust state management system to track translation status across browser tabs.
Architecture Overview
FluentRead’s context menu system follows a clean separation of concerns across three layers:
- Constants layer (
entrypoints/utils/constant.ts) – Defines immutable menu IDs shared across the extension. - Background layer (
entrypoints/background.ts) – Creates menu items, handles click events, manages per-tab state, and updates UI availability. - Content layer (
entrypoints/content.ts) – Receives messages from the background script and executes page-level translation or restoration logic.
Defining Menu Constants in constant.ts
To ensure type safety and prevent ID collisions, FluentRead centralizes context menu identifiers in a constants file. This approach allows the background and content scripts to reference the same string values without hardcoding literals.
// entrypoints/utils/constant.ts
export const CONTEXT_MENU_IDS = {
TRANSLATE_FULL_PAGE: 'fluent-read-translate-full-page',
RESTORE_ORIGINAL: 'fluent-read-restore-original',
};
These constants are imported throughout the extension to register menu items and validate click events.
Registering Context Menu Items in background.ts
When the extension initializes, the background script creates a parent menu labeled FluentRead and attaches two child items using the browser.contextMenus.create API. The restore option is initially disabled because no translation has occurred yet.
// entrypoints/background.ts
browser.contextMenus.create({
id: 'fluentread-parent',
title: 'FluentRead',
contexts: ['page', 'selection'],
});
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.TRANSLATE_FULL_PAGE,
title: '全文翻译',
parentId: 'fluentread-parent',
contexts: ['page', 'selection'],
});
browser.contextMenus.create({
id: CONTEXT_MENU_IDS.RESTORE_ORIGINAL,
title: '撤销翻译',
parentId: 'fluentread-parent',
contexts: ['page', 'selection'],
enabled: false, // initially disabled
});
The contexts array restricts these items to page and selection contexts, ensuring they appear when users right-click on web content rather than browser UI elements.
Handling User Interactions
Routing Clicks to Content Scripts
The background script listens for browser.contextMenus.onClicked events and routes them to the appropriate content script using browser.tabs.sendMessage. This decouples the UI layer from the translation logic.
// entrypoints/background.ts
browser.contextMenus.onClicked.addListener((info, tab) => {
if (!tab?.id) return;
if (info.menuItemId === CONTEXT_MENU_IDS.TRANSLATE_FULL_PAGE) {
browser.tabs.sendMessage(tab.id, {
type: 'contextMenuTranslate',
action: 'fullPage',
});
translationStateMap.set(tab.id, true);
updateContextMenus(tab.id);
} else if (info.menuItemId === CONTEXT_MENU_IDS.RESTORE_ORIGINAL) {
browser.tabs.sendMessage(tab.id, {
type: 'contextMenuTranslate',
action: 'restore',
});
translationStateMap.set(tab.id, false);
updateContextMenus(tab.id);
}
});
Managing Translation State Per Tab
To prevent users from translating already-translated pages or restoring untranslated ones, FluentRead maintains a translationStateMap that tracks whether each tab currently displays translated content. The updateContextMenus function synchronizes the menu UI with this state.
// entrypoints/background.ts (excerpt)
const updateContextMenus = (tabId) => {
const isTranslated = translationStateMap.get(tabId) || false;
browser.contextMenus.update(CONTEXT_MENU_IDS.TRANSLATE_FULL_PAGE, {
enabled: !isTranslated,
title: isTranslated ? '全文翻译 (已翻译)' : '全文翻译',
});
browser.contextMenus.update(CONTEXT_MENU_IDS.RESTORE_ORIGINAL, {
enabled: isTranslated,
title: isTranslated ? '撤销翻译' : '撤销翻译 (无翻译)',
});
};
This dynamic update provides immediate visual feedback: the translate option shows "(已翻译)" when disabled, and the restore option shows "(无翻译)" when no translation exists.
Processing Translation Requests in content.ts
The content script listens for messages from the background script and delegates to the appropriate translation functions. It first checks whether the extension is enabled in the user's settings before executing any page modifications.
// entrypoints/content.ts
if (message.type === 'contextMenuTranslate') {
if (config.on === false) {
sendResponse({status: 'disabled'});
return true;
}
if (message.action === 'fullPage') {
autoTranslateEnglishPage(); // ↔ translate whole page
sendResponse({status: 'success', action: 'translated'});
return true;
} else if (message.action === 'restore') {
restoreOriginalContent(); // ↔ revert to original
sendResponse({status: 'success', action: 'restored'});
return true;
}
}
The autoTranslateEnglishPage() and restoreOriginalContent() functions, implemented in entrypoints/main/trans.ts, handle the actual DOM manipulation and caching of original content.
Synchronizing State Across Tab Lifecycle Events
To ensure menu states remain accurate when users switch tabs or navigate to new pages, the background script hooks into browser tab events. It clears translation flags when pages reload and removes stale entries when tabs close.
// entrypoints/background.ts (excerpt)
browser.tabs.onActivated.addListener(info => updateContextMenus(info.tabId));
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === 'complete') {
translationStateMap.set(tabId, false);
updateContextMenus(tabId);
}
});
browser.tabs.onRemoved.addListener(tabId => translationStateMap.delete(tabId));
This lifecycle management prevents state leakage between navigation events and ensures that fresh pages always present the translate option as enabled.
Summary
- FluentRead implements context menu integration for translations using the standard WebExtension
browser.contextMenusAPI, creating a parent menu with two actionable child items. - State management relies on a
translationStateMapthat tracks per-tab translation status, enabling dynamic UI updates that prevent invalid actions (translating already-translated pages). - Message passing bridges the background script and content scripts, with the background handling UI events and the content script executing
autoTranslateEnglishPage()orrestoreOriginalContent(). - Lifecycle synchronization ensures menu states reset correctly on tab switches, page reloads, and tab closures through listeners on
tabs.onActivated,tabs.onUpdated, andtabs.onRemoved.
Frequently Asked Questions
How does FluentRead prevent users from translating a page twice?
FluentRead maintains a translationStateMap in entrypoints/background.ts that records whether each tab currently displays translated content. When a user clicks "全文翻译", the background script sets the state to true and calls updateContextMenus(), which disables the translate option and enables the restore option. This state persists until the user clicks "撤销翻译" or navigates to a new page.
Can I trigger FluentRead's translation programmatically without using the context menu?
Yes. You can send a message directly to the content script using the WebExtension messaging API. The background script listens for contextMenuTranslate messages with actions fullPage or restore, and you can replicate this from another extension or developer tools by calling browser.tabs.sendMessage(tabId, {type: 'contextMenuTranslate', action: 'fullPage'}).
Why does the "撤销翻译" option appear disabled when I first install the extension?
The "撤销翻译" (Restore Original) menu item is initialized with enabled: false in entrypoints/background.ts because no translation has occurred yet. The menu only becomes active after you successfully translate a page, which sets the tab's state in translationStateMap to true and triggers updateContextMenus() to enable the restore option while disabling the translate option.
Where does FluentRead store the original page content before translation?
While the background script tracks translation state, the actual storage and restoration of original content occurs in the content script layer. When autoTranslateEnglishPage() is invoked (referenced in entrypoints/content.ts and implemented in entrypoints/main/trans.ts), the extension caches the original DOM content before replacing it with translated text. Calling restoreOriginalContent() retrieves this cached data to revert the page.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →