diff --git a/src/content/docs/en/recipes/i18n.mdx b/src/content/docs/en/recipes/i18n.mdx
index 7c8aab3d22d8a..1a479501d3774 100644
--- a/src/content/docs/en/recipes/i18n.mdx
+++ b/src/content/docs/en/recipes/i18n.mdx
@@ -1,6 +1,6 @@
---
-title: Add i18n features
-description: Use dynamic routing and content collections to add internationalization support to your Astro site.
+title: Add i18n features
+description: Use Astro's i18n routing and content collections to add internationalization support to your site.
type: recipe
i18nReady: true
---
@@ -9,16 +9,34 @@ import ReadMore from '~/components/ReadMore.astro';
import { Steps } from '@astrojs/starlight/components';
import StaticSsrTabs from '~/components/tabs/StaticSsrTabs.astro';
-In this recipe, you will learn how to use content collections and dynamic routing to build your own internationalization (i18n) solution and serve your content in different languages.
+In this recipe, you will use Astro's built-in internationalization (i18n) routing, content collections, and dynamic routing to serve content in different languages. You will also translate UI strings and add a language picker.
-:::tip
-In v4.0, Astro added built-in support for i18n routing that allows you to configure default and supported languages and includes valuable helper functions to assist you in serving an international audience. If you want to use this instead, see our [internationalization guide](/en/guides/internationalization/) to learn about these features.
-:::
-This example serves each language at its own subpath, e.g. `example.com/en/blog` for English and `example.com/fr/blog` for French.
+See the [resources section](#resources) for external links to related topics such as right-to-left (RTL) styling and choosing language tags.
-If you prefer the default language to not be visible in the URL unlike other languages, there are [instructions to hide the default language](/en/recipes/i18n/#hide-default-language-in-the-url) below.
+## Prerequisites
-See the [resources section](#resources) for external links to related topics such as right-to-left (RTL) styling and choosing language tags.
+- An existing Astro project.
+- Content in two languages. This recipe uses English (`en`) and French (`fr`).
+- Astro's built-in i18n routing configured with a URL prefix for every language:
+
+ ```js title="astro.config.mjs"
+ import { defineConfig } from "astro/config";
+
+ export default defineConfig({
+ i18n: {
+ locales: ["en", "fr"],
+ defaultLocale: "en",
+ routing: {
+ prefixDefaultLocale: true,
+ redirectToDefaultLocale: true,
+ },
+ },
+ });
+ ```
+
+ This configuration serves English pages from `/en/`, French pages from `/fr/`, and redirects the root URL (`/`) to `/en/`.
+
+ Read more about [configuring i18n routing](/en/guides/internationalization/).
## Recipe
@@ -34,36 +52,19 @@ If you prefer the default language to not be visible in the URL unlike other lan
- about.astro
- index.astro
- **fr/**
- - about.astro
+ - a-propos.astro
- index.astro
- index.astro
-2. Set up `src/pages/index.astro` to redirect to your default language.
+ This produces `/en/about/` for the English page and `/fr/a-propos/` for its French translation.
-
-
- ```astro
- ---
- // src/pages/index.astro
- ---
-
- ```
-
- This approach uses a [meta refresh](https://en.wikipedia.org/wiki/Meta_refresh) and will work however you deploy your site. Some static hosts also let you configure server redirects with a custom configuration file. See your deploy platform’s documentation for more details.
-
-
-
- If you are using an SSR adapter, you can use [`Astro.redirect`](/en/guides/routing/#dynamic-redirects) to redirect to the default language on the server.
+2. Create an empty `src/pages/index.astro` file. This route is required when `prefixDefaultLocale: true`. The `redirectToDefaultLocale: true` setting redirects requests from `/` to `/en/`.
- ```astro
- ---
- // src/pages/index.astro
- return Astro.redirect('/en/');
- ---
- ```
-
-
+ ```astro title="src/pages/index.astro"
+ ---
+ ---
+ ```
### Use collections for translated content
@@ -77,23 +78,50 @@ If you prefer the default language to not be visible in the URL unlike other lan
- blog/
- **en/** Blog posts in English
- post-1.md
- - post-2.md
- **fr/** Blog posts in French
- post-1.md
- - post-2.md
+ Add at least one entry for each language. This example uses the same file name for equivalent translations so the language picker can keep the visitor on the same blog post:
+
+ ```md title="src/content/blog/en/post-1.md"
+ ---
+ title: Hello world
+ author: Astro
+ date: 2026-01-01
+ ---
+
+ This is my first post in English.
+ ```
+
+ ```md title="src/content/blog/fr/post-1.md"
+ ---
+ title: Bonjour le monde
+ author: Astro
+ date: 2026-01-01
+ ---
+
+ Ceci est mon premier article en français.
+ ```
+
+ For this example, create a matching entry for every supported language. In a production site, check that a translation exists before linking to it.
+
2. Create a `src/content.config.ts` file and export a collection for each type of content.
```ts title="src/content.config.ts"
import { defineCollection } from "astro:content";
+ import { glob } from "astro/loaders";
import { z } from "astro/zod";
const blogCollection = defineCollection({
+ loader: glob({
+ base: "./src/content/blog",
+ pattern: "**/*.{md,mdx}",
+ }),
schema: z.object({
title: z.string(),
author: z.string(),
- date: z.date(),
+ date: z.coerce.date(),
}),
});
@@ -101,78 +129,13 @@ If you prefer the default language to not be visible in the URL unlike other lan
blog: blogCollection,
};
```
-
- Read more about [Content Collections](/en/guides/content-collections/).
-
-3. Use [dynamic routes](/en/guides/routing/#dynamic-routes) to fetch and render content based on a `lang` and a `slug` parameter.
-
-
-
- In static rendering mode, use `getStaticPaths` to map each content entry to a page:
-
- ```astro title="src/pages/[lang]/blog/[...slug].astro"
- ---
- import { getCollection, render } from "astro:content";
-
- export async function getStaticPaths() {
- const pages = await getCollection("blog");
-
- const paths = pages.map((page) => {
- const [lang, ...slug] = page.id.split("/");
- return { params: { lang, slug: slug.join("/") || undefined }, props: page };
- });
-
- return paths;
- }
-
- const { lang, slug } = Astro.params;
- const page = Astro.props;
- const formattedDate = page.data.date.toLocaleString(lang);
- const { Content } = await render(page);
- ---
-
-
-
- ```
-
-
-
- Read more about [dynamic routing](/en/guides/routing/#dynamic-routes).
-
- :::tip[Date formatting]
- The example above uses the built-in [`toLocaleString()` date-formatting method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) to create a human-readable string from the frontmatter date.
- This ensures the date and time are formatted to match the user’s language.
- :::
+ Read more about [Content Collections](/en/guides/content-collections/).
### Translate UI strings
-Create dictionaries of terms to translate the labels for UI elements around your site. This allows your visitors to experience your site fully in their language.
+Create dictionaries of terms to localize shared UI labels for each language.
1. Create a `src/i18n/ui.ts` file to store your translation strings:
@@ -181,64 +144,100 @@ Create dictionaries of terms to translate the labels for UI elements around your
export const languages = {
en: "English",
fr: "Français",
- };
+ } as const;
- export const defaultLang = "en";
+ export type Lang = keyof typeof languages;
+ export const defaultLang = "en" satisfies Lang;
export const ui = {
en: {
"nav.home": "Home",
"nav.about": "About",
"nav.twitter": "Twitter",
+ "blog.by": "by",
},
fr: {
"nav.home": "Accueil",
"nav.about": "À propos",
+ "blog.by": "par",
},
} as const;
+
+ export const routes = {
+ home: {
+ en: "",
+ fr: "",
+ },
+ about: {
+ en: "about",
+ fr: "a-propos",
+ },
+ blog: {
+ en: "blog",
+ fr: "blog",
+ },
+ } as const satisfies Record>;
+
+ export type Route = keyof typeof routes;
```
-
-2. Create two helper functions: one to detect the page language based on the current URL, and one to get translations strings for different parts of the UI in `src/i18n/utils.ts`:
- ```ts title="src/i18n/utils.ts"
- import { ui, defaultLang } from "./ui";
+2. Create helper functions to normalize the current locale, return its UI strings, and generate a localized path from a route in `src/i18n/utils.ts`:
- export function getLangFromUrl(url: URL) {
- const [, lang] = url.pathname.split("/");
- if (lang in ui) return lang as keyof typeof ui;
- return defaultLang;
+ ```ts title="src/i18n/utils.ts"
+ import { getRelativeLocaleUrl } from "astro:i18n";
+ import {
+ defaultLang,
+ languages,
+ routes,
+ ui,
+ type Lang,
+ type Route,
+ } from "./ui";
+
+ export function getLang(locale: string | undefined): Lang {
+ return locale && locale in languages ? (locale as Lang) : defaultLang;
}
- export function useTranslations(lang: keyof typeof ui) {
- const localizedUI: Record = ui[lang];
+ export function useTranslations(lang: string | undefined) {
+ const currentLang = getLang(lang);
+ const localizedUI: Record = ui[currentLang];
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return key in localizedUI ? localizedUI[key] : ui[defaultLang][key];
};
}
+
+ export function getLocalizedPath(
+ route: Route,
+ lang: Lang,
+ path?: string,
+ ) {
+ const segments = [routes[route][lang], path].filter(Boolean);
+ return getRelativeLocaleUrl(lang, segments.join("/"));
+ }
```
:::note[Did you notice?]
In step 1, the `nav.twitter` string was not translated to French. You may not want every term translated, such as proper names or common industry terms. The `useTranslations` helper will return the default language’s value if a key is not translated. In this example, French users will also see “Twitter” in the nav bar.
:::
-3. Import the helpers where needed and use them to choose the UI string that corresponds to the current language. For example, a nav component might look like:
+3. Use [`Astro.currentLocale`](/en/reference/api-reference/#currentlocale) to translate UI strings and create links for the current language:
```astro title="src/components/Nav.astro"
---
- import { getLangFromUrl, useTranslations } from "../i18n/utils";
+ import { getLang, getLocalizedPath, useTranslations } from "../i18n/utils";
- const lang = getLangFromUrl(Astro.url);
+ const lang = getLang(Astro.currentLocale);
const t = useTranslations(lang);
---
@@ -250,73 +249,66 @@ Create dictionaries of terms to translate the labels for UI elements around your
```
-4. Each page must have a `lang` attribute on the `` element that matches the language on the page. In this example, a [reusable layout](/en/basics/layouts/) extracts the language from the current route:
-
- ```astro title="src/layouts/Base.astro"
- ---
- import { getLangFromUrl } from "../i18n/utils";
-
- const lang = getLangFromUrl(Astro.url);
- ---
+
-
-
-
-
-
- Astro
-
-
-
-
-
- ```
+### Translate route slugs
- You can then use this base layout to ensure that pages use the correct `lang` attribute automatically.
-
- ```astro title="src/pages/en/about.astro"
- ---
- import Base from "../../layouts/Base.astro";
- ---
+The `routes` object in `src/i18n/ui.ts` uses the same route key for each translated path. For example, the `about` key represents both the English `about` path and the French `a-propos` path.
-
-
About me
- ...
-
- ```
-
+The `getLocalizedPath()` helper first selects the path for the requested language, then passes it to Astro's [`getRelativeLocaleUrl()`](/en/reference/modules/astro-i18n/#getrelativelocaleurl) function. This produces `/en/about/` for English and `/fr/a-propos/` for French. Add every route used by your navigation or language picker to the `routes` object and use its shared key when creating links.
### Let users switch between languages
-Create links to the different languages you support so users can choose the language they want to read your site in.
+Create links to the equivalent translated route in each language so users can choose the language they want to read your site in.
-1. Create a component to show a link for each language:
+1. Create a component that accepts a shared route key and links to its path in each supported language:
```astro title="src/components/LanguagePicker.astro"
---
- import { languages } from "../i18n/ui";
+ import { languages, type Lang, type Route } from "../i18n/ui";
+ import { getLocalizedPath } from "../i18n/utils";
+
+ interface Props {
+ route: Route;
+ path?: string;
+ }
+
+ const { route, path } = Astro.props;
---
```
-2. Add `` to your site so it is shown on every page. The example below adds it to the site footer in a base layout:
+2. Create a [reusable layout](/en/basics/layouts/) that adds the localized navigation and language picker to every page. This layout also uses `Astro.currentLocale` to add a matching `lang` attribute to the `` element:
- ```astro ins={2,17-19} title="src/layouts/Base.astro"
+ ```astro title="src/layouts/Base.astro"
---
import LanguagePicker from "../components/LanguagePicker.astro";
- import { getLangFromUrl } from "../i18n/utils";
+ import Nav from "../components/Nav.astro";
+ import type { Route } from "../i18n/ui";
+ import { getLang } from "../i18n/utils";
+
+ interface Props {
+ route: Route;
+ path?: string;
+ }
- const lang = getLangFromUrl(Astro.url);
+ const { route, path } = Astro.props;
+ const lang = getLang(Astro.currentLocale);
---
@@ -327,208 +319,154 @@ Create links to the different languages you support so users can choose the lang
Astro
+
```
-
-### Hide default language in the URL
-
-
-1. Create a directory for each language except the default language. For example, store your default language pages directly in `pages/`, and your translated pages in `fr/`:
-
-
- - src/
- - pages/
- - about.astro
- - index.astro
- - **fr/**
- - about.astro
- - index.astro
-
+3. Pass the same route key from each translated version of a page. Both `/en/about/` and `/fr/a-propos/` use the `about` key:
-2. Add another line to the `src/i18n/ui.ts` file to toggle the feature:
+ ```astro title="src/pages/en/about.astro" ins="5"
+ ---
+ import Base from "../../layouts/Base.astro";
+ ---
- ```ts title="src/i18n/ui.ts"
- export const showDefaultLang = false;
+
+
About me
+
Welcome to my site.
+
```
-3. Add a helper function to `src/i18n/utils.ts`, to translate paths based on the current language:
-
- ```ts title="src/i18n/utils.ts"
- import { ui, defaultLang, showDefaultLang } from "./ui";
+ ```astro title="src/pages/fr/a-propos.astro" ins="5"
+ ---
+ import Base from "../../layouts/Base.astro";
+ ---
- export function useTranslatedPath(lang: keyof typeof ui) {
- return function translatePath(path: string, l: string = lang) {
- return !showDefaultLang && l === defaultLang ? path : `/${l}${path}`;
- };
- }
+
+
À propos de moi
+
Bienvenue sur mon site.
+
```
-4. Import the helper where needed. For example, a `nav` component might look like:
+4. Use the `home` key for both localized index pages:
- ```astro title="src/components/Nav.astro"
+ ```astro title="src/pages/en/index.astro" ins="5"
---
- import {
- getLangFromUrl,
- useTranslations,
- useTranslatedPath,
- } from "../i18n/utils";
-
- const lang = getLangFromUrl(Astro.url);
- const t = useTranslations(lang);
- const translatePath = useTranslatedPath(lang);
+ import Base from "../../layouts/Base.astro";
---
-
+
```
-5. The helper function can also be used to translate paths for a specific language. For example, when users switch between languages:
-
- ```astro title="src/components/LanguagePicker.astro"
+ ```astro title="src/pages/fr/index.astro" ins="5"
---
- import { languages } from "../i18n/ui";
- import { getLangFromUrl, useTranslatedPath } from "../i18n/utils";
-
- const lang = getLangFromUrl(Astro.url);
- const translatePath = useTranslatedPath(lang);
+ import Base from "../../layouts/Base.astro";
---
-
+
+
+ ```
+
+
-4. The helper function can be used to get a translated route. For example, when no translated route is defined, the user will be redirected to the home page:
+ Read more about [dynamic routing](/en/guides/routing/#dynamic-routes).
- ```astro title="src/components/LanguagePicker.astro"
- ---
- import { languages } from "../i18n/ui";
- import { getRouteFromUrl, useTranslatedPath } from "../i18n/utils";
+ :::tip[Date formatting]
+ The example above uses the built-in [`toLocaleDateString()` date-formatting method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString) to create a human-readable string that matches the page language. Setting the time zone to `UTC` keeps a date-only frontmatter value on the same calendar day.
+ :::
+
- const route = getRouteFromUrl(Astro.url);
- ---
+### Check the result
-
- ```
-
+Start your development server and visit the following routes:
+
+- `/` redirects to `/en/`.
+- `/en/` and `/fr/` display translated navigation labels and use the matching `lang` attribute.
+- The language picker links `/en/about/` to `/fr/a-propos/` and back again.
+- `/en/blog/post-1/` and `/fr/blog/post-1/` render the matching content entry, and the language picker links between them.
## Resources
- [Choosing a Language Tag](https://www.w3.org/International/questions/qa-choosing-language-tags)