Software localization

CSS Localization

CSS is surprisingly important when it comes to site localization. Let's take a look at some ways of providing a delightful localized UX.
Software localization blog category featured image | Phrase

In the ever-evolving landscape of web development, CSS has grown by leaps and bounds, transforming the way we design, build, and interact with web content. Yet one of its most powerful capabilities remains largely underutilized: CSS localization.

This guide aims to remedy this, covering the ins and outs of CSS localization. We start with essential HTML attributes for language-specific styling and directional text — including right-to-left layouts, and vertical text presentations for East Asian scripts. Logical properties are the perfect complement for these vertical and right-to-left layouts, and we cover those too.

We hope to offer designers and developers some hidden gems of CSS that can make your pages more inclusive, and available to a global audience without too much effort.

💡 Internationalization (i18n) and localization (l10n) allow us to make our apps available in different languages and to different regions, often for more profit. If you’re new to i18n and l10n, check out our guide to internationalization.

🔗 You can interact and play with many of the concepts we cover here via our live demo on StackBlitz. You can also get all the demo code from GitHub.

The lang attribute

Setting the lang attribute is the most essential thing we can do when localizing our web pages. lang is a global attribute, meaning that it’s available on all HTML tags. Since we normally use a single language on a given page, the most common use of lang is setting it on the <html> document element.

<!DOCTYPE html>
<!-- 👇 Set the language of the
        document to English. -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>An English Page</title>
</head>
<body>
  <h1>A English Page</h1>
  <!-- ... -->
</body>
</html>Code language: HTML, XML (xml)

🗒️ When setting the lang attribute on HTML elements, we typically use an IETF BCP 47 language tag. BCP 47 tags begin with a language code — en for English, fr for French, es for Spanish — and have optional region specifiers (more on regions a bit later).

🔗 Check out the list of language tags on Wikipedia.

In the above example, we set the entire document to English by using the language tag, en.

<html lang="en">Code language: HTML, XML (xml)

Setting the lang attribute makes using proper pronunciation easier for screen readers and other assistive technologies. It also lets search engines know which language our page is in, which helps with SEO (Search Engine Optimization).

🔗 Read more about the lang attribute on MDN.

We can set lang on almost any HTML element. For example, we might have a Korean paragraph in the middle of our otherwise English page.

<html lang="en">
  <!-- ... -->
  <body>
    <p>An English paragraph.</p>
    <p>Another English paragraph.</p>
    <p lang="ko">한국어 문단입니다.</p>
  </body>
</html>Code language: HTML, XML (xml)

We can target these elements using the CSS :lang() pseudo-class. :lang(en), for example, will target any element that has lang=”en”.

/* 
 * Global selector — selects any element 
 * with lang="en" 
 */
:lang(en) {
  color: deepskyblue;
}

/* 
 * Element selector — selects all paragraphs
 * with lang="ko"
 */
p:lang(ko) {
  color: deeppink;
  border: 1px solid deeppink;
}Code language: CSS (css)

🗒️ The lang attribute doesn’t need to be set directly on the element. If the element inherits lang="en" from an ancestor in the DOM, :lang(en) will target it.

Our global :lang() selector targets all elements on the page but is overridden by the p:lang() attribute.
Our global :lang() selector targets all elements on the page but is overridden by the p:lang() attribute.

Locale specificity

We mentioned earlier that lang values are typically BCP 47 tags, which can be a language tag like en or ko. BCP 47 tags can be further specified by adding a region code according to the ISO Alpha-2 standard (like BH for Bahrain, CN for China, US for the United States). So a qualified locale identifier might appear as en-US for American English or zh-CN for Chinese as used in China.

🔗 For country codes, the ISO’s search tool is a good resource.

When targeting elements with :lang(), general locales will match more specific locales. For example, :lang(en) will match elements with lang="en" and elements with lang="en-US".

<p lang="en">Some English text.</p>
<p lang="en-US">Some American English text.</p>
<p lang="en-GB">Some British English text.</p>
<p lang="en-CA">Some Canadian English text.</p>Code language: HTML, XML (xml)
p:lang(en) {
  color: darkslategray;
  border-bottom: 1px solid #ccc;
}

p:lang(en-CA) {
  /* Canadian flag red */
  color: #d80621;
}Code language: CSS (css)
The :lang(en) selector targets specific English locales, such as en-US, en-GB, and en-CA.
The :lang(en) selector targets specific English locales, such as en-US, en-GB, and en-CA.

Direction (right-to-left layouts)

