Translation
Reviewing User Sessions in Another Language
How on-demand translation lets support and product teams watch a recorded session in their own language, keeping labels short and product names intact.
Published · Updated · 6 min read
A product with users in twelve countries usually has a support team in one or two. When a ticket arrives from a user in Ankara and the agent sits in Lisbon, a session replay shows the agent exactly what happened — in Turkish. They can see the user clicked something three times; they cannot read what it said. This post is about removing that gap: watching any user's session in the reviewer's own language, and what it takes to do that well.
Who runs into this
- Support teams covering more markets than they have native speakers for. The customer support page walks through the ticket-to-replay workflow.
- Product managers watching sessions from a market they are expanding into, where the localised interface is the one users actually see.
- Localization teams checking their own work in context — truncated buttons, untranslated strings, layouts that break in German. See localization teams.
- Engineers reproducing a bug reported in a language they do not read.
The alternatives are all bad: find a colleague who speaks the language, paste screenshots into a translation app, or guess. None scale, and all of them lose the thing replay is for — seeing the session as a whole.
Why this needs structured recordings
You cannot translate a video. A screenshot-based recorder has thrown the words away by the time anything could read them; OCR on a frame is slow, error-prone and destroys layout. DOM recording keeps the text as text, and that is the property session translation builds on. The same is true on mobile, where AnyReplay records the React element tree rather than the screen for exactly this reason.
How the pipeline works
When a reviewer asks for a session in, say, English, AnyReplay does the following.
- Extract. Walk the recorded event stream — the full snapshot and every mutation — and collect the strings a reviewer would want translated: text nodes, and attributes that hold prose (
placeholder,title,alt,aria-label,aria-description,label). Skip anything insidescript,style,code,pre,svg,canvasand similar tags, which are markup or code rather than language. - Classify. Record the role each string plays: heading, button, label, link, input, attribute or plain text. "Kaydet" on a button should become "Save", not "Save the changes"; a heading is a noun phrase; a button is an imperative.
- Deduplicate. Hash each string (whitespace-normalised, case kept) and look it up in the organisation's translation memory. Anything already translated is reused.
- Translate what is new. Send the remaining strings, with their roles, to a language model in indexed batches. Each result carries the index of its input back, so if the model drops one item of forty, exactly one string stays untranslated instead of every translation after it shifting onto the wrong string.
- Substitute. Rewrite the event stream with the translations, then hand it to the player. Because substitution happens on the events rather than the rebuilt DOM, seeking, fast-forwarding and every later mutation are already translated. There is no per-frame work at replay time.
What the model is told
Interface strings are not prose, and a translator treated as a prose translator produces a replay that looks wrong. The instructions AnyReplay sends encode what was learned from that:
- Keep labels as short as the original. A button must still fit its button.
- Preserve placeholders such as
{name},%s,{{count}}and:idexactly. - Leave product names, brand names, URLs, email addresses and code identifiers unchanged.
- If a string is already in the target language, return it unchanged.
- Never add explanation, quotes or commentary.
- Return one element for every input, carrying its index. Never renumber, merge or omit.
A string the model returns nothing for simply stays in its original language. That is deliberate: an untranslated label is what the reviewer was looking at anyway, and it is not worth failing a whole session over one.
Glossaries: pinning the words that matter
Every product has terms that must not be translated, or must be translated one specific way. Your product name. A feature called "Spaces" that is not about astronomy. A plan tier. An organisation can define glossary terms — a term and its required translation, or a term with an empty translation meaning "leave this exactly as it is". Up to 200 of them are put into the model's instructions, and they take precedence over its own judgement.
Translation memory and human corrections
Sessions of the same application repeat the same interface: the same navigation, the same buttons, the same form labels. The translation memory is keyed on the organisation, not the session, so after a handful of recordings almost every string is already there and translating a new session costs close to nothing. That is the reason the feature is affordable enough to leave on.
It also means corrections stick. When someone on your team fixes a translation, the fix is marked reviewed and is never sent to a model again — every future session that contains that string uses the human version. Over time the memory becomes your product's own bilingual dictionary, built from real sessions.
Privacy: what the model sees
Translation sends interface strings, not recordings. Field values are not among the strings it extracts, and masking happens in the browser anyway, so a masked value — every field value under the default, anything marked ar-mask, or the password and payment floor that applies in every configuration — is asterisks before the stream exists. The one thing to watch is a rich-text editor: its content is ordinary page text rather than a field value, so a comment box someone typed into is extracted like any other text unless maskAllTyping: true is set. Otherwise what reaches the model is your product's copy: the same headings and labels a public marketing screenshot would show.
The dashboard is multilingual too
Translation covers the recorded session. The dashboard around it is available in English, Turkish, German, Spanish and French, with every locale carrying exactly the same keys — a test fails the build otherwise. A reviewer in Madrid reads Spanish chrome around a session translated into Spanish, which is the whole experience, not half of it.
Trying it
Install the SDK on a page in one language, record a session, open it, and pick another language. The first time costs a translation; the second time, for any session of the same app, mostly does not. The Free plan — one project and 500 sessions a month — is enough to see the memory fill up. Plans are on the pricing page, and the rrweb explained post covers the event-stream model the whole thing depends on.
Frequently asked questions
Does translation change the original recording?
No. The original event stream is kept; a translated copy is produced per target language and reused on every later view of that session in that language.
What happens if a string cannot be translated?
It stays in its original language. Results are indexed so one dropped string never shifts the others, and a missing translation is not treated as a failure.
Can I stop my product name being translated?
Yes. Add it as a glossary term with an empty translation, which instructs the model to leave it exactly as it is. Glossary entries take precedence over the model.
Is user data sent to the translation model?
Interface strings are sent, and field values are not among the strings extracted. Masked text is already asterisks before translation begins and anything marked ar-block is absent. The exception to watch is a rich-text editor, whose content is page text: set maskAllTyping: true if people type personal data into one.