Localizing CSS content with html[lang]
| The problem | |
| Simple example | |
| Real case: lesson buttons | |
| Complex cases |
The problem
CSS can render text that does not exist in the HTML: the content property of ::before and ::after pseudo-elements. Translation tools never see these strings, so a hardcoded label stays in one language on every page. On this site the previous/next lesson buttons render their captions purely from CSS, and every English article shows Russian captions because of it.
Simple example
A styled quote block with a hardcoded caption:
.quote::before {
content: "Цитата:";
display: block;
font-weight: bold;
}
Every page shows "Цитата:", whatever its language. The fix is a language-scoped override. Pages already declare their language in the root element, so the selector costs nothing:
<html lang="en">
html[lang="en"] .quote::before {
content: "Quote:";
}
The base rule keeps working as the fallback, and the override wins only inside English documents because an attribute selector is more specific. No HTML changes, no JavaScript, no per-page edits.
Real case: lesson buttons
The lesson navigation renders its two captions from CSS only:
.previous::before {
content: "Предыдущая статья";
}
.next::before {
content: "Следующая статья";
}
Rewriting these two strings to English would fix the English pages and break every Russian one at the same time. The scoped version fixes only what it should:
html[lang="en"] .previous::before {
content: "Previous article";
}
html[lang="en"] .next::before {
content: "Next article";
}
Russian pages keep the original captions untouched. The rule set lives next to the base rules in to_build/_next_previous_buttons.css, and the minified copies go to every served stylesheet that carries these selectors, exactly like any other CSS port on this site.
Complex cases
Several languages. Stack one override block per language; the unscoped base rule stays the fallback for anything else:
html[lang="de"] .previous::before {
content: "Vorheriger Artikel";
}
html[lang="de"] .next::before {
content: "Nächster Artikel";
}
Sections instead of pages. When one document mixes languages, scope by the closest ancestor instead of the root:
[lang="en"] .quote::before {
content: "Quote:";
}
The related :lang(en) pseudo-class also matches, with one difference worth knowing: :lang() follows the document language inheritance (it matches xml:lang too), while [lang="en"] matches the literal attribute only. For pages that always set lang on <html>, both work; the attribute form is explicit and easier to grep for.
Language switchers. A switcher only has to assign document.documentElement.lang — every scoped caption flips instantly with no string tables in JavaScript.
Article author: Andrei Olegovich