Languages like Arabic, Hebrew, and Urdu are read right-to-left (rtl). The dir HTML attribute handles these languages wonderfully. dir can have a value of ltr (left-to-right), rtl (right-to-left), or auto (based on content).

Again, since a web page is often in one language, and hence one direction, the most common use case for dir is setting on the html document element.

<html lang="ar" dir="rtl">
<!-- ... --> Code language: HTML, XML (xml)

All modern browsers will flow the page in the direction specified by dir.

Firefox reflowing the page according to the html element’s dir attribute.
Firefox reflowing the page according to the html element’s dir attribute.

🔗 Try the above demo yourself on StackBlitz. You can also get all demo code from GitHub.

Just like lang, dir is a global attribute and can be applied to any HTML element, not just the document root.

<p dir="ltr">
  But I must explain to you how all this mistaken idea of
  denouncing pleasure and praising pain was born.
</p>

/* Should be equivalent to dir="ltr" */
<p dir="auto">
  But I must explain to you how all this mistaken idea of
  denouncing pleasure and praising pain was born.
</p>

<p dir="rtl">
  لكن لا بد أن أوضح لك أن كل هذه الأفكار المغلوطة حول
  استنكار النشوة وتمجيد الألم نشأت بالفعل
</p>

/* Should be equivalent to dir="rtl" */
<p dir="auto">
  لكن لا بد أن أوضح لك أن كل هذه الأفكار المغلوطة حول
  استنكار النشوة وتمجيد الألم نشأت بالفعل
</p>Code language: HTML, XML (xml)
When dir=”auto”, the browser will use the first character with a strong directionality and use that for the direction of the element.
When dir=”auto”, the browser will use the first character with a strong directionality and use that for the direction of the element.

In CSS, we can target rtl and ltr elements with normal attribute selectors.

[dir="ltr"] {
  padding-left: 1rem;
  border-left: 2px solid deepskyblue;
}

[dir="rtl"] {
  padding-right: 1rem;
  border-right: 2px solid deepskyblue;
}Code language: CSS (css)
Note that with dir=”ltr” and dir=”rtl” we only target those explicitly set directions, but not dir=”auto”
Note that with dir=”ltr” and dir=”rtl” we only target those explicitly set directions, but not dir=”auto”

The dir="auto" case is interesting: The browser will look at the first character with strong directionality in the element and use that to set the element’s direction. It’s best to be explicit and use ltr | rtl. However, dir="auto" can be helpful when presenting content we don’t control, like user-generated text.

Notice that the CSS attribute selectors, [dir="ltr"] and [dir="rtl"], don’t target dir="auto". We can use the :dir() pseudo-class for this.

:dir(ltr) {
  padding-left: 1rem;
  border-left: 2px solid deepskyblue;
}

:dir(rtl) {
  padding-right: 1rem;
  border-right: 2px solid deepskyblue;
}Code language: CSS (css)
The :dir() targets dir=”auto”, since it looks at the browser-resolved direction for the element, not the value of the dir attribute.
The :dir() targets dir=”auto”, since it looks at the browser-resolved direction for the element, not the value of the dir attribute.

🗒️ At the time of writing the :dir() pseudo-class selector was supported by approximately 80% of globally used browsers. In contrast, attribute selectors like [dir="ltr"] enjoy nearly 100% coverage.

✋ In addition to the HTML dir attribute, a CSS direction property also exists. However, it’s best practice to prefer the HTML dir attribute where possible.

🤿 If you’re looking to override text direction inside runs of text, check out the <bdo> HTML element. For isolating elements inside text runs, look at the <bdi> element.

🔗 Check out the MDN docs for the dir attribute for further reading.

Writing mode (vertical layouts)

A lot of East Asian writing — Chinese, Japanese, Korean, Vietnamese — can be written horizontally or vertically. The traditional orientation for these East Asian scripts was top-to-bottom, then right-to-left.

🤿 The Wikipedia entry, Horizontal and vertical writing in East Asian scripts, is an interesting read here.

Today, horizontal, left-to-right orientation for East Asian languages is more common, especially in print and digital. However, modern browsers do support vertical layouts via the writing-mode CSS property.

writing-mode can take one of three values:

.wm-htb {
  /* Horizontal, then top-to-bottom (default). */
  writing-mode: horizontal-tb;
}

.wm-vrl {
  /* Vertical, then right-to-left. */
  writing-mode: vertical-rl;
}

.wm-vlr {
  /* Vertical, then left-to-right. */
  writing-mode: vertical-lr;
}Code language: CSS (css)
Three boxes with English text demonstrating the three different values of the writing-mode property.
Three boxes with English text demonstrating the three different values of the writing-mode property.

