Skip to content

Building Bilingual RTL Web Applications

2 min readi18nrtlcss

Shipping a website in English and a right-to-left language like Persian is not a matter of flipping a dir attribute at the last minute. It is a series of small, deliberate decisions that compound across the entire codebase.

The core principle: logical properties

The single most important rule is to never write a physical direction property. Instead of margin-left, write margin-inline-start. The browser resolves inline-start to the left in an LTR context and to the right in an RTL context, so one stylesheet serves both languages.

A concrete example

The same rule positions a card correctly whether the surrounding page is English or Persian:

styles/box.csscss
.box {
  margin-inline-start: 1rem;
  padding-inline: 1.5rem;
  border-inline-start: 1px solid var(--border);
}

Physical properties would require a parallel RTL override for every component. Logical properties remove the entire class of mirror-image bugs.

Content lives on the filesystem

A bilingual post is a folder, not a database row:

  • content/blog/slug/en.mdx is the English translation
  • content/blog/slug/fa.mdx is the Persian translation
  • the absence of fa.mdx means "no Persian version exists"

This convention keeps the filesystem as the single source of truth. Routing, the sitemap, and the language toggle all ask one function which languages actually exist — never a frontmatter field.

An hreflang pointing at a URL that returns 404 is worse than no hreflang at all. The type-safe content pipeline explains how validation keeps that from happening.

Direction is a property of content, not of the design system. Get it right at the source and every consumer inherits it.

For a deeper look at the tooling, see the Shiki documentation and the MDN guide to logical properties.