In my application, I want the following routing logic:
- URL prefix describing the language (
fr,enorzh), for example/fr/about,/en/about, etc… - Dynamic URLs based on content collections, for example
/fr/resources/my-resource,/zh/articles/my-article, etc… - If no language is specified in the URL, automatic redirection to the preferred language (from the languages configured in the browser)
Configuration
i18n integration is very easy in an Astro application.
In the main configuration file:
// astro.config.mjs
export default defineConfig({
i18n: {
locales: [
{ path: "fr", codes: ["fr", "fr-FR", "fr-CA"] },
{ path: "en", codes: ["en", "en-US", "en-GB"] },
{ path: "zh", codes: ["zh", "zh-CN", "zh-TW"] },
],
defaultLocale: "fr",
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: false,
},
},
//...
});
-
The
codesfor each language (path) allow setting the supported language codes, as configured in the browser. The codes must be compatible with the Accept-Language header syntax -
prefixDefaultLocaleallows to always have the locale in the URL, likehttps://domain.com/fr/page. -
redirectToDefaultLocale: falseallows to manually handle our redirection to the correct language when the user hits the homepage
Internationalization files
Following the recipe in the Astro documentation, we create the following files:
src/i18n/ui.ts: sets the supported languages of our application and the translation strings displayed in the UI (menus, footer, etc…)
export const languages = {
fr: "Français",
en: "English",
zh: "繁體中文",
};
export const ui = {
fr: {
"nav.home": "Accueil",
"nav.about": "À propos",
},
en: {
"nav.home": "Home",
"nav.about": "About",
},
zh: {
//...
},
} as const;
Next, still according to the recipe, we will also write utility functions allowing us to retrieve these localized strings (useTranslation in src/i18n/utils.ts).
Browser language detection
When the user hits the homepage, if no language is specified in the URL, we want to redirect them to their preferred language. The page in charge of this logic will be the homepage of the application (/, the root page).
If the user’s language is not supported by our application, then we will display the default language (defaultLocale declared in the configuration). The user will then be able to select the language of their choice with the language selector, in the UI.
Homepage (without a predefined language code):
---
// src/pages/index.astro
import { i18n } from "astro:config/server";
import { getPathByLocale } from "astro:i18n";
export const prerender = false;
const targetLocale = Astro.preferredLocale
? getPathByLocale(Astro.preferredLocale)
: i18n!.defaultLocale;
return Astro.redirect(`/${targetLocale}/`);
---
Redirecting to {`/${targetLocale}/`}...
The entire frontmatter part (between the ---) is executed on the server. With Astro, everything is statically pre-rendered during the application build, unless we explicitly write, as above, export const prerender = false;: every request on this URL will be evaluated by the server, in real-time.
Astro.preferredLocale
This property is only available for on-demand renders (prerender = false). It will contain the browser’s preferred language corresponding to an actual language of our application (processed by Astro). For instance, if the browser has its first language set as fr-FR, then Astro will find fr-FR in our configured codes and fill Astro.preferredLocale with the same value.
If our application does not support the browser’s language (for example we don’t provide Spanish es-ES), then Astro.preferredLocale will contain undefined: we can fall back to the defaultLocale.
getPathByLocale
The utility function getPathByLocale comes from the astro:i18n module: from a browser’s language code (en-US for example, or zh-TW), we retrieve the URL prefix to redirect to (en or zh). Every association is available in the configuration with the path and codes properties:
// astro.config.mjs
export default defineConfig({
i18n: {
locales: [
// The browser's language codes match a path in our application
{ path: "fr", codes: ["fr", "fr-FR", "fr-CA"] },
{ path: "en", codes: ["en", "en-US", "en-GB"] },
{ path: "zh", codes: ["zh", "zh-CN", "zh-TW"] },
],
//...
},
//...
});
This is how users fr-FR will be redirected to /fr/, users en-GB to /en/, etc…
Folder structure
The main page of the application (src/pages/index.astro) is responsible for redirecting the user to the correct language. But for other pages, we will create a folder src/pages/[locale]/ which will allow us to manage each page in multiple languages.
Thus, for static pages like the homepage or about, we can create a single file, and rely on the localized strings.
For more detailed contents like articles, we will create a page for displaying their content and then store our contents in content collections.
Content collections
As the entry point for the contents, let’s create a new src/content folder.
Inside this folder, we can create a sub-folder for each language. We will therefore have:
src/content/frsrc/content/ensrc/content/zh
Next, below each language, we can create a new sub-folder corresponding to the content collection type (for example articles):
src/content/fr/articlessrc/content/en/articlessrc/content/zh/articles
To load these contents, we declare a content collection whose pattern includes all our languages:
const articlesCollection = defineCollection({
loader: glob({
pattern: localizedPattern("articles/**/*.{md,mdx}"),
base: "src/content/",
}),
schema: z.object({
title: z.string(),
}),
});
localizedPattern prefixes the pattern with the different languages:
export function localizedPattern(pattern: string): string {
// remember the "languages" object ? src/i18n/ui.ts
const langCodes = Object.keys(languages);
return `(${langCodes.join("|")})/${pattern}`;
}
Following the example above, articles/**/*.{md,mdx} will give us (fr|en|zh)/articles/**/*.{md,mdx} (with base being the src/content/ folder, of course).
getStaticPaths
Having [locale] as the root folder implies that all pages have at least this one locale URL parameter. For static pages, it means that we will have to repeat the logic to retrieve all values of this parameter (/articles, /about, /contact, …) and generate all URLs.
We will create a utility function localizedPaths that will return objects containing our locale URL parameter for each supported language.
If there are no other URL parameters to fill, then we can directly re-export this function under the name getStaticPaths: Astro will generate the URLs for each language.
type LocalizedPath = {
params: {
locale: string;
};
};
/**
* For a given page, grab all languages and create the necessary paths with the locale param
*
* @returns An array of all localized paths
*/
export async function localizedPaths(): Promise<LocalizedPath[]> {
const langs = Object.keys(languages);
return langs.map((locale) => {
return { params: { locale } };
});
}
Then, in a page like [locale]/about.astro:
export { localizedPaths as getStaticPaths } from "@/lib/utils";