Of course, vertical modes are meant for East Asian scripts. The following is an example in Japanese. Note that, unlike English, Japanese characters keep a fixed rotation whether they’re displayed horizontally or vertically.

Three boxes with Japanese text demonstrating the three different values of the writing-mode property.
Three boxes with Japanese text demonstrating the three different values of the writing-mode property.

🗒️ With nearly 100% global browser support you can freely use the writing-mode property.

🔗 Check out the MDN writing-mode entry for more info.

🔗 Styling vertical Chinese, Japanese, Korean and Mongolian text from the W3C is a good concise guide.

🔗 Play with different writing modes live on the Layout page of our StackBlitz demo. If you want to run the code on your machine, clone or download our GitHub repo.

Logical properties

CSS layout properties that control element size, position, and spacing have traditionally been physical, like top or padding-right. The direction of physical properties is effectively based on the screen coordinate system.

A diagram of physical CSS properties showing them as based on the screen ie. right is always to the element's right, regardless of text orientation.

Physical properties don’t respect dir and writing-mode: right is always the physical right side of an element. To accommodate ltr and rtl layouts, we would have to target these directions and write separate rules for each:

[dir="ltr"] .element {
  margin-left: 50px;
}

[dir="rtl"] .element {
  margin-right: 50px;
}Code language: CSS (css)

Diagram showing the element with a left offset when the container direction is ltr and a right offset when the container direction is rtl. The margin is set explicitly for each direction.

More recently, however, the CSS standard has adopted logical properties. Logical properties adapt to both the dir and writing-mode of an element. Here’s the same example with a logical margin:

.element {
  margin-inline-start: 50px;
}Code language: CSS (css)

Diagram showing the element with a left offset when the container direction is ltr and a right offset when the container direction is rtl. The margin is set once with margin-inline-start.

Notice how we only needed to set the margin-inline-start property once: The browser responds to the container’s direction, interpreting inline-start as left when dir="ltr" and right when dir="rtl".

🗒️ At the time of writing, logical properties enjoy 95% global browser support, so there’s little reason not to prefer them over their physical counterparts.

Logical properties automatically adapt to dir and writing-mode, and we’ll see this in action as we explore the most important logical properties in the following sections.

🔗 CSS logical properties and values on MDN is an excellent resource.

A note on inline and block

When working with logical properties, we’ll be using inline and block instead of top, down, left, and right.

  • Block dimension is the direction that text stacks, like lines in a paragraph. It’s up and down (vertical) when you’re reading text written from left to right, like in English. But if you’re reading text that goes from top to bottom, like in traditional Japanese, the block dimension switches to side-to-side (horizontal).
  • Inline dimension is the direction text flows within a line, side-to-side (horizontal) in languages like English, but up and down (vertical) in vertically written text, such as traditional Chinese or Japanese.

It’s much easier to understand this with examples, so let’s dive into the properties themselves.

Sizing

Physical CSS sizing is controlled with width, height, and their min- and max- equivalents. The logical size counterparts are inline-size, block-size, with their min- and max- versions.

.physical-sizing {
  width: 210px;
  height: 80px;
}

.logical-sizing {
  inline-size: 210px;
  block-size: 80px;
}Code language: CSS (css)

Image illustrating CSS logical properties with different writing modes. In horizontal writing mode, width maps to inline-size, and height to block-size. In vertical modes, these mappings rotate, demonstrating the adaptability of logical properties to writing mode.

Sizing isn’t affected by dir, of course. It is affected by writing-mode, as seen in the above diagram. Note how the box set with physical width and height keeps its dimensions regardless of the writing-mode. In contrast, inline-size and box-size cause the box dimensions to adapt to vertical orientations.

It can be helpful to have a mapping between physical properties and their logical equivalents, so here is the simple mapping for sizing properties:

Physical property Logical property (writing-mode: horizontal-tb)
width inline-size
height block-size

🗒️ We omit min- and max- property variants for brevity, but you get the idea.

🔗 Read more in the Properties for sizing section of the logical properties MDN page.

Positioning

Physical positioning is achieved with inset, top, right, bottom, and left. For logical positioning, we have inset-block, inset-inline, with -start and -end variants.

.physical-positioning {
  position: absolute;
  top: 20px;
  right: 20px;
}

.logical-positioning {
  position: absolute;
  inset-block-start: 20px;
  inset-inline-end: 20px;
}Code language: CSS (css)

