How FluentRead Handles Full Page Translation: A Technical Deep Dive
FluentRead implements full page translation through a coordinated three-stage pipeline involving a background script for context menu handling, a content script for message routing, and a dedicated translation engine that uses IntersectionObserver and MutationObserver to translate content dynamically while preserving original text for restoration.
FluentRead is an open-source browser extension that enables seamless bilingual reading. Understanding how it performs full page translation requires examining the interplay between its background service worker, content scripts, and the core translation engine located in entrypoints/main/trans.ts.
The Three-Stage Translation Architecture
FluentRead's full page translation flow follows a strict separation of concerns across three distinct layers.
Stage 1: Context Menu Entry Point
The process begins in entrypoints/background.ts, where the extension registers a context menu item identified by CONTEXT_MENU_IDS.TRANSLATE_FULL_PAGE. When a user selects "全文翻译" (Translate Full Page) from the right-click menu, the background script dispatches a message to the active tab:
// entrypoints/background.ts
if (info.menuItemId === CONTEXT_MENU_IDS.TRANSLATE_FULL_PAGE) {
browser.tabs.sendMessage(tab.id, {
type: 'contextMenuTranslate',
action: 'fullPage'
});
}
This message includes action: 'fullPage' to distinguish it from other translation modes.
Stage 2: Content Script Message Routing
The content script in entrypoints/content.ts listens for contextMenuTranslate messages. Upon receiving the fullPage action, it invokes the translation engine:
// entrypoints/content.ts
browser.runtime.onMessage.addListener((request) => {
if (request.type === 'contextMenuTranslate') {
if (request.action === 'fullPage') {
autoTranslateEnglishPage();
} else if (request.action === 'restore') {
restoreOriginalContent();
}
}
});
This abstraction layer allows the background script to remain agnostic about the specific translation implementation.
Stage 3: Translation Engine Execution
The core logic resides in entrypoints/main/trans.ts. The autoTranslateEnglishPage() function (lines 78-122) orchestrates the full page translation process by combining node collection, viewport-aware processing, and dynamic content handling.
How the Full Page Translation Engine Works
The translation engine employs a sophisticated strategy to handle modern web pages without freezing the UI or missing dynamically loaded content.
Node Collection and Identification
The engine first gathers all translatable text nodes using grabAllNode(document.body). Each node receives a unique identifier (fr-node-${counter}) and its original HTML is stored in a Map called originalContents:
// entrypoints/main/trans.ts
const originalContents = new Map<string, string>();
let counter = 0;
function processNode(node: Element) {
const nodeId = `fr-node-${counter++}`;
originalContents.set(nodeId, node.innerHTML);
node.setAttribute('data-fr-node-id', nodeId);
node.setAttribute('data-fr-translated', 'true');
}
This preservation mechanism enables the restore functionality that reverts the page to its original state.
Viewport-Aware Translation with IntersectionObserver
To optimize performance on long pages, FluentRead uses an IntersectionObserver to translate nodes only when they enter the viewport. This prevents unnecessary API calls for content that remains below the fold:
// Conceptual implementation from trans.ts
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
translateNode(entry.target);
observer.unobserve(entry.target);
}
});
});
// Observe all collected nodes
nodes.forEach(node => observer.observe(node));
Handling Dynamic Content with MutationObserver
Modern web applications frequently inject content after the initial load. The translation engine attaches a MutationObserver to document.body to detect newly added nodes. When mutations occur, the engine processes new translatable elements and adds them to the translation queue, ensuring that content loaded via infinite scroll or AJAX gets translated without user intervention.
State Management and Content Restoration
When the user selects "撤销翻译" (Undo Translation) or disables the extension, the background script sends a restore action. The restoreOriginalContent() function in trans.ts iterates through all nodes marked with data-fr-translated="true", retrieves their original HTML from the originalContents Map, and reinstates the pristine content:
// entrypoints/main/trans.ts
export function restoreOriginalContent() {
document.querySelectorAll('[data-fr-translated="true"]').forEach(node => {
const nodeId = node.getAttribute('data-fr-node-id');
if (originalContents.has(nodeId)) {
node.innerHTML = originalContents.get(nodeId);
node.removeAttribute('data-fr-translated');
node.removeAttribute('data-fr-node-id');
}
});
originalContents.clear();
// Disconnect observers...
}
This ensures a clean state reversal without page reload.
Practical Implementation Examples
Triggering Full Page Translation Programmatically
While FluentRead primarily uses the context menu, you can invoke the translation engine directly from the content script context:
import { autoTranslateEnglishPage } from '@/entrypoints/main/trans';
// Bind to a custom keyboard shortcut
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
autoTranslateEnglishPage();
}
});
Restoring Original Content
To programmatically revert translations, listen for the restore message or call the function directly:
import { restoreOriginalContent } from '@/entrypoints/main/trans';
// Direct invocation
document.getElementById('restore-btn').addEventListener('click', () => {
restoreOriginalContent();
});
// Or via message passing
browser.runtime.onMessage.addListener((msg) => {
if (msg.action === 'restore') {
restoreOriginalContent();
}
});
Summary
FluentRead's full page translation system demonstrates a sophisticated approach to browser extension architecture:
- Three-stage pipeline: Background script handles UI entry points, content script manages message routing, and
trans.tscontains the core engine. - Performance optimization:
IntersectionObserverensures only visible content gets translated, reducing API costs and improving load times. - Dynamic content support:
MutationObservercaptures AJAX-loaded content without requiring manual re-triggering. - State preservation: A
Mapstores original HTML content, enabling complete restoration viarestoreOriginalContent().
The implementation in entrypoints/main/trans.ts provides a reusable pattern for any extension requiring non-destructive DOM manipulation at scale.
Frequently Asked Questions
How does FluentRead identify which text elements to translate?
FluentRead uses the grabAllNode(document.body) function to recursively collect all text-bearing DOM elements. Each node receives a unique fr-node-${counter} identifier and is marked with data-fr-translated="true" and data-fr-node-id attributes to track its state and enable restoration.
What happens if new content loads after I start the translation?
The translation engine attaches a MutationObserver to document.body that detects DOM changes. When new nodes are added (such as via infinite scroll or AJAX requests), the observer automatically processes them and adds them to the translation queue, ensuring continuous translation without user intervention.
Can I revert the page to its original language without refreshing?
Yes. FluentRead stores the original HTML of every translated node in a Map called originalContents. When you select "撤销翻译" (Undo Translation) or trigger restoreOriginalContent(), the function retrieves the original content using the data-fr-node-id attribute and restores the pristine HTML, disconnecting all observers in the process.
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 →