
How to design typography for multilingual websites?
- Sajjad
- Typography
- 11 Oct, 2026
You design typography for multilingual websites by choosing fonts that support every script and language you publish in, marking up each page and passage with the correct lang attribute, and building font stacks and layouts that adapt to each language's needs. Different scripts need different line heights, sizes and fallback fonts, and translated text is often longer or shorter than the original, so components must flex rather than rely on fixed widths. CSS features such as :lang(), unicode-range and logical properties let you tune typography per language without separate stylesheets. The goal is that every language looks deliberately designed, not like a fallback.
A site that looks polished in English can fall apart in other languages: accented capitals get clipped, a missing glyph appears in a different font, German labels overflow their buttons, or Thai text renders in a system font that clashes with the rest of the page. In this article you'll learn how to choose fonts for multiple languages, how to use lang and font stacks properly, how to load fonts efficiently per script, how to handle line height, size and text expansion, and how to test multilingual typography.
Start With the lang Attribute
Every multilingual site depends on correct language markup. Set lang on the html element for the page's main language, and on any element whose language differs:
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Über uns</title>
</head>
<body>
<h1>Über uns</h1>
<p>Wir arbeiten mit Partnern in ganz Europa.</p>
<blockquote lang="fr">
<p>« La qualité avant tout. »</p>
</blockquote>
</body>
</html>
The lang attribute does far more than help search engines:
- Font selection: Browsers use it to choose appropriate system fonts and glyph variants. The same Unicode character can be drawn differently in Japanese, Simplified Chinese and Traditional Chinese, and
langdecides which form appears. - Hyphenation:
hyphens: autoonly works when the browser knows the language. - Case transformation:
text-transformfollows language-specific rules, such as Turkish dotted and dotless i. - Quotation marks: The
qelement uses quotation marks appropriate to the language. - Screen readers: Assistive technology switches pronunciation rules based on
lang. - Line breaking: Some languages have specific rules for where lines may break.
Use standard language tags, such as en-GB, pt-BR, zh-Hans or sr-Latn, so that region and script are explicit where they matter.
Choose Fonts With the Right Coverage
A font can only display the characters it contains. Before choosing a typeface, list every language you publish in, including future ones, and check that the font supports them all.
Check Language Support, Not Just the Alphabet
Latin script alone covers hundreds of languages with very different needs. Vietnamese stacks multiple diacritics on single letters, Polish needs the ogonek (ą, ę), Romanian needs s and t with a comma below (ș, ț), and Icelandic needs þ and ð. A font described as "Latin" may only support Western European languages.
When evaluating a font:
- Check the foundry's language list: Google Fonts lists supported languages for each family, and most commercial foundries publish character sets.
- Test real text: Type sample sentences in each language, including capitals with accents, rather than relying on character tables.
- Inspect the glyphs: You can list a font's character map with
fonttools:
pip install fonttools brotli
python -c "from fontTools.ttLib import TTFont; f = TTFont('MyFont.woff2'); print(len(f.getBestCmap()), 'characters'); print(ord('ș') in f.getBestCmap())"
1043 characters
True
Superfamilies and Noto
For sites covering several scripts, it's much easier to start from a family designed to work across them. Google's Noto project aims to cover every script in Unicode with visually harmonised designs, and families such as Noto Sans, Noto Sans Arabic, Noto Sans Devanagari, Noto Sans Thai and Noto Sans JP share consistent proportions. Other families with broad, multi-script coverage include IBM Plex (with Plex Sans Arabic, Hebrew, Devanagari, Thai and others) and Source Sans 3 with Source Han Sans for Chinese, Japanese and Korean.
Matching families across scripts helps different languages look like they belong to the same brand, even though letterforms can't be identical.
Build Font Stacks Per Script
Rather than a single stack for everything, combine fonts so each script uses a font designed for it. The browser picks fonts character by character, so a stack can include several scripts:
:root {
--font-sans: "Inter", "Noto Sans", "Noto Sans Arabic", "Noto Sans Devanagari",
system-ui, sans-serif;
}
body {
font-family: var(--font-sans);
}
For finer control, use :lang() to set a primary font for each language:
:lang(ar) {
font-family: "Noto Sans Arabic", "Segoe UI", Tahoma, sans-serif;
}
:lang(hi) {
font-family: "Noto Sans Devanagari", "Nirmala UI", sans-serif;
}
:lang(ja) {
font-family: "Noto Sans JP", "Hiragino Sans", "Yu Gothic", "Meiryo", sans-serif;
}
:lang(th) {
font-family: "Noto Sans Thai", "Leelawadee UI", "Thonburi", sans-serif;
}
The :lang() selector matches by language prefix, so :lang(ar) also matches ar-EG and ar-SA. It also inherits correctly, unlike an attribute selector such as [lang="ar"], which only matches the element that carries the attribute.
Avoid Synthesised Styles
If a font has no bold or italic for a script, browsers create fake versions by smearing or slanting the regular weight. Faux bold Arabic or Devanagari can look poor, and many scripts, including Chinese, Japanese and Korean, have no italic tradition at all. Turn off synthesis where it does more harm than good:
:lang(ja),
:lang(zh),
:lang(ko) {
font-synthesis: none;
}
:lang(ja) em,
:lang(zh) em,
:lang(ko) em {
font-style: normal;
font-weight: 700;
}
Use emphasis styles that suit each script instead, such as bold, or the text-emphasis property, which adds emphasis marks commonly used in East Asian typography.
Load Fonts Efficiently for Each Script
Fonts for some scripts are very large. A full Japanese or Chinese font can contain tens of thousands of glyphs and weigh several megabytes. You don't want English visitors downloading it.
Split Files With unicode-range
The unicode-range descriptor tells the browser which characters a font file covers. The browser only downloads a file if the page actually uses a character in its range:
@font-face {
font-family: "Brand Sans";
src: url("/fonts/brand-sans-latin.woff2") format("woff2");
font-weight: 400;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Brand Sans";
src: url("/fonts/brand-sans-latin-ext.woff2") format("woff2");
font-weight: 400;
font-display: swap;
unicode-range: U+0100-024F, U+1E00-1EFF, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Brand Sans";
src: url("/fonts/brand-sans-cyrillic.woff2") format("woff2");
font-weight: 400;
font-display: swap;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
A page in English only fetches the Latin file; a page in Polish fetches both Latin files; a page in Ukrainian fetches the Cyrillic file. Google Fonts uses this technique automatically, and it's the main reason its CSS contains many @font-face blocks for one family. You can create these subsets yourself with pyftsubset, covered in more depth in what font subsetting is and how it reduces file size.
Preload Per Language
If you preload fonts, preload only the files each language actually needs. A Japanese page shouldn't preload the Latin file if most of its text is Japanese, and an English page should never preload a CJK font. Generate preload tags in your templates based on the page's language.
Adjust Size and Line Height Per Script
Scripts have very different proportions. At the same font size, some appear much smaller or need more vertical room:
- Vietnamese: Stacked diacritics on capitals need extra space above the line.
- Thai, Lao and Khmer: Vowel and tone marks above and below letters need taller lines.
- Devanagari and Bengali: The headline (shirorekha) and marks above and below need generous line height.
- Arabic: Often looks smaller than Latin at the same size, with tall ascenders and deep descenders.
- Chinese, Japanese and Korean: Dense characters usually read best with a slightly larger size and looser line height.
body {
font-size: 1.0625rem;
line-height: 1.5;
}
:lang(vi) {
line-height: 1.65;
}
:lang(th),
:lang(hi) {
line-height: 1.75;
}
:lang(ar) {
font-size: 1.125rem;
line-height: 1.7;
}
:lang(ja),
:lang(zh) {
line-height: 1.8;
letter-spacing: 0.02em;
}
Treat these values as starting points and tune them with real content and native readers. Avoid adding letter spacing to Arabic and other connected scripts, because it can break the joins between letters.
Check for Clipping
Fixed heights and overflow: hidden often clip tall diacritics and descenders. Test headings and buttons with text such as "Ỗ Ặ Ậ" in Vietnamese, or Thai text with stacked marks, and use min-height and padding instead of fixed heights.
Design for Text Expansion and Contraction
Translations rarely match the original length. German and Finnish often have long compound words, French and Spanish tend to run longer than English, and Chinese and Japanese are usually much shorter in characters but denser. Short labels can grow proportionally more than long paragraphs.
Build components that cope:
- Let buttons and tabs grow: Use padding and
min-widthinstead of fixed widths. - Allow wrapping: Navigation items and headings should be able to wrap onto a second line.
- Break long words: Use
overflow-wrapand, where supported for the language,hyphens: auto. - Avoid text in images: It can't be translated without recreating the image.
.button {
display: inline-flex;
align-items: center;
min-height: 2.75rem;
padding-inline: 1.25em;
max-width: 100%;
text-align: center;
}
h1,
h2,
.card__title {
overflow-wrap: break-word;
hyphens: auto;
text-wrap: balance;
}
text-wrap: balance evens out line lengths in headings, which helps when translations change where lines break. Browsers without support simply ignore it.
Use Logical Properties for Direction
If you publish in Arabic, Hebrew, Persian or Urdu, text runs right to left. Logical properties such as margin-inline-start and padding-inline-end adapt automatically when the dir attribute changes, so the same CSS works in both directions:
.note {
border-inline-start: 4px solid #2563eb;
padding-inline-start: 1rem;
text-align: start;
}
<html lang="ar" dir="rtl">
There's a lot more to right-to-left layout, including mixed-direction text and mirrored icons, which is covered in how to handle right-to-left typography.
Respect Local Typographic Conventions
Each language has its own conventions for punctuation and spacing. A few common ones:
- Quotation marks: English uses “ ”, German typically uses „ “, and French uses « » with spaces inside.
- Spacing before punctuation: French traditionally puts a narrow space before ; : ! and ?.
- Numbers and dates: Decimal separators, thousands separators and date order differ. Use
Intl.NumberFormatandIntl.DateTimeFormatin JavaScript rather than hard-coding formats.
const amount = 1234567.89;
console.log(new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" }).format(amount));
console.log(new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount));
console.log(new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(amount));
£1,234,567.89
1.234.567,89 €
1 234 567,89 €
The French output uses narrow no-break spaces as thousands separators and a no-break space before the euro sign, which keeps numbers from breaking across lines.
How to Test Multilingual Typography
- Use real translated content: Lorem ipsum and machine-generated placeholders hide length and glyph problems.
- Check for fallback glyphs: In Chrome's developer tools, the Computed tab lists Rendered Fonts for an element. If a language shows an unexpected system font, your web font is missing characters.
- Look for tofu: Empty boxes or question marks mean no available font has the character.
- Test the longest language: Check navigation, buttons, cards and forms in the language with the longest strings.
- Test tall scripts: Look for clipped diacritics in headings, buttons and inputs.
- Check network requests per language: Filter the Network panel by Font and confirm each page downloads only what it needs.
- Ask native readers: They will spot awkward line breaks, wrong punctuation and unsuitable fonts that you can't.
FAQ: Multilingual Typography
Not for every language, but often for every script. One Latin font can cover most European languages if it has extended character support, while Arabic, Devanagari, Thai and CJK text usually need fonts designed for those scripts.
Browsers use it to choose fonts and glyph variants, apply hyphenation, transform case correctly and select quotation marks. Screen readers also use it to pronounce text properly.
It tells the browser which characters a font file contains, so the file is only downloaded when the page uses those characters. This lets you split large fonts by script and keep pages light.
Avoid fixed heights and overflow hidden on text containers, increase line height for languages with stacked marks such as Vietnamese and Thai, and test headings and buttons with real accented capitals.
Google's Noto family aims to cover all of Unicode. IBM Plex, Source Sans 3 with Source Han Sans, and many system fonts also offer broad, harmonised multi-script support.
Generally no. These scripts have no italic tradition, and browsers produce slanted fake italics. Use bold or emphasis marks with the text-emphasis property instead.
Conclusion
Multilingual typography works when every language gets the same care as the original. That starts with correct lang attributes, which drive font selection, hyphenation, case rules and screen reader pronunciation, and with fonts whose character sets genuinely cover each language you publish. Per-script font stacks, :lang() rules and unicode-range subsets let you serve the right font to each reader without making everyone download everything.
From there, tune size and line height for each script, turn off synthesised styles where they look wrong, and design components that stretch for longer translations and adapt to right-to-left text. Test with real content, check rendered fonts and network requests per language, and ask native readers for feedback. The result is a site that feels designed for each audience rather than translated as an afterthought.