Image showing CSS physical and logical properties for different writing modes and directions. Physical properties 'top' and 'right' change depending on text direction, whereas logical properties 'inset-block-start' and 'inset-inline-end' adapt to the direction and writing mode, ensuring consistent placement across languages and writing modes.

Logical positioning adapts to both dir and writing-mode. Here are the physical-to-logical positioning mappings:

Physical property Logical property (writing-mode: horizontal-tb; dir: ltr)
top inset-block-start
bottom inset-block-end
left inset-inline-start
right inset-inline-end

🔗 Read more in the Properties for positioning section of the logical properties MDN page.

Spacing

Our trusty margin and padding values are how we space elements physically. Their logical equivalents are margin-block and margin-inline.

.physical-spacing {
  margin-top: 40px;
  padding-right: 60px;
}

.logical-spacing {
  margin-block-start: 40px;
  padding-inline-end: 60px;
}Code language: CSS (css)

Image comparing CSS physical and logical spacing properties under various writing modes and directions. It highlights how physical properties like margin-top and padding-right are direction-dependent, while logical properties like margin-block-start and padding-inline-end adapt to the writing orientation.

As you might have guessed, logical margins and paddings adapt to both dir and writing-mode. Here are the physical-to-logical spacing mappings:

Physical property Logical property
(writing-mode: horizontal-tb;
dir: ltr)
margin-top margin-block-start
margin-bottom margin-block-end
margin-left margin-inline-start
margin-right margin-inline-end
padding-top padding-block-start
padding-bottom padding-block-end
padding-left padding-inline-start
padding-right padding-inline-end

🔗 Read more in the Properties for margins and Properties for paddings section of the logical properties MDN page.

Other logical properties

There are logical properties for borders, border radius, and more. To keep things brief, we’ll refer you to the full list on MDN.

✋ The logical properties we covered above, along with those for border and border-radius, have excellent modern browser support. For others, always check browser compatibility before using them in production.

🔗 We cover logical border properties and more in our companion demo. Play with it live on StackBlitz or get the code from GitHub.

Fonts

Let’s switch gears and look at localization and typography. When it comes to typefaces, want to choose fonts that support all the characters in our language’s scripts e.g. Latin, Arabic, Korean, etc. Some common options are system fonts, multilingual fonts, and Google Noto.

System fonts

A font will often cover a few languages but leave out many. System fonts, however, tend to have wide language coverage and are pre-installed on operating systems. So they’re generally safe to use for multilingual sites. The Windows default system font is Segoe UI, macOS and iOS use San Francisco, and Android uses Roboto.

Here’s a common system font stack:

html {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}Code language: CSS (css)
The Windows 11 system font displaying characters from different languages.
The Windows 11 system font displaying characters from different languages.
The macOS system font displaying characters from different languages.
The macOS system font displaying characters from different languages.

Multilingual fonts

While safe, system fonts don’t always represent our brand. We might want to control the fonts on our websites to ensure a consistent experience for our visitors no matter their operating system. Multilingual fonts can fit the bill here, depending on the languages you need covering. Here are some multilingual fonts (keep in mind these are all paid):

Helvetica World

Languages covered: Arabic, Bulgarian, Bosnian, Catalan, Czech, Danish, German, Greek, English, Spanish, Estonian, Finnish, French, Irish, Hebrew, Croatian, Hungarian, Icelandic, Italian, Lithuanian, Latvian, Maltese, Dutch, Norwegian, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Albanian, Serbian, Swedish, Turkish.

🔗 Get Helvetica World from MyFonts.

Neue Helvetica World

Languages covered: Arabic, Bulgarian, Bosnian, Catalan, Czech, Danish, German, Greek, English, Spanish, Estonian, Finnish, French, Irish, Hebrew, Croatian, Hungarian, Icelandic, Italian, Lithuanian, Latvian, Maltese, Dutch, Norwegian, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Albanian, Serbian, Swedish, Turkish.

🔗 Get Neue Helvetica World from MyFonts.

Greta Sans

Languages covered: Arabic, Armenian, Cyrillic, Devanagari, Georgian, Greek, Hebrew, Korean, Latin.

🔗 Get Greta Sans from Typothque.

Google Noto

If a multilingual font meets your needs it could be a good fit for your brand. If, however, you want near-universal language coverage, check out Google Noto. Not only is the Noto font collection free, but it also has outstanding language coverage. Noto isn’t just one font: It’s a system of fonts that were designed to look good and work well together while covering over 1,000 languages!

After assembling our desired Noto collection from Google Fonts, we can target languages with the CSS :lang() pseudo-class to set them.

:lang(en) {
  font-family: "Noto Serif", serif;
}

