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:

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:

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:

  1. An IntersectionObserver watches top-level paragraph elements with a 600-pixel root margin, triggering translation logic when elements approach the viewport
  2. Multiple MutationObserver instances 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_CLASS to 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 IntersectionObserver and MutationObserver instances
  • 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 PageTranslationManager in src/entrypoints/host.content/translation-control/page-translation.ts serves as the single authority for translation state, coordinating observers and provider APIs
  • Preemptive observation via 600-pixel margin IntersectionObserver and shadow root-aware MutationObserver instances ensures translations render before content enters the viewport
  • DOM walking with crypto.randomUUID() walk IDs in src/utils/host/dom/traversal.ts creates labeled element sets that prevent duplicate processing
  • Cross-script communication occurs exclusively through chrome.runtime.sendMessage wrappers in src/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →