Internationalization
Explainer supports multiple languages across all three applications — docs, blog, and website — each with an approach tailored to its needs.
Shared behavior
Regardless of the application, the locale is persisted via a cookie (locale) scoped to the root domain. This means a language switch on one subdomain (e.g. blog.example.com) is automatically picked up by the others (e.g. docs.example.com).
Detection order (on first visit):
localecookie (set when the user explicitly switches)- Browser language (
navigator.language) - Default locale (
en)
Docs
The docs app uses locale-based URL routing. The locale appears as the first segment in the URL:
/{locale}/{project}/{path}
| URL | Locale | Page |
|---|---|---|
/en/explainer/getting-started | English | Getting Started |
/fr/explainer/getting-started | French | Getting Started |
Adding a locale
Create the locale directory
Inside your project’s version directory, create a new directory with the locale code:
mkdir -p apps/docs/src/content/docs/explainer/default/fr Add translated content
Create MDX files that mirror the English structure:
---
title: Premiers pas
description: Configurez Explainer v2 et commencez à créer votre site de documentation.
order: 1
---
# Premiers pas
Bienvenue dans la documentation d'Explainer v2.Verify
Navigate to /fr/explainer/getting-started. The locale switcher in the navbar allows users to switch between available languages.
File structure
explainer/
default/
en/ # English (primary)
getting-started.mdx
guides/
installation.mdx
fr/ # French
getting-started.mdx
guides/
installation.mdx
Fallback behavior
There is no automatic fallback to English. Each locale must have its own content files. This ensures translations are intentional and complete.
Blog
The blog app uses locale-based URL routing combined with a translation dictionary for UI strings.
URL structure
/{locale} # Article listing
/{locale}/{slug} # Article page
/{locale}/rss.xml # RSS feed
Content files live in locale-prefixed directories under src/content/posts/:
src/content/posts/
en/
introducing-explainer-v2.mdx
fr/
introducing-explainer-v2.mdx
UI translations
All static UI strings (navigation labels, headings, placeholders, aria-labels) are stored in a translation dictionary at src/i18n/ui.ts and accessed via a useTranslations() helper:
import { ui, defaultLang, type UiKey } from './ui'
export function useTranslations(locale: string) {
return function t(key: UiKey): string {
return ui[locale as keyof typeof ui]?.[key] ?? ui[defaultLang][key]
}
}In .astro files, the locale comes from the URL parameter:
---
const { locale } = Astro.props
const t = useTranslations(locale)
---
<h1>{t('index.heading')}</h1>
In .tsx components, the locale is passed as a prop:
export function TagFilter({ tags, locale = 'en' }: TagFilterProps) {
const t = useTranslations(locale)
return <input placeholder={t('tagFilter.placeholder')} />
}
Redirect
The root / detects the user’s preferred locale (cookie → browser → default) and redirects to /{locale}.
Website
The website is a single-page landing page — there is no URL-based routing for locales. Instead, it uses a client-side translation swap approach.
How it works
- The page is statically generated with English content as the default.
- Each translatable element carries a
data-tattribute referencing a translation key:
<h1 data-t="hero.title">Explain your ideas</h1>
- An inline script detects the locale (cookie → browser → default) and replaces text content with the matching translation:
document.querySelectorAll('[data-t]').forEach(function (el) {
const key = el.getAttribute('data-t')
if (!el.dataset.tDefault) el.dataset.tDefault = el.textContent
if (translations[key]) el.textContent = translations[key]
})
Translation dictionary
Translations live in src/i18n/ui.ts, identical in structure to the blog:
export const ui = {
en: {
'hero.title': 'Explain your ideas',
'cta.action': 'Read the docs',
// ...
},
fr: {
'hero.title': 'Expliquez vos idées',
'cta.action': 'Lire la documentation',
// ...
},
}The French translations are embedded in the page at build time via Astro’s define:vars and passed to the inline script.
Locale switcher
The navbar’s LocaleSwitcher sets the locale cookie and reloads the page (via onLocaleChange) instead of navigating to a different URL. On reload, the script picks up the new cookie and applies the correct translations.
Locale switcher
The LocaleSwitcher component in the navbar is shared across all apps. It:
- Displays the current locale with a dropdown to switch
- Sets a
localecookie on the root domain (e.g..example.com) so the choice persists across subdomains - Supports two modes:
- URL navigation (docs, blog) — follows
switchUrlslinks to locale-prefixed routes - In-place reload (website) — uses the
onLocaleChangecallback to reload the page after setting the cookie
- URL navigation (docs, blog) — follows