:lang(ar) {
  font-family: "Noto Naskh Arabic", "Noto Serif", serif;
}

:lang(jp) {
  font-family: "Noto Serif JP", sans-serif;
}Code language: CSS (css)
Different languages displayed in complimentary Noto fonts.
Different languages displayed in complimentary Noto fonts.

🔗 Get Noto from Google Fonts.

Font fallback

If a font doesn’t support a particular script, the browser will fall back to a font that does. We can get ahead of this by specifying a fallback font in our CSS. The following uses the Impact font without fallback.

.font-no-fallback {
  font-family: Impact;
}Code language: CSS (css)

Note how non-Latin glyphs fall back to the system font.

A font stack without fallback will cause non-supported glyphs to fall back to the current system font.
A font stack without fallback will cause non-supported glyphs to fall back to the current system font.

Here’s a Noto stack with fallback:

.font-fallback {
  font-family: "Noto Naskh Arabic", "Noto Serif Devanagari",
    "Noto Serif SC", "Noto Serif KR", "Noto Serif JP",
    "Noto Serif", serif;
}Code language: CSS (css)

Since we’ve controlled the fallback, we can be confident that our supported language glyphs will be covered.

A Noto font stack with fallback ensures that we control which font is used for a given glyph.
A Noto font stack with fallback ensures that we control which font is used for a given glyph.

Of course, this solution means that we need to load all of our fonts on the page, which can be a performance issue. It’s probably wiser to load only the fonts our main page language uses, and use system font fallback for secondary languages.

🗒️ An alternative is to mix and match, creating our own collection, where different fonts cover different supported subsets of our languages. We cover this briefly in our demo app — play with the demo on StackBlitz or download the code from GitHub and run it on our machine.

Pronunciation marks

Some languages add pronunciation text or marks to their scripts, like Ruby or diacritics. Modern CSS allows us to target some of these marks and control their appearance.

Ruby

Ruby annotations are small texts placed above or to the right of the base text to give pronunciation guides, and explanations, or to show the use of characters in East Asian languages.

Ruby has special HTML tags:

<p lang="jp">
  <ruby>
    東京 <rp>(</rp> <rt>とうきょう</rt><rp>)</rp>
  </ruby>
</p>Code language: HTML, XML (xml)

<ruby> contains text that has ruby annotations. The annotations themselves go inside <rt> tags. <rp> tags are used for ruby parentheses shown to browsers that don’t support Ruby.

🔗 Read about the <ruby>, <rt>, and <rp> tags on MDN.

✋ While over 98% of globally used browsers support basic ruby, complex ruby and writing-mode with ruby are only partially supported by modern browsers. Check out Can I use for the latest info on ruby support.

We can control ruby using the ruby-position CSS property.

.ruby-over {
  ruby-position: over;
}

.ruby-under {
  ruby-position: under;
}Code language: CSS (css)
Japanese text shown with ruby annotations above and below.
Japanese text shown with ruby annotations above and below.

🔗 More info about the ruby-position property can be found on MDN.

Text emphasis

East Asian languages often use text emphasis marks to highlight text instead of bold or italics. We can control text emphasis style, color, and position with CSS.

<p lang="jp">
  明日は<em class="text-emphasis-dot">晴れ</em>です。
</p>

<p lang="jp">
  明日は<em
    class="text-emphasis-sesame text-emphasis-under-left"
  >晴れ</em
  >です。
</p>Code language: HTML, XML (xml)
em:lang(jp) {
  font-style: normal;
  text-emphasis-color: deepskyblue;
  text-emphasis-position: over right;
}

.text-emphasis-dot:lang(jp) {
  text-emphasis-style: dot;
}

.text-emphasis-sesame:lang(jp) {
  text-emphasis-style: sesame;
}

.text-emphasis-under-left:lang(jp) {
  text-emphasis-position: under left;
}Code language: CSS (css)

Two examples of Japanese writing with text emphasis, one with marks above the characters and one below.

🗒️ Japanese, Korean, Mongolian, and Chinese each have a preferred position for text emphasis and ruby, depending on whether they’re horizontal or vertical. MDN has a handy table that shows these preferences.

🔗 Play with live demo app on StackBlitz; get all the demo app code from GitHub. We cover setting quotes per language in the demo among other things.

The joy of CSS

That about does it for this guide. We hope our exploration into CSS localization has been as enlightening and enjoyable, and that it showed you a few things about the untapped potential of CSS localization. Here’s to creating vibrant, global web experiences with the ever-growing awesomeness of CSS. Happy coding!