Why this matters
Fewer than one in five people on Earth speak English, and only about one in twenty speak it natively. A large share of your readers will meet your page through a machine translator: the built-in translation in Chrome, Edge, Safari, and Firefox, or a service like Google Translate.
Here is the deal the platform offers. You write honest HTML; the browser translates it into more than 100 languages for free, forever, including languages you have never heard of. You build nothing. You just avoid breaking the machinery. That is the world part of the World Wide Web, and you get it for the price of not fighting it.
Pages fail translation in two ways: text the translator cannot see, and layout or logic that collapses when the text changes underneath it. Both are self-inflicted.
How in-browser translation works on your page
Understanding the mechanism explains every failure that follows. When a reader translates your page, the browser:
- Detects the source language, preferring your
langattribute over statistical guessing. - Walks the DOM collecting text nodes. Not pixels, not canvas drawings, not CSS-generated content, not strings sitting inside your JavaScript — text nodes.
- Replaces each text node with translated text, typically wrapping it in extra inline elements.
- Watches for DOM mutations and re-translates nodes that change.
Three consequences fall straight out of this. Anything that is not a text node is invisible to the translator. Any layout that assumed the original string length or direction is now wrong. And any script that rewrites the DOM wholesale, or reads the DOM expecting English, is now fighting the reader.
The failure catalog
1. No language declaration
Omit lang and everything downstream guesses: translation source detection, screen reader pronunciation, hyphenation, quotation marks, font selection. Worse than omitting it is lying — a template that ships lang="en" on Spanish content actively misroutes all of those systems.
<html lang="en">
<p>The German word <span lang="de">Weltanschauung</span>
has no English equivalent.</p> 2. Text that isn't text
Words baked into raster images, drawn on a <canvas>, or generated by CSS are pixels or style, not content. The translator never sees them. Neither does a screen reader, a search engine, or find-in-page — text-as-pixels fails four audiences at once.
/* Untranslatable, unfindable, unspeakable */
button::after { content: "Add to cart"; } <!-- Text stays text; style is style -->
<section class="hero">
<h2>Spring Sale</h2>
</section> 3. Sentences assembled from fragments
English word order is not universal. A sentence split across elements — <span>You have</span> <span>3</span> <span>item(s)</span> — forces every language into English word order and hands the translator three meaningless fragments instead of one sentence. "item(s)" adds insult: many languages have three to six plural forms, not two.
<p>You have <data id="cart-count" value="3">3</data>
items in your cart.</p> Keep each translatable unit a complete phrase. Put variable values in their own element (<data>) inside the phrase so scripts can update the value without rewriting the sentence.
4. Text expansion versus fixed boxes
Translated text is rarely the same length. German and Finnish routinely run 30–40% longer than English; short UI strings can double. "Checkout" becomes "Zur Kasse gehen". A button sized to fit the English amputates everything else.
/* Fragile: sized to one language */
nav a { width: 92px; white-space: nowrap; overflow: hidden; }
/* Resilient: minimum size, room to grow */
nav a { min-inline-size: 92px; padding-inline: 0.75rem; } 5. Direction: physical CSS and directional prose
Arabic, Hebrew, Persian, and Urdu run right to left. Two things break. First, physical CSS (margin-left, text-align: left) pins the layout to left-to-right assumptions; logical properties (margin-inline-start, text-align: start, inset-inline-end) flip with the text for free. Second, prose that encodes direction — "use the menu on the left" — translates into a lie when the layout mirrors. Name things by what they are, not where they sit.
6. Dates, numbers, units, and money
"04/03/26" is April 3rd or March 4th depending on where you grew up. "20°" is freezing in Fahrenheit and beach weather in Celsius. Machine translation translates words, not conventions — your ambiguity survives translation intact. Give machines the unambiguous value and humans a clear rendering:
<time datetime="2026-04-03">April 3, 2026</time>
<data value="USD 29.99">$29.99</data>
<p>Rated to −7 °C (20 °F)</p> 7. JavaScript that fights the translator
Two patterns turn heavy scripting into an i18n hazard:
- Wholesale re-rendering. A timer that rebuilds a section with
innerHTMLevery few seconds throws away the translator's work each tick. The reader watches your page flicker between their language and yours indefinitely. Render once; when data changes, patch only the nodes that changed. - Label sniffing. Logic like
if (btn.textContent === 'Add to cart')breaks the moment the label is translated: the comparison fails silently and the button goes dead. Visible text is presentation. Keep state indata-*attributes and branch on those.
<button id="toggle-cart" data-state="out">Add to cart</button> toggle.addEventListener('click', () => {
const entering = toggle.dataset.state === 'out';
toggle.dataset.state = entering ? 'in' : 'out';
toggle.textContent = labels[toggle.dataset.state];
}); This is the general lesson hiding inside the i18n lesson: the more of your page that exists only as client-side string manipulation, the more consumers you break — translators, screen readers, search engines, reader modes. Server-rendered or static HTML with targeted enhancement survives all of them.
8. Unprotected tokens
The inverse failure: translators are enthusiastic and will happily "translate" your brand, product codes, commands, and email addresses. Scope translate="no" to exactly those tokens — and nothing more. (This page uses it on its own code samples for the same reason: translate this article into German and the prose changes while every sample stays verbatim.)
<h1 translate="no">Terra Outfitters</h1>
<code translate="no">npm install</code> The checklist
| Check | Failure if skipped |
|---|---|
Accurate lang on <html>, inline lang for foreign phrases | Wrong detection, wrong pronunciation, wrong typography |
| All words exist as DOM text (no canvas, image, or CSS-content text) | Untranslatable, unsearchable, silent to screen readers |
Complete phrases per element; values in <data>; no "item(s)" | Garbled word order, broken plurals |
min-inline-size and wrapping, never fixed-width clipped text | Truncated labels in longer languages |
| Logical properties; no directional words in prose | Broken RTL layout, instructions that lie |
<time datetime>, <data value>, units stated or paired | Ambiguity that survives translation |
Minimal DOM churn; state in data-*, not labels | Flickering reversion; dead controls after translation |
translate="no" scoped to brands, codes, commands | "Translated" product names and commands that no longer work |
The exercise
- Open the broken store. Skim it as an English reader. Seems fine.
- Translate it to German: right-click the page, choose Translate to English, then open the ⋮ menu in the translation bar and pick Choose another language → German. (Or paste the page URL into translate.google.com.) Watch the hero banner ignore you, the nav clip, the cart sentence garble, the deals flicker back to English, and the cart button die.
- Repeat with Arabic. Notice the layout refusing to mirror and "the menu on the top left" becoming fiction.
- Open the fixed store and repeat both translations. Same look; everything works. View source on both pages and diff the sins against the fixes — they carry matching numbers in the comments.
- Read the honest opt-out for the case where you genuinely don't want translation — and what that choice costs.
The choice on display: work with the grain and the platform hands your page to the whole world, or work against it and ship something that only works for people exactly like you — and calls itself finished.