Skip to content

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).


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.json

These files are loaded automatically via import.meta.glob in src/i18n/ui.ts.

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.


---
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.


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)

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 };
});

  1. Add the key to src/locales/en/common.json (or main.json / sections.json).
  2. Add the Spanish translation to the matching file in src/locales/es/.
  3. Call t("namespace:your.new.key") in your component.
src/locales/en/common.json
{
"cta": {
"bookCall": "Book a free call"
}
}
src/locales/es/common.json
{
"cta": {
"bookCall": "Reservar una llamada gratuita"
}
}

  1. Add the locale to src/i18n/ui.ts:
export const languages = {
en: "English",
es: "Español",
fr: "Français", // ← add
};
  1. Create src/locales/fr/common.json, main.json, and sections.json with French translations.

  2. Add route mappings to src/i18n/routes.ts:

export const routes = {
es: { contact: "contacto", /* ... */ },
fr: { contact: "contact", about: "a-propos", /* ... */ },
};
  1. Mirror all pages under src/pages/fr/ following the same pattern as src/pages/es/.

  2. Update astro.config.mjs to include the new locale:

i18n: {
defaultLocale: "en",
locales: ["en", "es", "fr"],
}