Building Multilingual RTL Websites with Nuxt
How we build English, Persian and Arabic sites from one Nuxt codebase: i18n routing, hreflang, dir, logical CSS, fonts, Jalali dates and the SEO traps to avoid.
The site you are reading runs in English, Persian and Arabic from one Nuxt codebase. Two of those languages read right to left, use different digits, and one of them usually shows dates in a different calendar. Getting that right is less about translation and more about a dozen small engineering decisions, most of which are invisible when they are correct and painful when they are not.
This is how we build multilingual, right-to-left sites with Nuxt, including the SEO details that decide whether Google treats your language versions as one site or as competing duplicates.
Routing: one URL per language
Every page needs a distinct, crawlable URL in every language. The cleanest scheme for most sites is a locale prefix, with the default language at the root:
https://asan.team/servicesin Englishhttps://asan.team/fa/servicesin Persianhttps://asan.team/ar/servicesin Arabic
With @nuxtjs/i18n that is the prefix_except_default strategy:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{ code: 'en', language: 'en-US', file: 'en.json', dir: 'ltr' },
{ code: 'fa', language: 'fa-IR', file: 'fa.json', dir: 'rtl' },
{ code: 'ar', language: 'ar-SA', file: 'ar.json', dir: 'rtl' },
],
defaultLocale: 'en',
strategy: 'prefix_except_default',
baseUrl: 'https://asan.team',
langDir: 'locales/',
},
})
Two settings matter more than they look. baseUrl must be the absolute origin, because canonical and hreflang URLs are useless as relative paths. Declaring dir on each locale means direction comes from configuration rather than from an if (locale === 'fa') check somewhere in a component. That check is exactly how Arabic ends up rendering left to right when someone adds it later.
Direction and language on the html element
The lang and dir attributes belong on <html>, set on the server so the first paint is already right:
// app.vue
const { locale, localeProperties } = useI18n()
const direction = computed(() => localeProperties.value.dir ?? 'ltr')
const i18nHead = useLocaleHead({ addSeoAttributes: true })
useHead({
htmlAttrs: { lang: locale, dir: direction },
link: computed(() => i18nHead.value.link ?? []),
meta: computed(() => i18nHead.value.meta ?? []),
})
useLocaleHead also generates the canonical link, the hreflang alternates and the og:locale tags for every page. That covers most of the SEO wiring in one call.
hreflang: telling Google the pages are translations
Without hreflang, Google sees /services, /fa/services and /ar/services as three separate pages with overlapping topics, and may pick one to show everywhere. With hreflang, each page declares its siblings:
<link rel="alternate" hreflang="en" href="https://asan.team/services">
<link rel="alternate" hreflang="fa" href="https://asan.team/fa/services">
<link rel="alternate" hreflang="ar" href="https://asan.team/ar/services">
<link rel="alternate" hreflang="x-default" href="https://asan.team/services">
The rules that trip teams up:
- Alternates must be reciprocal. Every page in the set lists all the others, including itself.
- Every alternate must return 200. An hreflang pointing at a 404 or a redirect is ignored, and repeated errors make Google trust the rest less.
- Add
x-defaultfor visitors whose language you do not serve. - Repeat the set in the sitemap. We generate the sitemap from the prerendered output, so every URL in it exists and carries its alternates.
Content that exists in only one language needs special handling. Our insights are written separately for each language, not translated, so a Persian article has no English twin. Its page drops the hreflang alternates entirely and keeps only its canonical. The language switcher sends visitors to the insights index in their language rather than to a URL that would 404.
Do not redirect crawlers by browser language
A common mistake is redirecting every visitor to the language their browser prefers. Googlebot usually crawls without an Accept-Language header, from US IP addresses. Aggressive redirects can hide your non-English pages from it entirely, and they frustrate users who deliberately chose a language.
Our rule: detect the language only on the root URL, remember the choice in a cookie, and never redirect a URL that already carries a locale prefix. In @nuxtjs/i18n that is detectBrowserLanguage with redirectOn: 'root' and alwaysRedirect: false.
CSS: think in start and end, not left and right
Right-to-left layout is mostly solved by one habit: use logical properties everywhere.
| Instead of | Use |
|---|---|
margin-left | margin-inline-start |
padding-right | padding-inline-end |
left: 0 | inset-inline-start: 0 |
text-align: left | text-align: start |
border-left | border-inline-start |
Flexbox and grid already follow the document direction. If the codebase uses logical properties from day one, most of the RTL layout comes for free. Retrofitting a finished LTR site is where the real cost is.
A few things logical properties do not cover:
- Directional icons. Arrows and chevrons that mean "next" or "back" must mirror. We mark them with a
flip-rtlclass and applytransform: scaleX(-1)underhtml[dir='rtl']. Icons that are not directional, like a search glass or a check mark, stay as they are. - Transforms and animations.
translateX(4px)moves right in both directions, so hover nudges need an RTL counterpart. - Mixed-direction content. Email addresses, phone numbers, code and Latin product names inside Persian text need
dir="ltr"orunicode-bidi: isolate, or their punctuation ends up on the wrong side.
Typography for Persian and Arabic
- Never letter-space Arabic script. Persian and Arabic letters join. Adding
letter-spacingbreaks the joins and makes text look broken. The same goes fortext-transform: uppercase, which does nothing useful. Wherever our design uses tracked, uppercase labels in English, we reset both forfaandar. - Choose fonts designed for the script. We use Vazirmatn for Persian and Arabic body text and Estedad for Persian display type, both loaded with
font-display: swap. Latin fonts with bolted-on Arabic glyphs rarely read well. - Increase line height. Arabic script has taller ascenders and deeper descenders. Headings that look right at 1.1 line height in English need around 1.4 in Persian.
Numbers and dates
Intl handles most of it with no library:
new Intl.NumberFormat('fa-IR').format(1405) // "۱٬۴۰۵"
new Intl.DateTimeFormat('fa-IR-u-ca-persian', { dateStyle: 'long' })
.format(new Date('2026-09-27')) // "۵ مهر ۱۴۰۵"
new Intl.DateTimeFormat('ar-u-ca-gregory', { dateStyle: 'long' })
.format(new Date('2026-09-27')) // a Gregorian date in Arabic
Persian readers expect the Solar Hijri calendar, which -u-ca-persian provides. Saudi locales default to the Islamic calendar, so set the calendar explicitly when you mean Gregorian. For machine-readable output such as <time datetime> and structured data, always use ISO dates.
Normalize input as well as output. Users type Persian, Arabic and Latin digits interchangeably, and Persian text often contains the Arabic forms of "ی" and "ک". Normalize both before validating a phone number or running a search, or identical queries will miss each other.
Per-language SEO, not translated SEO
Translation is not localization, and it is definitely not SEO. For each language:
- Research keywords in that language. Persian searchers do not phrase queries as translations of English ones. Our Persian service pages target different terms than the English ones, not translated terms.
- Write titles and descriptions per language. Each locale file carries its own
seoblock. - Set
og:localecorrectly. Usefa_IRandar_AR. A missing mapping silently falls back toen_US. - Keep structured data in the page's language. Names, descriptions and FAQ answers in JSON-LD should match the visible text of that locale.
A checklist before launch
langanddiron<html>, rendered on the server.- Canonical plus reciprocal hreflang with
x-defaulton every page. - No hreflang pointing at URLs that 404 or redirect.
- A sitemap generated from real output, with alternates.
- Logical CSS properties throughout, and mirrored directional icons.
- No letter-spacing or uppercase transforms on Arabic-script text.
- Script-appropriate fonts and larger line heights.
- Locale-aware numbers and dates, and normalized input.
- Keyword research, titles and descriptions written per language.
- Each language checked in Search Console after launch.
If you are planning a multilingual or right-to-left product, our frontend development and SEO pages describe how we work, and this site is a live example of the result.
Frequently asked questions
Yes. Search engines index URLs, not languages. If languages share one URL and switch by cookie or browser setting, only one version will ever be indexed. Use a prefix such as /fa/, a subdomain, or a separate domain.
For most sites, subdirectories like /fa/ are simpler and keep all languages under one domain's authority. Separate country domains make sense when you run distinct businesses per market.
Declare dir: 'rtl' on the locale in @nuxtjs/i18n, set the dir attribute on the html element from the active locale, and write CSS with logical properties such as margin-inline-start so layouts mirror automatically.
Yes. hreflang tells search engines which version to show to which audience and stops the versions from competing with each other. Add x-default for everyone else.
Most Persian readers expect the Solar Hijri calendar. In JavaScript, Intl.DateTimeFormat with the fa-IR-u-ca-persian locale produces it without a library. Keep ISO dates in markup and structured data.
More insights
n8n vs Make vs Zapier: Which Automation Platform to Choose
An honest comparison from a team that has shipped on all three: pricing models, hosting, complexity, AI features and when each one is the wrong choice.
September 27, 2026How to Monitor Competitor Prices with Web Scraping
A practical guide to competitor price monitoring: sources, product matching, refresh rates, anti-bot defences, alerts and running costs.
September 27, 2026Google Maps Lead Scraping: What Is Possible and Legal
What data Google Maps has, the Places API versus scraping, what Google's terms and privacy law say, and how to build a lead list responsibly.
