Read Frog Content Script Architecture: How It Handles Page Translation Across Entrypoints
Read Frog centralizes page translation in a single PageTranslationManager instance that coordinates IntersectionObserver and MutationObserver instances to detect and translate content, while isolated content script entry points communicate via chrome.runtime.sendMessage to delegate translation tasks without sharing DOM references.
Read Frog is an open-source browser extension that injects multiple content scripts into webpages to provide seamless translation capabilities. Understanding how its content script architecture handles page translation across different entrypoints reveals sophisticated patterns for managing isolated worlds in Chrome Manifest V3 extensions. The system maintains translation state through a centralized manager while distributing detection responsibilities across specialized entry points.
Architecture Overview
Read Frog implements three distinct content-script entry points that serve different purposes within the same webpage:
host.content– Contains the core translation logic insrc/entrypoints/host.content/translation-control/page-translation.ts, handling page-wide translation, node-level processing, and shortcut triggersinterceptor.content– Observes the page for dynamic media elements like video players insrc/entrypoints/interceptor.content/index.ts, injecting interceptor scripts that forward messages to the hostguide.content– Manages the onboarding guide overlay throughsrc/entrypoints/guide.content/index.ts, remaining isolated from the translation observer graph to avoid performance overhead
All entry points share a common translation engine located under src/utils/host/translate, but only the host script instantiates the PageTranslationManager. Because Chrome Manifest V3 enforces isolated worlds for content scripts, entry points cannot directly share DOM references or JavaScript state. Instead, they communicate through chrome.runtime.sendMessage wrapped by src/utils/message.ts, with the host script serving as the single authority for all translation operations.
The PageTranslationManager Core
The PageTranslationManager class orchestrates page translation through multiple independent triggers registered in src/entrypoints/host.content/translation-control/page-translation.ts. When the host script loads, it initializes the manager and binds user interaction handlers:
- Touch-gesture triggers – Four-finger tap detection via
registerPageTranslationTriggers() - Keyboard shortcuts – User-configurable hotkeys bound in
src/entrypoints/host.content/translation-control/bind-translation-shortcut.ts - Configuration listeners – Mode changes (bilingual, translation-only) handled by
handleTranslationModeChangeinsrc/entrypoints/host.content/translation-control/handle-config-change.ts
The manager persists as a singleton throughout the page lifecycle, maintaining observer states and translation caches until explicitly stopped.
Translation Execution Flow
Observer Registration and Viewport Detection
When PageTranslationManager.start() executes, it establishes aggressive viewport monitoring to ensure translations render before content becomes visible. The manager registers two primary observation mechanisms:
- An
IntersectionObserverwatches top-level paragraph elements with a 600-pixel root margin, triggering translation logic when elements approach the viewport - Multiple
MutationObserverinstances monitor the entire document tree including shadow roots, detecting new nodes, style changes, and attribute modifications
The generous root margin ensures translateWalkedElement receives elements before the user scrolls to them, creating a seamless reading experience without visible translation delays.
DOM Walking and Element Labeling
Before translation occurs, elements must be marked with a unique traversal identifier. The manager calls walkAndLabelElement from src/utils/host/dom/traversal.ts:
const walkId = crypto.randomUUID()
walkAndLabelElement(container, walkId, config)
This function sets the data-read-frog-walked attribute on every element in the current traversal, creating a labeled subgraph that subsequent translation steps can identify. The UUID-based walk ID prevents stale elements from being processed multiple times if the DOM changes during translation.
Batching and Provider API Integration
When observers detect walked elements entering the viewport, translateWalkedElement in src/utils/host/translate/core/translation-walker.ts validates the element state:
- Checks for existing translation wrappers via
CONTENT_WRAPPER_CLASSto avoid duplicate processing - Verifies the element carries the current walk ID from
data-read-frog-walked - Recursively walks the DOM to distinguish block-level versus inline text fragments
Inline fragments batch together through translateNodes in src/utils/host/translate/core/translation-modes.ts, which implements bilingual and translation-only rendering modes. The system then delegates to provider-specific APIs under src/utils/host/translate/api/* to fetch and inject translations.
Cross-Entry Point Communication
Isolated entry points interact with the translation engine through message passing rather than direct instantiation.
The interceptor script (interceptor.content) detects dynamic media players inside isolated shadow roots that the host script cannot access directly. When it discovers a new player, it posts a message to the host script, which then adds the player's root element to the manager's observation set via observerTopLevelParagraphs and walkAndLabelElement.
The guide script (guide.content) remains completely separate from the translation pipeline. It toggles UI visibility without participating in DOM observation, preventing onboarding elements from triggering unnecessary translation cycles.
This architecture ensures that only the host script manipulates the PageTranslationManager state, while specialized entry points focus on detection and messaging.
State Management and Cleanup
Translation state persists until explicitly disabled. When users toggle translation off or change modes, handleTranslationModeChange restarts the manager with new configuration parameters.
Upon stopping, the manager executes removeAllTranslatedWrapperNodes() from src/utils/host/translate/dom/translation-cleanup.ts, which:
- Disconnects all
IntersectionObserverandMutationObserverinstances - Removes injected translation wrapper elements
- Restores the original DOM structure without translation artifacts
The following example demonstrates initializing translation from a popup UI:
// src/entrypoints/popup/atoms/auto-translate.ts
import { PageTranslationManager } from '@/entrypoints/host.content/translation-control/page-translation'
let manager: PageTranslationManager | null = null
async function enablePageTranslation() {
if (!manager) {
manager = new PageTranslationManager()
manager.registerPageTranslationTriggers()
}
await manager.start()
}
document.getElementById('auto-translate-toggle')?.addEventListener('click', () => {
manager?.isActive ? manager.stop() : void enablePageTranslation()
})
Summary
- Read Frog uses three isolated content script entry points (
host.content,interceptor.content,guide.content) that cannot share DOM references due to Chrome Manifest V3 security boundaries - The
PageTranslationManagerinsrc/entrypoints/host.content/translation-control/page-translation.tsserves as the single authority for translation state, coordinating observers and provider APIs - Preemptive observation via 600-pixel margin
IntersectionObserverand shadow root-awareMutationObserverinstances ensures translations render before content enters the viewport - DOM walking with
crypto.randomUUID()walk IDs insrc/utils/host/dom/traversal.tscreates labeled element sets that prevent duplicate processing - Cross-script communication occurs exclusively through
chrome.runtime.sendMessagewrappers insrc/utils/message.ts, with the interceptor forwarding shadow root detections to the host script - Complete cleanup via
removeAllTranslatedWrapperNodes()restores original content when translation disables
Frequently Asked Questions
How does Read Frog handle translation in shadow DOM elements that are isolated from the main document?
The interceptor.content entry point specifically monitors for dynamic media players and components rendered inside shadow roots. When it detects new shadow hosts, it posts a message to the host.content script via chrome.runtime.sendMessage. The host script then adds the shadow root to the PageTranslationManager's observation set, allowing the standard IntersectionObserver and MutationObserver logic to traverse and translate content within the isolated tree.
What prevents Read Frog from translating the same text multiple times during page mutations?
Before processing any element, the system assigns a unique walk ID using crypto.randomUUID() through walkAndLabelElement in src/utils/host/dom/traversal.ts. This ID attaches to elements via the data-read-frog-walked attribute. The translateWalkedElement function in src/utils/host/translate/core/translation-walker.ts checks this ID against the current session and skips elements already wrapped with CONTENT_WRAPPER_CLASS, ensuring idempotent translation even during aggressive DOM mutations.
Why does Read Frog use a 600-pixel root margin for its IntersectionObserver?
The 600-pixel root margin creates a buffer zone that triggers translation logic before content actually enters the viewport. According to the source code in src/entrypoints/host.content/translation-control/page-translation.ts, this preemptive observation ensures translations complete and render while content is still below the fold, preventing users from seeing untranslated text flash or load delays as they scroll through long documents.
Can multiple content script entry points create separate PageTranslationManager instances?
No, the architecture enforces a singleton pattern where only host.content instantiates and owns the PageTranslationManager. Other entry points like interceptor.content and guide.content run in isolated JavaScript worlds and cannot access the manager directly. They must communicate via chrome.runtime.sendMessage as implemented in src/utils/message.ts. This design prevents race conditions and ensures consistent translation state across all DOM mutations and user interactions.
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 →