i18n
Before applying any of this: check the project's astro.config.mjs for defaultLocale/locales/prefixDefaultLocale, and check whether localePath/switchLocalePath/useTranslations are actually imported anywhere in src/pages. This skill documents the astro-v7-template default — projects frequently override the locale direction (e.g. a German-market site making de the unprefixed default), and some projects abandon the shared-dictionary pattern entirely in favor of hand-duplicated per-locale page files with hardcoded content. Don't assume either pattern without verifying against the actual project first.
Architecture (astro-v7-template default)
Two layers:
- Astro i18n routing — URL prefixing (
/de/contact). - App-level translations —
t('key')per locale.
Typical locales
enas default without prefix —/,/contact.deprefixed —/de/,/de/contact.
(Some projects flip this — de as the unprefixed default for a German-only market, en prefixed. Same mechanism, just swap which locale has the empty prefix in astro.config.mjs and langPrefixes/routeMap.)
Astro config
// astro.config.mjs
export default defineConfig({
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
routing: {
prefixDefaultLocale: false, // EN at root, DE at /de/
},
},
});
Utilities
// Build a locale-prefixed path
localePath('/contact', 'en'); // → '/contact'
localePath('/contact', 'de'); // → '/de/contact'
// Detect locale from URL
getLocaleFromPath('/de/contact'); // → 'de'
getLocaleFromPath('/contact'); // → 'en'
// Swap locale in the current path
switchLocalePath('/de/contact', 'en'); // → '/contact'
switchLocalePath('/contact', 'de'); // → '/de/contact'
// Translation helper
const t = useTranslations(dictionary);
t('nav.home'); // → 'Home'
Translation files
src/i18n/
en.ts # English
de.ts # German
index.ts # t() helper wiring
// src/i18n/en.ts
export default {
'nav.home': 'Home',
'nav.contact': 'Contact',
'home.title': 'My site',
'contact.title': 'Contact',
'contact.send': 'Send',
'404.title': '404 — Page not found',
} as const;
// src/i18n/index.ts
import type { Locale, Translations } from '@/lib/i18n';
import { useTranslations } from '@/lib/i18n';
import de from './de.js';
import en from './en.js';
const translations: Record<Locale, Translations> = { en, de };
export function t(locale: Locale) {
return useTranslations(translations[locale]);
}
Page pattern
Each locale gets its own page file:
src/pages/
index.astro # EN homepage
contact.astro # EN contact
de/
index.astro # DE homepage
contact.astro # DE contact
---
import { localePath } from '@/lib/i18n';
import { t } from '@/i18n';
const locale = 'de';
const tr = t(locale);
const links = [
{ href: localePath('/', locale), label: tr('nav.home') },
{ href: localePath('/contact', locale), label: tr('nav.contact') },
];
---
<BaseLayout title={tr('home.title')} lang={locale}>
<Navbar slot="header" links={links}>
<a href="/contact">EN</a>
</Navbar>
<h1>{tr('home.title')}</h1>
</BaseLayout>
Adding a new page (both locales)
- Create
src/pages/<name>.astrowithconst locale = 'en'. - Create
src/pages/de/<name>.astrowithconst locale = 'de'. - Add translation keys to both
en.tsandde.ts. - Add nav links via
localePath('/<name>', locale). - Add OG image entries for both locales in the OG generator.
Adding a new locale
- Add the locale to
astro.config.mjs(locales: ['en', 'de', 'fr']). - Extend your
Localetype (['en', 'de', 'fr'] as const). - Create
src/i18n/fr.tsand register it insrc/i18n/index.ts. - Create
src/pages/fr/and duplicate the localized pages withconst locale = 'fr'. - Update the sitemap config:
locales: { en: 'en', de: 'de', fr: 'fr' }. - Add OG image entries for the new locale.
Sitemap and hreflang
sitemap({
i18n: {
defaultLocale: 'en',
locales: { en: 'en', de: 'de' },
},
}),
The integration emits <xhtml:link rel="alternate" hreflang="…"> for each localized URL automatically.
Language switcher pattern
<!-- On EN pages: link to DE version -->
<a href="/de/contact" class="text-xs font-medium no-underline">DE</a>
<!-- On DE pages: link to EN version -->
<a href="/contact" class="text-xs font-medium no-underline">EN</a>
Build the href with switchLocalePath(Astro.url.pathname, otherLocale) if you want a switcher that works on any page.