Formatting & Rich Text
This area covers 27 features. Watch the walkthrough, then use the reference below — each feature links to the exact moment it appears (▶).
Who this is for: Translator (contributor). Each feature below lists the role/permission it requires.
Features
Bold text formatting ▶ 00:00
As a translator, I want to apply bold formatting to text in the target cell, so that I can match or emphasize specific words like the source.
How it works. User selects text in the translation cell → BubbleMenu appears with a Bold button → clicking the button toggles bold on/off via Cmd+B or the UI button. The button shows active state (bg-accent class) when bold is active. Bold is rendered as <strong> or <b> HTML tags and preserved in getHTML() output. StarterKit's Bold mark is enabled.
Key files
src/components/TranslatedEditor.tsx:701-712src/components/TranslatedEditor.tsx:233-241
Italic text formatting ▶ 00:00
As a translator, I want to apply italic formatting to text in the target cell, so that I can match the source formatting or add emphasis.
How it works. User selects text in the translation cell → BubbleMenu appears with an Italic button → clicking toggles italic on/off via Cmd+I or UI button. Button shows active state (bg-accent class) when italic is active. Italic is rendered as <em> or <i> tags, preserved in getHTML().
Key files
src/components/TranslatedEditor.tsx:714-726src/components/TranslatedEditor.tsx:233-241
Underline text formatting ▶ 00:00
As a translator, I want to apply underline formatting to text, so that I can preserve underline styling from the source or add emphasis.
How it works. User selects text in the translation cell → BubbleMenu appears with Underline button (tooltip: 'Underline (Cmd+U)') → clicking toggles underline on/off via Cmd+U or UI button. Button shows active state (bg-accent class) when underlined. Underline is rendered as <u> tag, preserved in getHTML().
Key files
src/components/TranslatedEditor.tsx:727-739src/components/TranslatedEditor.tsx:233-241
Strikethrough text formatting ▶ 00:00
As a translator, I want to apply strikethrough formatting to text, so that I can mark text as deleted or show corrections.
How it works. User selects text in the translation cell → BubbleMenu appears with Strikethrough button (tooltip: 'Strikethrough') → clicking toggles strikethrough on/off. Button shows active state (bg-accent class) when strikethrough is active. Strikethrough is rendered as <s>, <strike>, or <del> tags, preserved in getHTML().
Key files
src/components/TranslatedEditor.tsx:740-752src/components/TranslatedEditor.tsx:233-241
Inline code formatting ▶ 00:00
As a translator, I want to apply inline code formatting to text, so that I can mark snippets as code or preserve code styling from the source.
How it works. User selects text in the translation cell → BubbleMenu appears with Inline code button (tooltip: 'Inline code') → clicking toggles inline code on/off. Button shows active state (bg-accent class) when code mark is active. Code is rendered as <code> tag, preserved in getHTML().
Key files
src/components/TranslatedEditor.tsx:753-765src/components/TranslatedEditor.tsx:233-241
Bubble menu formatting toolbar ▶ 00:00
As a translator, I want a floating formatting menu to appear when I select text, so that I can quickly apply formatting without a toolbar.
How it works. When user selects text inside a focused contenteditable cell (from !== to), a BubbleMenu floats above the selection with buttons for Bold, Italic, Underline, Strikethrough, and Inline code. Menu appears with placement: 'top'. Menu disappears when selection is cleared or editor loses focus. Each button reflects active mark state with bg-accent class.
Key files
src/components/TranslatedEditor.tsx:695-767src/components/TranslatedEditor.tsx:20-21
Inline formatting preservation on paste ▶ 00:00
As a translator, I want pasted HTML to preserve only safe inline formatting, so that I can safely paste without unwanted styles breaking the layout.
How it works. User pastes HTML into the editor → transformPastedHTML(html) is called → stripToAllowedHtml() preserves only <b>, <strong>, <i>, <em>, <u>, <s>, <strike>, <del>, <code>, <p>, <br> tags and removes all other tags, attributes, and styles. Footnote spans (data-usfm-footnote) are preserved verbatim. Text content inside removed tags is kept.
Key files
src/components/TranslatedEditor.tsx:278-280src/components/TranslatedEditor.tsx:1041-1076src/components/TranslatedEditor.tsx:1048
Formatting loss warning icon ▶ 00:00
As a translator, I want to see when the source has formatting that my translation doesn't preserve, so that I can decide whether to add the formatting or accept the loss.
How it works. When viewing a cell row in EditorTable: if sourceHasFormatting=true AND targetHasFormatting=false AND translated is non-empty, an AlertTriangle/TriangleAlert warning icon appears in the row with title 'Source has inline formatting (bold, italic, etc.) that the target doesn\'t preserve. Formatting will be lost on export.'
Key files
e2e/specs/editor/formatting-loss-warning.smoke.spec.ts:57
USFM footnote atomic node insertion and selection ▶ 00:00
As a translator, I want to insert footnote markers into my translation and have them behave as atomic units, so that I can manage footnotes without accidentally breaking the marker syntax.
How it works. User calls editor.insertFootnoteMarker(marker, anchor) via imperative handle or through AddFootnoteDialog → footnote node is inserted at the anchor position with marker content (raw USFM \f..\f*) → node is non-selectable and non-focusable but atomic (selecting text across it includes the whole marker as one unit). editor.getText() includes the raw marker verbatim. Marker is displayed as a numbered/lettered badge via the footnote-decoration-plugin.
Key files
src/components/TranslatedEditor.tsx:512-543src/lib/richtext/footnote-node.ts:63-136src/components/TranslatedEditor.tsx:243
Footnote marker deletion with confirmation ▶ 00:00
As a translator, I want to delete a footnote marker with confirmation, so that I don't accidentally lose footnote content.
How it works. User presses Backspace or Delete with cursor adjacent to a footnote marker → system shows inline confirmation banner 'Delete footnote [label]?' with 'I'm sure' and 'Cancel' buttons → user clicks 'I'm sure' to confirm OR presses Backspace/Delete a second time → footnote node is deleted from the editor. Pressing another key before the second Backspace/Delete cancels the pending delete.
Key files
src/components/TranslatedEditor.tsx:355-372src/components/TranslatedEditor.tsx:845-868src/components/TranslatedEditor.tsx:672-694
Footnote Shift+Arrow selection across atomic markers ▶ 00:00
As a translator, I want to extend text selection across footnote markers using Shift+Arrow keys, so that I can reliably select text containing footnotes.
How it works. User holds Shift and presses Left or Right arrow while the selection touches or straddles a footnote node → editor dispatches a deterministic TextSelection that extends across the atomic footnote (one character step includes the whole footnote node). Native Safari selection extension is bypassed to avoid corruption (selection balloon/collapse bugs).
Key files
src/components/TranslatedEditor.tsx:373-392src/components/TranslatedEditor.tsx:822-843
Footnote text hover tooltip display ▶ 00:00
As a translator, I want to see footnote text when I hover or focus a footnote marker, so that I can quickly check the footnote without opening a separate panel.
How it works. User hovers over or focuses a rendered footnote marker in the editor → if showFootnoteTooltips=true (default), a tooltip element appears inside the marker DOM showing the footnote text or reference. Tooltip is created as a span with role='tooltip' and class 'usfm-footnote-marker-tooltip'. On mouseout or focusout, tooltip is hidden.
Key files
src/lib/richtext/footnote-node.ts:108-122src/components/TranslatedEditor.tsx:174src/components/TranslatedEditor.tsx:290-322
Footnote marker numbering with offset ▶ 00:00
As a translator working on a file with multiple chapters, I want footnote markers to number correctly across cell boundaries, so that chapter-level footnote ordering is preserved.
How it works. Parent component passes footnoteNumberOffset (number of footnotes in earlier cells in the chapter) → each footnote marker in the editor displays label = numberOffset + ordinal + 1 (for auto-numbered footnotes with caller='+' or '-'). Explicit caller characters (a, b, etc.) override the numbering.
Key files
src/components/TranslatedEditor.tsx:129-130src/lib/richtext/footnote-decoration-plugin.ts:61-91src/lib/richtext/footnote-node.ts:27-33
Footnote insertion anchor detection ▶ 00:00
As a translator, I want to insert a footnote at the current cursor position or at a selected word, so that I can quickly add footnotes without precise positioning.
How it works. When adding a footnote via AddFootnoteDialog, getFootnoteInsertionAnchor() returns an anchor object with position, from, to, plainPosition, source ('selection'|'word'|'cursor'|'end'), and previewText. If text is selected, source='selection'. If cursor is on a word, source='word' and the word is highlighted as the anchor. If on whitespace or end of doc, source='cursor' or 'end'. Preview includes previewBefore and previewAfter (text before/after insertion point).
Key files
src/components/TranslatedEditor.tsx:476-511src/components/TranslatedEditor.tsx:57-66src/components/TranslatedEditor.tsx:950-987
Footnote addition dialog with marker style selection ▶ 00:00
As a translator, I want to choose whether footnotes use auto-numbering (1, 2, 3) or lettering (a, b, c), so that I can create multiple independent footnote sequences.
How it works. AddFootnoteDialog renders two marker style options: 'Numbering' (caller='+') and 'Lettering' (caller='a'). User clicks a radio-button-like tile to select active style. Live preview shows how the selected footnote marker will appear in context (before and after text). When user clicks 'Add footnote', the marker style (numbered or lettered) is passed to onAdd callback, along with caller, ref, text, and startNewSequence.
Key files
src/components/footnotes/AddFootnoteDialog.tsx:55-172src/components/footnotes/AddFootnoteDialog.tsx:14-31src/components/footnotes/AddFootnoteDialog.tsx:175-192
Footnote text editor with inline rich text formatting ▶ 00:00
As a translator, I want to format footnote text with bold, italic, and underline, so that I can preserve or add semantic formatting within footnotes.
How it works. FootnoteTextEditor component renders a textarea with a toolbar above it containing Bold, Italic, and Underline buttons. Clicking a button inserts USFM formatting markers (\bd text \bd*, \it text \it*, \ul text \ul*) around selected text or fallback placeholder. The textarea preserves these markers and renderFootnoteRichText() parses them on display, applying CSS classes (font-semibold, italic, underline underline-offset-2).
Key files
src/components/footnotes/FootnoteTextEditor.tsx:21-92src/components/footnotes/FootnoteTextEditor.tsx:116-152
Inline rules violation decoration (linting) ▶ 00:00
As a translator, I want to see rule violations highlighted inline as I translate, so that I can quickly identify and fix issues.
How it works. When infractions prop is provided to TranslatedEditor, createViolationDecorationExtension() builds decorations for each infraction span targeting the target side. Span is highlighted with an inline decoration (class 'violation-blot violation-blot-major' or 'violation-blot-minor'; 'violation-blot-waived' if waived). Clicking a violation calls onRuleClick(ruleId, element). Waived rules show 'violation-blot-waived' class. Terminology rules show 'violation-blot-term' class.
Key files
src/lib/richtext/violation-decoration-plugin.ts:10-48src/components/TranslatedEditor.tsx:250src/components/TranslatedEditor.tsx:784-787src/components/TranslatedEditor.tsx:196-200
Terminology chip inline rendering ▶ 00:00
As a translator, I want to see managed terminology highlighted inline with a status indicator, so that I can quickly identify where managed terms are used.
How it works. When terminologyConcepts prop is provided, createTerminologyChipExtension() scans the editor text for case-insensitive, word-boundary matches of each concept's sourceTerm (with inflectional wildcard support via buildTermRegex). Each match gets an inline decoration (position:relative host span) and a widget decoration (position:absolute chip at end of match). Chip shows status-tinted dot (CSS background/content). Clicking chip calls onTermChipClick(term, chipElement).
Key files
src/lib/richtext/terminology-chip-plugin.ts:54-100src/components/TranslatedEditor.tsx:252-254src/components/TranslatedEditor.tsx:772-780
Audio karaoke word highlighting ▶ 00:00
As a translator with audio, I want the current word being played to be highlighted, so that I can follow along with the audio and sync my translation.
How it works. When audioTimings prop is provided and audioCurrentTime updates, createKaraokeExtension() computes the active word index and decorates the matching word span with class 'karaoke-active' inline decoration. Alt+click on a word calls onSeekToTime(timing.t0) to jump audio to that word's start time.
Key files
src/lib/richtext/karaoke-plugin.ts:76-121src/components/TranslatedEditor.tsx:251src/components/TranslatedEditor.tsx:100-101src/components/TranslatedEditor.tsx:590-599
Footnote selection overlay for Safari compatibility ▶ 00:00
As a translator using Safari, I want text selection to render correctly around footnote markers, so that I don't see misaligned or broken selection highlights.
How it works. When the editor doc contains footnote nodes AND the user has a non-empty text selection, footnoteSelectionPluginKey renders an inline decoration overlay (class 'usfm-text-sel') painting the selection range via PM's position-based decoration. Native ::selection is hidden via CSS (class 'usfm-fn-editor' tags the editor root) so the custom overlay is visible. Overlay is hidden in footnote-free cells.
Key files
src/lib/richtext/footnote-decoration-plugin.ts:54-159src/components/TranslatedEditor.tsx:246-249
Text direction (LTR/RTL) toggle ▶ 00:00
As a translator working with RTL languages, I want to toggle text direction for source and target columns, so that I can read and edit in the correct directionality.
How it works. ViewSettingsMenu renders two separate text direction toggles: one for source (ltr/rtl), one for target (ltr/rtl). Toggling calls onSourceTextDirectionChange() or onTargetTextDirectionChange() with new value. EditorTable applies dir={sourceTextDirection} and dir={targetTextDirection} to column divs. When RTL is detected on file open and not dismissed, an auto-popover hint nudges the user ('Detected right-to-left for...') with Adjust and Dismiss buttons.
Key files
src/components/ViewSettingsMenu.tsx:41-251src/components/ViewSettingsMenu.tsx:19-20, 31-32src/components/ViewSettingsMenu.tsx:211-226src/components/ViewSettingsMenu.tsx:63-133
Double-click word selection (contextual) ▶ 00:00
As a translator, I want to double-click a word to select it (without selecting footnote syntax), so that I can quickly select words for formatting or footnote anchoring.
How it works. User double-clicks on a word in the editor → handleDoubleClick(view, pos, event) is called → selectVisibleWord() identifies the word boundary (using plain-text space/word-char logic) and expands to include the visible word, skipping hidden footnote syntax boundaries. Selection is extended via setSelection(TextSelection.create(doc, from, to)). Word is defined via isWordChar() (\p{L}\p{N}''-) and space/punctuation boundaries.
Key files
src/components/TranslatedEditor.tsx:281-289src/components/TranslatedEditor.tsx:870-918src/components/TranslatedEditor.tsx:947-949
Cell navigation via keyboard (Up/Down/Tab) ▶ 00:00
As a translator, I want to navigate between translation cells using Tab and arrow keys, so that I can move through the document without using the mouse.
How it works. When focused inside a translation cell: Tab moves to next cell (down), Shift+Tab to previous (up). Up arrow moves to previous cell only if caret is at first visual line; Down arrow moves to next only if at last line. Escape commits pending edits (via blur) and signals parent to return focus to grid-row wrapper. Parent resolves direction → target cell and focuses it.
Key files
src/components/TranslatedEditor.tsx:339-410src/components/TranslatedEditor.tsx:115
Accessibility: ARIA labels and screen reader support for formatting ▶ 00:00
As a translator using a screen reader, I want clear labels for all formatting buttons and editor state, so that I can understand and use formatting features.
How it works. Each formatting button in BubbleMenu has aria-label ('Bold', 'Italic', 'Underline', 'Strikethrough', 'Inline code'). Editor root has role='textbox', aria-multiline='true', and optional aria-label (e.g., 'GEN 1:1 — validated'). Footnote markers have role='note' and descriptive aria-label ('Footnote [ref]: [text]'). Term chips have aria-label and title attributes ('Managed term: [term]').
Key files
src/components/TranslatedEditor.tsx:262-265src/components/TranslatedEditor.tsx:701-765src/lib/richtext/footnote-node.ts:102-119src/lib/richtext/terminology-chip-plugin.ts:86-91
Read-only editor mode (collaborative lock display) ▶ 00:00
As a translator in a collaborative session, I want to see when another user is editing my cell and not be able to edit it, so that I don't create conflicting changes.
How it works. When heldByLabel prop is set (e.g., 'Alice'), editor becomes read-only (setEditable(false)). An amber banner appears at top-right: 'Alice is editing' (role='status', aria-live='polite'). User cannot type, apply formatting, or insert footnotes. BubbleMenu doesn't appear. All edit callbacks are no-ops. When heldByLabel clears, editor becomes editable again.
Key files
src/components/TranslatedEditor.tsx:91-92src/components/TranslatedEditor.tsx:216src/components/TranslatedEditor.tsx:574-576src/components/TranslatedEditor.tsx:652-659
Remote change reconciliation banner and discard-reload action ▶ 00:00
As a translator, I want to be notified when my cell changes remotely while I'm editing, and choose to discard my edits or keep them, so that I can handle conflicts gracefully.
How it works. When remoteChangedDuringEdit prop is true (parent received event.applied while user holds focus), an amber banner appears: 'This cell changed elsewhere while you were editing.' with a 'Discard and reload' button. Editor stays editable. Clicking button calls onDiscardLocal() (parent reloads cell content and resets state). If banner is not dismissed and user keeps typing, the banner stays visible until onDiscardLocal is called or editor blurs.
Key files
src/components/TranslatedEditor.tsx:107-108src/components/TranslatedEditor.tsx:660-671
Idle-based commit debouncing ▶ 00:17
As a translator, I want my edits to be saved automatically after I stop typing, so that I don't lose work and the system stays in sync.
How it works. User types in editor → onUpdate fires on every keystroke → idleTimer is reset to COMMIT_IDLE_MS (1200ms) → when no keystroke fires for 1200ms, onCommit(snapshot) is called with { value: text, valueHtml: html }. On blur, any pending idle commit is flushed immediately. On unmount, pending commit is flushed so navigate-away doesn't drop work.
Key files
src/components/TranslatedEditor.tsx:48src/components/TranslatedEditor.tsx:413-425src/components/TranslatedEditor.tsx:429-442src/components/TranslatedEditor.tsx:627-640