astro-loader-i18n
is a content loader for internationalized content in Astro. It builds on top of Astroโs glob()
loader and helps manage translations by detecting locales, mapping content, and enriching getStaticPaths
.
-
Extracts locale information from file names or folder structures:
๐ Folder structure example
. (project root) โโโ README.md โโโ src โโโ content โโโ pages โโโ de-CH โ โโโ about.mdx โ โโโ projects.mdx โโโ zh-CN โโโ about.mdx โโโ projects.mdx
๐ File name suffix example
. (project root) โโโ src โโโ content โโโ pages โโโ about.de-CH.mdx โโโ about.zh-CN.mdx โโโ projects.de-CH.mdx โโโ projects.zh-CN.mdx
-
Supports nested folders:
๐ Nested folder structure example
. (project root) โโโ src โโโ content โโโ pages โโโ de-CH โ โโโ about.mdx โ โโโ projects โ โโโ project1.mdx โ โโโ project2.mdx โโโ zh-CN โโโ about.mdx โโโ projects โโโ project1.mdx โโโ project2.mdx
- Generates a translation identifier to easily match different language versions of content.
- Helps to define schemas for your localized content.
- Add
translationId
andlocale
to the schema by usingextendI18nLoaderSchema
. You need this when usingi18nLoader
ori18nContentLoader
. - When you have multiple locales in a single file, you can use
localized
to define the necessary schema. This is useful when usingi18nContentLoader
.
- Includes a helper utility called
i18nPropsAndParams
- Helps to fill and translate URL params like
[...locale]/[files]/[slug]
, whereas[...locale]
is the locale,[files]
is a translated segment and[slug]
is the slug of the title. - Adds a
translations
object to each entry, which contains paths to the corresponding content of all existing translations.
- Helps to fill and translate URL params like
- Keeps
Astro.props
type-safe.
-
Install the package
astro-loader-i18n
:npm
npm install astro-loader-i18n
yarn
yarn add astro-loader-i18n
pnpm
pnpm add astro-loader-i18n
-
Configure locales, a default locale and segments for example in a file called
site.config.ts
:export const C = { LOCALES: ["de-CH", "zh-CN"], DEFAULT_LOCALE: "de-CH" as const, SEGMENT_TRANSLATIONS: { "de-CH": { files: "dateien", }, "zh-CN": { files: "files", }, }, };
-
Configure i18n in
astro.config.ts
:import { defineConfig } from "astro/config"; import { C } from "./src/site.config"; export default defineConfig({ i18n: { locales: C.LOCALES, defaultLocale: C.DEFAULT_LOCALE, }, });
-
Define collections using
astro-loader-i18n
incontent.config.ts
. Don't forget to useextendI18nLoaderSchema
orlocalized
to extend the schema with the i18n specific properties:import { defineCollection, z } from "astro:content"; import { extendI18nLoaderSchema, i18nContentLoader, i18nLoader, localized } from "astro-loader-i18n"; import { C } from "./site.config"; const filesCollection = defineCollection({ loader: i18nLoader({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/files" }), schema: extendI18nLoaderSchema( z.object({ title: z.string(), }) ), }); const folderCollection = defineCollection({ loader: i18nLoader({ pattern: "**/[^_]*.{md,mdx}", base: "./src/content/folder" }), schema: extendI18nLoaderSchema( z.object({ title: z.string(), }) ), }); /* Example of a content file: navigation: de-CH: - path: /projekte title: Projekte - path: /ueber-mich title: รber mich zh-CN: - path: /zh/projects title: ้กน็ฎ - path: /zh/about-me title: ๅ ณไบๆ */ const infileCollection = defineCollection({ loader: i18nContentLoader({ pattern: "**/[^_]*.{yml,yaml}", base: "./src/content/infile" }), schema: extendI18nLoaderSchema( // `extendI18nLoaderSchema` defines `translationId` and `locale` for you in the schema. z.object({ navigation: localized( // `localized` defines an object with the locale as key and applies the schema you provide to the value. z.array( z.object({ path: z.string(), title: z.string(), }) ), C.LOCALES ), }) ), }); export const collections = { files: filesCollection, folder: folderCollection, infile: infileCollection, };
-
Create content files in the defined structure:
โ ๏ธ WARNING The content files need to be structured according to the locales defined inastro.config.ts
.. (project root) โโโ src โโโ content โโโ pages โโโ about.de-CH.mdx โโโ about.zh-CN.mdx โโโ projects.de-CH.mdx โโโ projects.zh-CN.mdx
-
Retrieve the
locale
andtranslationId
identifier during rendering:import { getCollection } from "astro:content"; const pages = await getCollection("files"); console.log(pages["data"].locale); // e.g. de-CH console.log(pages["data"].translationId); // e.g. src/content/files/about.mdx
-
Use
i18nPropsAndParams
to provide params and get available translations paths via the page props:import { i18nPropsAndParams } from "astro-loader-i18n"; import sluggify from "limax"; // sluggify is used to create a slug from the title export const getStaticPaths = async () => { // โ ๏ธ Unfortunately there is no way to access the routePattern, that's why we need to define it here again. // see https://github.com/withastro/astro/pull/13520 const routePattern = "[...locale]/[files]/[slug]"; const filesCollection = await getCollection("files"); return i18nPropsAndParams(filesCollection, { defaultLocale: C.DEFAULT_LOCALE, routePattern, segmentTranslations: C.SEGMENT_TRANSLATIONS, // `generateSegments` is a function that generates per entry individual segments. generateSegments: (entry) => ({ slug: sluggify(entry.data.title) }), }); };
-
Use
Astro.props.translations
to provide a same site language switcher.
Sometimes to have multilingual content in a single file is more convenient. For example data for menus or galleries. This allows sharing untranslated content across locales.
Use the i18nContentLoader
loader to load in-file localized content.
-
Create a collection:
๐ Infile collection example
. (project root) โโโ src โโโ content โโโ navigation โโโ footer.yml โโโ main.yml
๐ Content of
main.yml
# src/content/navigation/main.yml navigation: de-CH: - path: /projekte title: Projekte - path: /ueber-mich title: รber mich zh-CN: - path: /zh/projects title: ้กน็ฎ - path: /zh/about-me title: ๅ ณไบๆ
-
Use
extendI18nLoaderSchema
andlocalized
to define the schema:const infileCollection = defineCollection({ loader: i18nContentLoader({ pattern: "**/[^_]*.{yml,yaml}", base: "./src/content/infile" }), schema: extendI18nLoaderSchema( // `extendI18nLoaderSchema` defines `translationId` and `locale` for you in the schema. z.object({ navigation: localized( // `localized` defines an object with the locale as key and applies the schema you provide to the value. z.array( z.object({ path: z.string(), title: z.string(), }) ), C.LOCALES ), }) ), });
-
When you get the collection, you will receive for each locale the localized content. For example if you have two locales
de-CH
andzh-CN
with two filesmain.yml
andfooter.yml
, you will get four entries in the collection:import { getCollection } from "astro:content"; const navigation = await getCollection("infile"); console.log(navigation[0].data.locale); // e.g. de-CH console.log(navigation[0].data.translationId); // e.g. src/content/infile/main.yml console.log(navigation[0].data.navigation); // e.g. [{ path: "/projekte", title: "Projekte" }, ...]
Sometimes you want to translate a page that is not based on i18n content. For example an index page or a 404 page.
createI18nCollection
allows you to create a virtual collection that is not based on any content:
export const getStaticPaths = async () => {
const routePattern = "[...locale]/[files]";
const collection = createI18nCollection({ locales: C.LOCALES, routePattern });
return i18nPropsAndParams(collection, {
defaultLocale: C.DEFAULT_LOCALE,
routePattern,
segmentTranslations: C.SEGMENT_TRANSLATIONS,
});
};
Below you can find a description of all exported functions and types.
i18nLoader
parses i18n information from file names or folder structures.
As this is a wrapper around the glob()
loader, you can use all options from the glob()
loader. See the Astro documentation for more information.
It adds the following properties to an entrys data
object:
locale
: The locale of the entry. This is either the folder name or the file name suffix.translationId
: The translation identifier. This helps to identify the same content in different languages.contentPath
: The path to the file relative to the content base path. This is useful if you want to add the folder names into the path. For examplesrc/content/pages/de-CH/deeply/nested/about.mdx
it would bedeeply/nested
.basePath
: The base path from the Astro config. This is a workaround, because fromgetStaticPaths()
you don't have access to the base path and you need it for generating paths.
i18nContentLoader
creates multiple entries from a single yaml or json file.
See i18nLoader for more information.
localized
is a helper function to define a schema for in-file localized content. It takes a schema and an array of locales and returns a schema that is an object with the locale as key and the schema as value.
Parameters:
schema
: The schema to apply to the value of the object.locales
: An array of locales to use as keys for the object.partial
: Optional. Iftrue
, not all locales need to be defined in the schema.
extendI18nLoaderSchema
is a helper function to extend the schema of the i18nLoader
and i18nContentLoader
. It adds the translationId
, locale
, contentPath
and basePath
properties to the schema.
i18nLoaderSchema
is a schema that is used by the i18nLoader
and i18nContentLoader
. It defines the properties that are added to the entrys data
object.
i18nPropsAndParams
is a helper function to generate the params
and props
object for getStaticPaths()
and to provide the translations object to the page props.
Parameters:
collection
: The collection to use. This can be a collection fromgetCollection()
or a virtual collection created withcreateI18nCollection()
.options
: An object with the following properties:defaultLocale
: The default locale to use.routePattern
: The route pattern to use. This is the pattern that is used in thegetStaticPaths()
function. Unfortunately there is no way to access the routePattern, that's why we need to define it here again.segmentTranslations
: An object with the segment translations. This is used to translate the segments in the URL.generateSegments
: (Optional) A function that generates the segments for each entry. This is useful if you want to generate slugs or other segments.localeParamName
: (Optional) The name of the locale parameter in the URL. This is used to generate the URL for the translations object.prefixDefaultLocale
: (Optional) Iftrue
, the default locale will be prefixed to the URL. This is useful if you want to have a clean URL for the default locale.
It returns an object with params
and props
. props
contains additionally a translations
object with the paths to the corresponding content of all existing translations. The translatedPath
is the current entry path.
createI18nCollection
creates a virtual collection that is not based on any content. This is useful if you want to create a collection for a page that is not based on i18n content.
resolvePath
is a helper function that connects path segments and deals with slashes.
Made by the author of astro-loader-i18n
:
- Test project (Source): Minimal example of how to use
astro-loader-i18n
with Astro. - Astro Theme International (Demo / Source): A demo theme with the goal to be as international as possible.
- r.obin.ch (Demo / Source): A personal website with a blog and projects, built with Astro and
astro-loader-i18n
.
Made by the community:
- Improve types of params returned by
i18nPropsAndParams
- Include a language switcher Astro component
To make internationalization easier, Astro could offer the following features:
- Provide routing information to
getStaticPaths()
such as theroutePattern
to avoid manual repetition. Also see this pull request: withastro/astro#13520 - Allow to define custom parameters for
getStaticPaths()
likepaginate
from integrations and loaders. This makes integrating additional helpers for buildinggetStaticPaths()
way easier. - Allow to define different schemas for input (this already exists, today) and output of a loader. This is useful if a loader transforms the data. Currently the schema wouldn't match the output of the loader anymore.
- Allow to define additional custom properties from loaders apart from the
data
object, that are available insidegetStaticPaths()
and while rendering. This is useful if a loader calculates additional properties that later used in the template and are not necessarily part of the data object to avoid collisions with the user provided data.