Articles & knowledge

Building a Multilingual Astro Site with Testable Contracts

A practical method that connects routes, language, text direction, content identity, and hreflang to explicit tests instead of treating localization as page duplication.

Translation is not the whole problem

A site can display three languages while its behavior remains ambiguous: a language switch points to an unrelated home page, lang is correct but dir is wrong, or hreflang references a relative or nonexistent URL. The solution is not another layer of component conditions. It is a set of small contracts that can be tested.

This example assumes:

  • Arabic is the default language at the root;
  • English uses /en/ and Turkish uses /tr/;
  • long-form content is not necessarily available in every language;
  • unapproved content must not enter the public build.

Astro supports locales, defaultLocale, and unprefixed default-language routes through prefixDefaultLocale: false. Routing configuration alone does not solve text direction, content parity, or search metadata.

Contract 1: a path resolves to one locale

Centralize locale extraction and path generation. Components should not each invent their own string manipulation.

type Locale = 'ar' | 'en' | 'tr';

const prefixes: Record<Locale, string> = {
  ar: '',
  en: '/en',
  tr: '/tr',
};

export function localeFromPath(pathname: string): Locale {
  const first = pathname.split('/').filter(Boolean)[0];
  return first === 'en' || first === 'tr' ? first : 'ar';
}

export function localizedPath(locale: Locale, basePath: string): string {
  const normalized = basePath === '/' ? '/' : `/${basePath.replace(/^\/+|\/+$/g, '')}/`;
  return `${prefixes[locale]}${normalized}` || '/';
}

At minimum, test:

InputLocaleEnglish counterpart
/ar/en/
/about/ar/en/about/
/tr/projects/tr/en/projects/

When a counterpart does not exist, do not pretend that it does. Hide that locale for the item or lead to a language index with an explicit explanation. A silent redirect to different content misleads people and crawlers.

Contract 2: language does not imply text direction

The root html element should carry both lang and dir:

---
const locale = 'ar';
const direction = locale === 'ar' ? 'rtl' : 'ltr';
---

<html lang={locale} dir={direction}>

W3C guidance places dir="rtl" on html for an overall right-to-left document and warns against using CSS to establish base direction. Use logical CSS properties so layout adapts:

.card {
  padding-inline: 1rem;
  margin-block-end: 1.5rem;
  border-inline-start: 0.25rem solid currentColor;
}

User-generated text of unknown direction can use dir="auto". Do not apply it to the entire page when the document direction is known.

Contract 3: content identity is separate from its title

Titles change and are translated, so they are weak identifiers. Use a stable translationKey, locale, workflow status, and visibility:

locale: 'en'
translationKey: 'multilingual-astro-contracts'
status: 'draft'
visibility: 'private'
title: 'Building a Multilingual Astro Site with Testable Contracts'

Astro Content Collections load content and validate its shape. Add a schema rule that rejects public content unless it is published and carries the required summary and review dates. “Private by default” then becomes executable behavior rather than an editorial reminder.

Use a positive publication filter:

const visible = entries.filter(
  ({ data }) =>
    data.locale === locale &&
    data.status === 'published' &&
    data.visibility === 'public' &&
    (!data.publishedAt || data.publishedAt <= now),
);

Avoid “publish everything except draft.” A future workflow state could become public accidentally.

Contract 4: canonical and hreflang match reality

Every page needs an absolute canonical URL for the current version. When translations exist, each version should reference itself and its real counterparts with fully qualified URLs. Google requires fully qualified hreflang URLs and emphasizes reciprocal links between language versions.

<link rel="canonical" href={new URL(currentPath, siteOrigin)} />
<link rel="alternate" hreflang="ar" href={new URL(arPath, siteOrigin)} />
<link rel="alternate" hreflang="en" href={new URL(enPath, siteOrigin)} />
<link rel="alternate" hreflang="x-default" href={new URL(arPath, siteOrigin)} />

Do not emit a Turkish alternate when that article has no Turkish version. Derive available locales from the actual translation group, not the site's global locale list.

For a preview environment, build canonical URLs and the sitemap against the preview origin and add noindex. Do not advertise absolute URLs on a domain that is not owned; the metadata would describe a different origin than the response.

Contract 5: test the matrix, not one example

An Arabic home-page check proves little about the full language surface. Build a route matrix and apply the same assertions:

1. successful HTTP status;

2. correct html[lang][dir];

3. canonical matches the route;

4. available-language links are reciprocal and do not return 404;

5. no horizontal overflow at 320 CSS px;

6. no selected automated WCAG A/AA violations;

7. local links and same-page fragments exist;

8. draft content stays out of search and the sitemap.

Include a negative test: a draft/private item must not produce a public detail route. Success-path tests alone do not prove that the publication gate is closed.

What this design does not solve

  • It does not establish translation quality; independent human review is still required.
  • It does not require every language to have identical coverage; selective translation is an editorial decision that should be visible.
  • It does not choose a safe fallback automatically. Rewriting Arabic content under an English URL can misstate both lang and SEO intent.
  • It does not replace real screen-reader or Safari testing.
  • It does not justify forced browser-language redirects that prevent visitors from choosing and sharing stable URLs.

Acceptance checklist

  • Document the default locale and path rules.
  • Set document-level lang and dir.
  • Use logical CSS rather than structural left and right rules.
  • Use a stable translationKey and derive available locales from real content.
  • Apply a positive publication filter with private defaults.
  • Emit absolute, reciprocal canonical and hreflang links.
  • Test every route, direction, and negative publication case.
  • Complete human language review before launch.

References

Update record

  • 2026-10-09: Publication approved by the site owner; primary Astro and W3C documentation was rechecked. Publication approval does not establish independent human language review. The checklist is a reader's guide, not a certificate of readiness for any site.
  • 2026-10-04: First draft based on the current platform implementation and tests plus primary Astro, Google, and W3C documentation. Independent technical and language review are still required before publication.

Help improve this content

Was this content useful?
Suggest a correction or edit

Your submission is private, used for review and improvement, and processed through Google Firebase and Firestore. The proposed retention period is 12 months from creation; automated deletion is not enabled in this preview. You may request deletion of contact details through the contact form. Read the Privacy Policy and Terms of Use.