Astro i18n: Build an Arabic and English Website with RTL
Build an Astro site with English default routes, Arabic localized content, RTL layout, translated collections, canonical URLs, hreflang, and a sitemap.
Model language as content data, pair translations with a stable key, generate English and Arabic routes at build time, and make lang, dir, canonical, and hreflang derive from the same route source.
A bilingual Astro site needs more than translated navigation. Routing, content identity, document direction, metadata, alternate URLs, feeds, and missing translations must follow one consistent model.
This guide uses English as the default language without an /en/ prefix and Arabic under /ar/. That produces /blog/example/ and /ar/blog/example/, which is a clean fit for an English-first site.
Configure Astro i18n routing
Set the site origin, supported locales, default locale, and trailing-slash policy:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://example.com',
trailingSlash: 'always',
output: 'static',
i18n: {
locales: ['en', 'ar'],
defaultLocale: 'en',
routing: {
prefixDefaultLocale: false,
},
},
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: {
en: 'en-US',
ar: 'ar',
},
},
}),
],
});
With prefixDefaultLocale: false, Astro’s documented URL strategy keeps the default locale unprefixed. Decide this before launch; changing it later requires redirects for every indexed URL.
Keep UI translations typed
A small dictionary works well for navigation and repeated labels:
export type Locale = 'en' | 'ar';
export const ui = {
en: {
dir: 'ltr',
nav: { home: 'Home', blog: 'Articles' },
routes: { home: '/', blog: '/blog/' },
},
ar: {
dir: 'rtl',
nav: { home: 'الرئيسية', blog: 'المقالات' },
routes: { home: '/ar/', blog: '/ar/blog/' },
},
} as const;
Do not mix visible strings, route paths, and direction checks across many components. Derive them from one locale value so an Arabic page cannot accidentally render English metadata or dir="ltr".
Model translated articles explicitly
Use Astro content collections for article metadata and validation:
const articles = defineCollection({
loader: glob({
base: './src/content/articles',
pattern: '**/*.{md,mdx}',
}),
schema: z.object({
title: z.string(),
description: z.string(),
locale: z.enum(['en', 'ar']),
routeSlug: z.string(),
translationKey: z.string(),
publishedAt: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
Store content under language directories:
src/content/articles/
├── en/astro-i18n-arabic-english-rtl.md
└── ar/astro-i18n-arabic-english-rtl.md
translationKey pairs meaning; routeSlug controls the URL. Keeping the same English slug in both languages avoids encoded Arabic paths, but the two fields should remain separate so routing can evolve without losing translation identity.
Arabic content should be localized, not mechanically mirrored. Keep commands and code in their original syntax while explaining decisions naturally in Arabic.
Generate static article routes
For English:
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection(
'articles',
({ data }) => data.locale === 'en' && !data.draft,
);
return articles.map((article) => ({
params: { slug: article.data.routeSlug },
props: { article },
}));
}
---
Place that route at src/pages/blog/[slug].astro. Repeat the filter for Arabic at src/pages/ar/blog/[slug].astro.
Static generation keeps article HTML available without client JavaScript and makes missing route collisions fail during the build when you validate them.
Set lang and dir on the document
The layout must set both attributes:
---
const { locale = 'en' } = Astro.props;
const dir = locale === 'ar' ? 'rtl' : 'ltr';
---
<html lang={locale} dir={dir}>
<body>
<slot />
</body>
</html>
Use CSS logical properties so components adapt without separate mirrored styles:
.article-card {
padding-inline: 1.25rem;
border-inline-start: 3px solid var(--accent);
}
.article-content pre,
.article-content code {
direction: ltr;
text-align: left;
}
Test mixed Arabic and English text, numbers, punctuation, inline code, tables, and long commands. Direction is a content behavior, not just text alignment.
Generate canonical and hreflang together
Every page needs a self-canonical plus language alternates:
---
const canonical = new URL(Astro.url.pathname, 'https://example.com');
const alternatePath =
locale === 'en'
? `/ar${Astro.url.pathname}`
: Astro.url.pathname.replace(/^\/ar(?=\/)/, '') || '/';
const alternate = new URL(alternatePath, 'https://example.com');
---
<link rel="canonical" href={canonical} />
<link rel="alternate" hreflang={locale} href={canonical} />
<link
rel="alternate"
hreflang={locale === 'en' ? 'ar' : 'en'}
href={alternate}
/>
<link
rel="alternate"
hreflang="x-default"
href={locale === 'en' ? canonical : alternate}
/>
Only output an alternate when the paired article exists. Find it by translationKey, not by assuming every slug has a translation. A false hreflang pointing to a 404 is worse than omitting it.
The canonical for an Arabic page must remain Arabic; hreflang expresses equivalence without collapsing languages into one indexable URL.
Localize structured data and navigation
Set inLanguage in BlogPosting or Article JSON-LD, use the localized title and description, and point mainEntityOfPage at the self-canonical.
The language switcher should link directly to the paired page, not to the other homepage. When no translation exists, either omit the control or clearly send the reader to the localized archive.
Generate separate RSS feeds for each language and ensure the sitemap contains both routes. The sitemap is a discovery mechanism; it does not replace visible links or hreflang.
Validate before deployment
Automate checks for:
- Unique
(locale, routeSlug)values. - Exactly one page per
translationKeyand locale. - Self-canonical URLs using the configured origin.
- Alternates that return
200. - Correct
<html lang>anddir. - No draft entries in routes, RSS, or sitemap.
- Internal links that stay in the current language where a translation exists.
Then review mobile layouts with real Arabic text. A page can be technically RTL and still have awkward icon order, code overflow, or unreadable mixed-direction metadata.
The Astro content architecture guide covers scalable collections, while the Astro performance budget keeps the multilingual layer lightweight.
Common mistakes
Avoid deriving Arabic URLs with string replacement in many components, canonicalizing Arabic to English, switching languages to the homepage, loading a client-side translation framework for static content, and forcing code blocks into RTL.
One route model should drive page generation, language switching, canonical, hreflang, RSS, and sitemap. That consistency is the real i18n feature.
Frequently asked questions
Can Arabic and English articles use the same slug?
Yes. The locale prefix keeps their URLs distinct, while a shared English slug avoids encoded paths. Pair the content with a separate translation key rather than relying on the slug alone.
Does a static Astro site need a client-side i18n library?
Not for static translated pages. Build-time routes, content collections, and a small UI dictionary usually provide localized HTML with less JavaScript.
Official references
Have a question about this guide or an idea for a technical collaboration? Contact Bakry through the Dev Hub.
End of field note.