Internationalization
The template ships with English (en) as the default locale and Spanish (es) as a secondary locale. English pages have no URL prefix (/about); Spanish pages are prefixed with /es/ (/es/contacto).
Translation files
Section titled “Translation files”All user-facing strings live in JSON files under src/locales/:
src/locales/├── en/│ ├── common.json ← nav labels, footer, cookie banner, theme toggle│ ├── main.json ← hero, stats, about, testimonials│ └── sections.json ← features, pricing, FAQ, contact, gallery, etc.└── es/ ├── common.json ├── main.json └── sections.jsonThese files are loaded automatically via import.meta.glob in src/i18n/ui.ts.
Key format
Section titled “Key format”Translation keys use a namespace:dot.path format:
t("common:nav.ariaLabel") // → "Main navigation"t("main:hero.headline") // → "Design That<br />Speaks Volumes"t("sections:features.heading") // → "Everything you need"The namespace matches the filename (common, main, sections). The path is the nested JSON key path.
Using translations in components
Section titled “Using translations in components”---import { useTranslations } from "@i18n/utils";
const { lang } = Astro.props; // "en" | "es"const t = useTranslations(lang);
const heading = t("main:hero.headline") as string;const features = t("sections:features.items") as Array<{ title: string; description: string }>;---
<h1 set:html={heading} />{features.map(f => <div>{f.title}</div>)}Arrays and objects can be cast to their expected types — TypeScript will infer unknown by default.
Translated paths
Section titled “Translated paths”Astro’s i18n routing requires route names to match in each locale. A Spanish “contact” page lives at /es/contacto, not /es/contact. The mapping is defined in src/i18n/routes.ts:
export const routes: Record<string, Record<string, string>> = { es: { about: "sobre-nosotros", contact: "contacto", blog: "blog", pricing: "precios", },};Use useTranslatedPath(lang) to generate correct URLs regardless of locale:
import { useTranslatedPath } from "@i18n/utils";const translatePath = useTranslatedPath(lang);
translatePath("/contact") // → "/contact" (en) or "/es/contacto" (es)translatePath("/blog") // → "/blog" (en) or "/es/blog" (es)Language switcher
Section titled “Language switcher”The LanguageSwitcher component in the mobile menu footer reads the current URL, maps the slug to the target locale’s slug via routes.ts, and renders links to both language variants. It is generated per-page in Navbar.astro:
const langSwitchUrls = (Object.keys(languages) as Array<"en" | "es">).map((code) => { // maps current route slug to target locale slug ... return { code, label: languages[code], url };});Adding a translation string
Section titled “Adding a translation string”- Add the key to
src/locales/en/common.json(ormain.json/sections.json). - Add the Spanish translation to the matching file in
src/locales/es/. - Call
t("namespace:your.new.key")in your component.
{ "cta": { "bookCall": "Book a free call" }}{ "cta": { "bookCall": "Reservar una llamada gratuita" }}Adding a third language
Section titled “Adding a third language”- Add the locale to
src/i18n/ui.ts:
export const languages = { en: "English", es: "Español", fr: "Français", // ← add};-
Create
src/locales/fr/common.json,main.json, andsections.jsonwith French translations. -
Add route mappings to
src/i18n/routes.ts:
export const routes = { es: { contact: "contacto", /* ... */ }, fr: { contact: "contact", about: "a-propos", /* ... */ },};-
Mirror all pages under
src/pages/fr/following the same pattern assrc/pages/es/. -
Update
astro.config.mjsto include the new locale:
i18n: { defaultLocale: "en", locales: ["en", "es", "fr"],}