Translation Toggle Logic and Content Detection#
This document explains the logic for toggling translation visibility in Read Frog, including how the system detects translated content nodes and their wrappers, differentiates between original and translated text, and implements intuitive toggle behavior. It also covers test coverage and user experience improvements.
Toggle Logic Overview#
The translation mode is now selectable by users via a dedicated mode selector in the popup UI. This selector allows switching between bilingual and translation-only modes, with localized labels for each option. When a user selects "translation-only" mode and the current provider does not support it (for example, Google), the system will automatically switch to a supported provider, such as Microsoft (if enabled) or the first enabled LLM provider. This ensures that the selected translation mode is always available and prevents unsupported configurations.
The translation mode selector updates the configuration (translate.mode) in real time, and the DOM continues to reflect the current mode using the data-read-frog-translation-mode attribute on wrapper elements. The toggle logic and translation functions (translateNodesBilingualMode and translateNodeTranslationOnlyMode) operate according to the selected mode, as described above.
Detection of Translated Content Nodes and Wrappers#
Translated content nodes are identified using the isTranslatedContentNode(node) function, which returns true if the node is an HTMLElement with either the BLOCK_CONTENT_CLASS or INLINE_CONTENT_CLASS CSS class. Wrappers for translated content are detected with isTranslatedWrapperNode(node), which checks for the CONTENT_WRAPPER_CLASS class.
To find the wrapper for a translated node, findPreviousTranslatedWrapper(node: Element | Text, walkId: string) checks if the node itself is a wrapper (with a different walkId) or looks for a wrapper as a child that doesn't match the current walkId. The wrapper element always includes a data-read-frog-translation-mode attribute indicating the mode (bilingual or translationOnly) and a data-read-frog-walked attribute for walk tracking.
The system uses additional DOM attributes and classes to label nodes during traversal, such as WALKED_ATTRIBUTE, BLOCK_ATTRIBUTE, INLINE_ATTRIBUTE, and PARAGRAPH_ATTRIBUTE, which help distinguish between block and inline nodes and manage translation state. The FLOAT_WRAP_ATTRIBUTE (data-read-frog-float-wrap) is applied to block translations when they should maintain layout flow around floated sibling elements.
Exclusion of Hidden Elements:
Elements that are visually hidden or marked as not intended for user visibility are explicitly excluded from translation and text extraction. This includes:
- Elements with the
sr-onlyorvisually-hiddenclasses - Elements with
aria-hidden="true" - Elements with
display: noneorvisibility: hiddenstyles - Elements with the
hiddenattribute - Certain tags like
<script>
These elements are detected using the isDontWalkIntoAndDontTranslateAsChildElement function and are skipped during both translation and text extraction. This ensures that only visible, user-facing content is translated.
Ruby Annotation Exclusion:
Ruby annotations (RT and RP tags) used in East Asian typography are explicitly excluded from translation traversal. The <rt> (ruby text) and <rp> (ruby parenthesis) tags are added to DONT_WALK_AND_TRANSLATE_TAGS, ensuring that pronunciation guides and fallback parentheses in ruby annotations are not translated separately from their base text. This prevents translation errors and maintains the semantic relationship between ruby base text and annotations.
The isDontWalkIntoButTranslateAsChildElement function identifies elements that should not be traversed into but whose text content can still be translated as a child element. This includes elements with the NOTRANSLATE_CLASS and certain tags like <code>. The sr-only and visually-hidden classes are explicitly handled by isDontWalkIntoAndDontTranslateAsChildElement to ensure accessibility elements (like screen-reader-only content) are consistently excluded from both traversal and translation.
Site-Specific Exclusions:
Certain elements are excluded from translation on specific sites using the CUSTOM_DONT_WALK_INTO_ELEMENT_SELECTOR_MAP. For www.reddit.com, the following elements are excluded:
faceplate-screen-reader-content > *- Screen reader contentreddit-header-large *- Header navigation elementsshreddit-comment-action-row > *- Comment action buttonsshreddit-post-flair- Post flair tags (to prevent misalignment during translation)
For github.com, the following elements are excluded:
table.diff-table- PR review diff tables (to prevent code diffs in review comments from being translated). This feature is live as of v1.31.2 and ensures diff tables in GitHub PR reviews are properly skipped during page translation
These site-specific exclusions ensure that UI elements, navigation, metadata, and code snippets are not translated, preventing visual misalignment and maintaining the intended user experience on each platform.
Site-Specific Block Translation Rules:
In addition to exclusions, certain sites have custom rules that force specific elements to be translated as block-level paragraphs rather than inline text. This ensures proper translation presentation and prevents visual rendering issues.
For www.reddit.com, the following element is forced to block translation:
shreddit-post-text-body- Post text body content (to ensure post bodies are translated as block paragraphs rather than being squeezed into a single inline translation area). This feature is live as of v1.31.2 and forces Reddit post text bodies to be translated as block elements for better translation quality
These force-block rules use the CUSTOM_FORCE_BLOCK_TRANSLATION_SELECTOR_MAP and ensure that content is rendered with appropriate spacing and formatting, improving readability and maintaining the intended layout of translated content.
Exclusion of Structural Elements in Main Content Mode:
In main content mode (when config.translate.page.range is not set to 'all'), top-level structural elements such as <header>, <footer>, <nav>, and <aside> are excluded from translation to avoid translating site-wide navigation and structural elements. However, when these same elements appear inside content containers like <article> or <main>, they are included in translation, as they are likely to be content-specific (such as article titles within a <header> inside an <article>).
This behavior is implemented using the isInsideContentContainer() helper function added to src/utils/host/dom/filter.ts. This function walks up the parent hierarchy to check if an element is nested inside an <article> or <main> tag. The exclusion logic in isDontWalkIntoAndDontTranslateAsChildElement checks !isInsideContentContainer(element) to ensure that only top-level structural elements (not inside content containers) are skipped, while content headers and footers are properly translated.
This fix resolves a bug (issue #940) where article titles wrapped in <header> elements inside <article> or <main> containers were incorrectly excluded from translation in main content mode.
Differentiation Between Original and Translated Text#
- Bilingual mode: Original text remains in the DOM. Translated text is wrapped in a
<span>element with theNOTRANSLATE_CLASSandCONTENT_WRAPPER_CLASSclasses, and thedata-read-frog-translation-mode="bilingual"attribute. Inside this wrapper, the translated content itself is further classified as inline or block usingINLINE_CONTENT_CLASSorBLOCK_CONTENT_CLASS. - Translation-only mode: The original content is replaced by a wrapper with the
NOTRANSLATE_CLASSandCONTENT_WRAPPER_CLASSclasses, and thedata-read-frog-translation-mode="translationOnly"attribute. The original HTML is preserved in memory and restored if toggled off.
Enhanced Inline Display Detection:
The system uses an enhanced isInlineDisplay() function to accurately detect inline display values. In addition to checking for display values starting with "inline" or "contents", the function recognizes ruby-related CSS display values as inline:
rubyruby-baseruby-textruby-base-containerruby-text-container
This ensures that ruby annotations are correctly identified as inline elements during translation traversal. However, display values like "block ruby" are not treated as inline, maintaining proper block/inline distinction for complex layouts.
Empty Block Element Handling:
During traversal, empty block elements (elements where textContent?.trim() === "" and not forced as block) are ignored to simplify the translation logic. This prevents unnecessary processing of decorative or structural elements without text content and avoids splitting paragraphs at empty block descendants.
Float Layout Detection:
When rendering block translations, the system detects if there are floated elements (CSS float: left or float: right) that would cause the translation to drop below them. The detection logic includes:
isFloatedElement(): Checks if an element has CSS float left or righthasVisibleLayoutBox(): Verifies the element has non-zero width and heightfindActiveFloatSibling(): Searches for floated elements in sibling nodes that vertically overlap with the paragraph being translatedshouldWrapInsideFloatFlow(): Determines if a translation needs the float-wrap attribute
When an active floated sibling is detected, the data-read-frog-float-wrap="true" attribute is added to the block translation element. This triggers the CSS rule .read-frog-translated-block-content[data-read-frog-float-wrap="true"] which applies display: block !important; to preserve layout flow. This prevents translations from dropping below floated images, info boxes, or other floated content on sites like Wikipedia. The detection runs during translation insertion for any element marked with the paragraph attribute.
As of v1.31.3 (PR #1188), the system correctly keeps float-wrapped bilingual translations in flow beside floated content, ensuring that translations maintain proper positioning relative to floated elements without dropping below them.
For example, a translated block in bilingual mode:
<span class="notranslate read-frog-content-wrapper" data-read-frog-translation-mode="bilingual">
<span class="notranslate read-frog-block-content">[Translated Text]</span>
</span>
In translation-only mode:
<span class="notranslate read-frog-content-wrapper" data-read-frog-translation-mode="translationOnly" style="display: contents;">
[Translated Text]
</span>
Intuitive Toggle Behavior#
The toggle logic is designed for simplicity and predictability. When a user requests a toggle (e.g., via a gesture or UI action), the system checks if a translated wrapper exists for the target node. If it does, the wrapper is removed—restoring the original content in translation-only mode, or simply removing the translation in bilingual mode. If not, translation is performed and the result is wrapped and inserted. This ensures toggling is a single, reversible action.
For page-wide translation, the PageTranslationManager class manages translation state. It uses an IntersectionObserver to translate elements as they enter the viewport and a four-finger tap gesture to toggle translation for the entire page. This gesture-based toggle is both discoverable and avoids accidental activation.
Automatic Tab Title Translation:
When page translation is enabled, the browser tab title (document.title) is automatically translated. The system tracks source title changes using a MutationObserver and updates the translated title in real-time. When translation is stopped, the original source title is automatically restored. This feature operates only in the top-level window (not in iframes), verified by checking window === window.top.
To ensure proper context-aware translation of the title, the article context is primed before translating the title by calling getOrFetchArticleData(). The translation request uses a version-controlled mechanism (titleRequestVersion) to discard stale translation results. If the source title changes during a translation request, only the result for the latest source title is applied. This prevents race conditions where outdated translations could overwrite current content. Additionally, the translation uses a targeted hash tag (pageTitleTranslation) to avoid cache collisions with regular page content translations.
During translation, a spinner is shown in the wrapper to indicate that translation is in progress. The spinner is styled with CSS isolation to maintain consistent appearance across all pages, regardless of host page CSS rules.
Selection Toolbar Translation:
The selection toolbar provides an intuitive interface for translating selected text. Users can select text on a page to open the selection toolbar, which includes translation controls.
Target Language Selection:
A target language selector is available directly in the selection toolbar translation popover, allowing users to choose their preferred target language without navigating to settings. This selector is integrated into the translation interface for quick access and immediate language switching during text selection.
Pin Support for Selection Toolbar Popovers:
Selection toolbar popovers can be pinned to keep them visible while interacting with the page. This allows users to maintain access to translation controls and results even when clicking elsewhere or scrolling, improving the workflow for translating multiple selections or comparing translations.
Interactive Element Click Handling:
The selection toolbar correctly handles clicks on interactive elements (buttons, links, inputs, textareas, selects, summary elements, or elements with role="button") outside the current selection. When users click on these interactive controls that are not part of the selected text, the toolbar is properly dismissed rather than inappropriately reappearing.
The system uses the event's composedPath() to capture the original interactive target synchronously before selection changes, which correctly handles:
- Shadow DOM event retargeting (where clicks on shadow DOM elements are retargeted to their host elements)
- Interactive elements nested under button ancestors
- Comprehensive detection of interactive elements beyond just
HTMLButtonElement
As of v1.31.3 (PR #1185), retargeted interactive clicks are properly filtered. The implementation walks through the event's composed path to identify the true interactive element that was clicked, regardless of shadow DOM boundaries or event retargeting mechanisms. This ensures that clicks originating from buttons or other interactive elements inside shadow roots are correctly recognized, even when the event's target property is retargeted to the shadow host element.
The implementation checks whether the interactive element is contained within the current selection using selection.containsNode(). If the interactive element is outside the selection, the toolbar is hidden; if it's inside the selection, the toolbar remains visible. This ensures intuitive behavior when users interact with translation controls within the toolbar itself while preventing unexpected toolbar reappearance when clicking unrelated page controls.
Overlay Selection Filtering:
The selection toolbar ignores text selections made within its own overlay UI elements to prevent the toolbar from appearing or changing state in response to overlay interactions. Overlay elements (popovers, toaster, toolbar surfaces) are marked with the data-rf-selection-overlay-root attribute, and the toolbar detects and ignores selection events originating from these marked elements.
When users select text within overlay UI (e.g., selecting text in a translation result), the system preserves the original page selection session rather than replacing it. This ensures that rerunning toolbar actions (such as regenerating a translation) continues to use the original page paragraphs and context, maintaining a consistent workflow and preventing unwanted toolbar state changes.
The filtering logic inspects mousedown, mouseup, and selectionchange events to determine if they originate from overlay elements. Selection boundary nodes (anchor nodes, focus nodes, and range containers) are checked against the overlay container, the overlay shadow root (if present), and any elements with the overlay root attribute. If the selection is contained within an overlay, the original page selection state is preserved and the toolbar does not show, hide, or update inappropriately. The implementation uses the event's composedPath() to correctly handle selections across shadow DOM boundaries, ensuring that text selections inside shadow roots within overlay elements are properly detected and filtered.
Translation Flow and Request Handling:
The selection toolbar translation flow is designed for reliability and responsiveness. The system includes hardened stale-request cancellation handling to manage rapid selection changes. When a user makes a selection and then quickly changes it, pending translation requests for the previous selection are cancelled, ensuring that only the most current selection is translated. This prevents race conditions and ensures that outdated translations do not overwrite current content, providing a smooth and predictable translation experience.
To further optimize performance, the system avoids re-fetching translation resources after page navigation (issue #1064). Resources loaded during the initial page load are cached and reused across navigation events, reducing unnecessary network requests and improving responsiveness.
Error Handling:
Translation errors are displayed as inline alerts within the selection toolbar, providing immediate, non-intrusive feedback to users. The SelectionToolbarErrorAlert component renders errors as alert UI elements with an alert icon, an error title, and a description, using proper spacing and positioning within the toolbar. This approach unifies error handling across translation and custom action features (such as Dictionary custom actions), replacing the previous toast notification system.
Before attempting translation, the system performs precheck validation to ensure the provider is available and enabled. If prechecks fail, the system immediately displays an inline alert with localized error messages (e.g., "Selected provider is unavailable" or "Selected provider is disabled"), preventing unnecessary API calls and providing instant feedback. During runtime, if a translation request fails, the error is caught and displayed inline using the same alert component. Error messages are extracted from the API response and displayed in a user-friendly format.
The inline alert styling includes destructive borders and backgrounds for visual emphasis, proper text wrapping, and consistent spacing. Error alerts are positioned below the translation content area and above the footer, ensuring they are visible without obstructing other UI elements. When users retry a translation (via the "Regenerate" button), the error alert is automatically cleared before the new request begins. If the retry succeeds, the error alert remains hidden, providing a seamless recovery experience.
Test Coverage#
Unit and integration tests cover the detection logic for translated content nodes and wrappers, as well as the new translation-only mode and complex DOM structures. The isTranslatedContentNode function is tested to ensure it returns true for elements with the correct classes and false otherwise, including for text nodes. The findPreviousTranslatedWrapper function is tested to verify it finds the correct wrapper for translated nodes, returns null for non-translated content, and correctly traverses multiple parent levels. The translation-only mode is tested to ensure that original content is restored correctly when toggled off, and that the wrapper and attributes are set as expected. Integration tests now cover mixed inline/block/nested structures and switching between translation modes. Numeric content exclusion and <pre> tag exclusion are also covered to ensure numbers and preformatted/code blocks are not translated or wrapped.
Hidden Element Exclusion Tests:
Additional unit tests verify that elements with the sr-only or visually-hidden classes, elements with aria-hidden="true", and similar hidden elements are excluded from translation and text extraction. This ensures that only visible content is processed, preventing hidden or accessibility-only content from being translated or included in extracted text. Tests also verify that isDontWalkIntoButTranslateAsChildElement correctly excludes sr-only and visually-hidden elements (returning false), while isDontWalkIntoAndDontTranslateAsChildElement correctly identifies these elements for complete exclusion.
Site-Specific Exclusion Tests:
Unit tests verify that site-specific elements in the CUSTOM_DONT_WALK_INTO_ELEMENT_SELECTOR_MAP are correctly excluded. For www.reddit.com, tests confirm that shreddit-post-flair elements are identified by isCustomDontWalkIntoElement and excluded by isDontWalkIntoAndDontTranslateAsChildElement, preventing flair tag misalignment during Reddit translations. For github.com, tests confirm that table.diff-table elements are correctly identified and excluded, ensuring that PR review code snippets are not translated and maintaining the integrity of code diffs displayed in review comments.
Site-Specific Block Translation Tests:
Unit tests verify that site-specific force-block translation rules in the CUSTOM_FORCE_BLOCK_TRANSLATION_SELECTOR_MAP work correctly. For www.reddit.com, tests confirm that shreddit-post-text-body elements are identified by isCustomForceBlockTranslation, ensuring that Reddit post text bodies are translated as block paragraphs rather than inline text, preventing visual rendering issues where post content would be squeezed into a single inline translation area.
Content Container Context Tests:
Unit tests verify that structural elements like <header>, <footer>, and <nav> are correctly excluded or included based on their context. The tests confirm that:
- Top-level
<header>elements are skipped in main content mode <header>elements inside<article>are NOT skipped in main content mode<header>elements inside<main>are NOT skipped in main content mode- Deeply nested
<header>elements inside<article>are NOT skipped in main content mode - The same logic applies to
<footer>and<nav>elements - All structural elements are included when the page range is set to
'all'
These tests ensure that the isInsideContentContainer() helper function correctly identifies content context and that article titles and content-specific headers are properly translated while site-wide navigation remains excluded.
Ruby Tag and Inline Display Tests:
Unit tests verify the enhanced inline display detection logic:
- Ruby elements with display value "ruby" are correctly identified as inline
- Elements with display value "block ruby" are not treated as inline
- RT (ruby text) and RP (ruby parenthesis) tags are properly excluded during traversal and text extraction
- Ruby annotations do not split paragraphs during translation
Integration tests confirm that forced-inline translation cases work correctly and that empty block elements are filtered out to prevent paragraph splitting in complex nested structures.
Translation Error Tests:
Integration tests in src/utils/host/__tests__/translate.integration.test.tsx and src/entrypoints/selection.content/selection-toolbar/__tests__/request-rerun.test.tsx verify error handling behavior in both translation modes:
- Translation errors are displayed as inline alerts (not toast notifications) within the selection toolbar
- In bilingual mode, when translation fails, the original text is kept and inline error alert is displayed
- In translation-only mode, when translation fails, the original text is kept and inline error alert is displayed
- Precheck errors (such as provider unavailable or provider disabled) are shown immediately as inline alerts without attempting API calls
- Error alerts are automatically cleared when users retry the translation using the "Regenerate" button
- After a successful retry, the error alert is removed and the translated content is displayed
- The alert component uses the
role="alert"attribute for accessibility and proper testing - Custom action errors follow the same inline alert pattern for consistency across all selection toolbar features
Overlay Selection Filtering Tests:
Integration tests in src/entrypoints/selection.content/selection-toolbar/__tests__/request-rerun.test.tsx and src/entrypoints/selection.content/selection-toolbar/__tests__/selection-toolbar.test.tsx verify overlay selection filtering behavior:
- When users select text inside the translation popover and then rerun the translation, the original page selection and context are preserved
- Selection boundaries (anchor node, focus node, range containers) inside overlay root elements are correctly detected and filtered
- Text selections within elements marked with
data-rf-selection-overlay-rootdo not trigger the toolbar to show - Text nodes inside overlay root elements are correctly identified as overlay selections and do not trigger toolbar state changes
- The toolbar does not show, hide, or update inappropriately when selections are made inside overlay UI elements
- Rerunning toolbar actions after overlay interactions uses the original page paragraphs rather than overlay content
- Shadow DOM boundaries are correctly handled when detecting overlay selections
Float Layout Tests:
Integration tests verify the float layout detection and handling:
- In bilingual mode, block translations are marked with
data-read-frog-float-wrap="true"when the translated content would drop below an active floated sibling - Block translations that stay beside floated siblings do not receive the float-wrap attribute
- The CSS test suite confirms that
.read-frog-translated-block-content[data-read-frog-float-wrap="true"]appliesdisplay: block !important;to maintain layout flow around floated elements
User Experience Improvements#
A translation mode selector is now available in the popup UI, allowing users to easily switch between bilingual and translation-only modes. The selector uses localized labels and provides tooltips for additional guidance. If a user selects a mode that is not supported by the current provider, the system automatically switches to a compatible provider, ensuring seamless operation and reducing the likelihood of errors. This smart fallback mechanism improves reliability and user confidence in the translation feature.
These enhancements make translation mode selection more intuitive and accessible, giving users direct control over how translations are displayed and ensuring that their preferences are respected regardless of provider limitations.