UNPKG

astro-loader-i18n

Version:

An Astro content loader for i18n files and folder structures.

429 lines (338 loc) โ€ข 16.8 kB
# astro-loader-i18n [![NPM Downloads](https://img.shields.io/npm/dw/astro-loader-i18n)](https://npmjs.org/astro-loader-i18n) [![npm bundle size](https://img.shields.io/bundlephobia/min/astro-loader-i18n)](https://npmjs.org/astro-loader-i18n) `astro-loader-i18n` is a **content loader** for internationalized content in [Astro](https://astro.build). It builds on top of Astroโ€™s [`glob()` loader](https://docs.astro.build/en/reference/content-loader-reference/#glob-loader) and helps manage translations by detecting locales, mapping content, and enriching `getStaticPaths`. ## Features ### โœ… Automatic locale detection - Extracts locale information from file names or folder structures: <details> <summary>๐Ÿ“‚ Folder structure example</summary> ```plaintext . (project root) โ”œโ”€โ”€ README.md โ””โ”€โ”€ src โ””โ”€โ”€ content โ””โ”€โ”€ pages โ”œโ”€โ”€ de-CH โ”‚ โ”œโ”€โ”€ about.mdx โ”‚ โ””โ”€โ”€ projects.mdx โ””โ”€โ”€ zh-CN โ”œโ”€โ”€ about.mdx โ””โ”€โ”€ projects.mdx ``` </details> <details> <summary>๐Ÿ“„ File name suffix example</summary> ```plaintext . (project root) โ””โ”€โ”€ src โ””โ”€โ”€ content โ””โ”€โ”€ pages โ”œโ”€โ”€ about.de-CH.mdx โ”œโ”€โ”€ about.zh-CN.mdx โ”œโ”€โ”€ projects.de-CH.mdx โ””โ”€โ”€ projects.zh-CN.mdx ``` </details> - Supports nested folders: <details> <summary>๐Ÿ“‚ Nested folder structure example</summary> ```plaintext . (project root) โ””โ”€โ”€ src โ””โ”€โ”€ content โ””โ”€โ”€ pages โ”œโ”€โ”€ de-CH โ”‚ โ”œโ”€โ”€ about.mdx โ”‚ โ””โ”€โ”€ projects โ”‚ โ”œโ”€โ”€ project1.mdx โ”‚ โ””โ”€โ”€ project2.mdx โ””โ”€โ”€ zh-CN โ”œโ”€โ”€ about.mdx โ””โ”€โ”€ projects โ”œโ”€โ”€ project1.mdx โ””โ”€โ”€ project2.mdx ``` </details> ### โœ… Translation mapping - Generates a translation identifier to easily match different language versions of content. ### โœ… Schema support - Helps to define schemas for your localized content. - Add `translationId` and `locale` to the schema by using `extendI18nLoaderSchema`. You need this when using `i18nLoader` or `i18nContentLoader`. - When you have multiple locales in a single file, you can use `localized` to define the necessary schema. This is useful when using `i18nContentLoader`. ### โœ… `getStaticPaths()` helpers included - 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. ### โœ… Type safety - Keeps `Astro.props` type-safe. ## Usage 1. Install the package `astro-loader-i18n`: <details open> <summary>npm</summary> ```bash npm install astro-loader-i18n ``` </details> <details> <summary>yarn</summary> ```bash yarn add astro-loader-i18n ``` </details> <details> <summary>pnpm</summary> ```bash pnpm add astro-loader-i18n ``` </details> 2. Configure locales, a default locale and segments for example in a file called `site.config.ts`: ```typescript export const C = { LOCALES: ["de-CH", "zh-CN"], DEFAULT_LOCALE: "de-CH" as const, SEGMENT_TRANSLATIONS: { "de-CH": { files: "dateien", }, "zh-CN": { files: "files", }, }, }; ``` 3. Configure i18n in `astro.config.ts`: ```typescript import { defineConfig } from "astro/config"; import { C } from "./src/site.config"; export default defineConfig({ i18n: { locales: C.LOCALES, defaultLocale: C.DEFAULT_LOCALE, }, }); ``` 4. Define collections using `astro-loader-i18n` in `content.config.ts`. Don't forget to use `extendI18nLoaderSchema` or `localized` to extend the schema with the i18n specific properties: ```typescript 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, }; ``` 5. Create content files in the defined structure: > โš ๏ธ WARNING > The content files need to be structured according to the locales defined in `astro.config.ts`. ``` . (project root) โ””โ”€โ”€ src โ””โ”€โ”€ content โ””โ”€โ”€ pages โ”œโ”€โ”€ about.de-CH.mdx โ”œโ”€โ”€ about.zh-CN.mdx โ”œโ”€โ”€ projects.de-CH.mdx โ””โ”€โ”€ projects.zh-CN.mdx ``` 6. Retrieve the `locale` and `translationId` identifier during rendering: ```typescript 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 ``` 7. Use `i18nPropsAndParams` to provide params and get available translations paths via the page props: ```typescript 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) }), }); }; ``` 8. Use `Astro.props.translations` to provide a same site language switcher. ### In-file localized content 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. 1. Create a collection: <details> <summary>๐Ÿ“„ Infile collection example</summary> ```plaintext . (project root) โ””โ”€โ”€ src โ””โ”€โ”€ content โ””โ”€โ”€ navigation โ”œโ”€โ”€ footer.yml โ””โ”€โ”€ main.yml ``` </details> <details> <summary>๐Ÿ“„ Content of <code>main.yml</code></summary> ```yaml # 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: ๅ…ณไบŽๆˆ‘ ``` </details> 1. Use `extendI18nLoaderSchema` and `localized` to define the schema: ```typescript 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 ), }) ), }); ``` 1. When you get the collection, you will receive for each locale the localized content. For example if you have two locales `de-CH` and `zh-CN` with two files `main.yml` and `footer.yml`, you will get four entries in the collection: ```typescript 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" }, ...] ``` ### Virtual i18n collections 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: ```typescript 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, }); }; ``` ## API Below you can find a description of all exported functions and types. ### `i18nLoader` `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](https://docs.astro.build/en/reference/content-loader-reference/#glob-loader) 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 example `src/content/pages/de-CH/deeply/nested/about.mdx` it would be `deeply/nested`. - `basePath`: The base path from the Astro config. This is a workaround, because from `getStaticPaths()` you don't have access to the base path and you need it for generating paths. ### `i18nContentLoader` `i18nContentLoader` creates multiple entries from a single yaml or json file. See [i18nLoader](#i18nloader) for more information. ### `localized` `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. If `true`, not all locales need to be defined in the schema. ### `extendI18nLoaderSchema` `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` `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` `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 from `getCollection()` or a virtual collection created with `createI18nCollection()`. - `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 the `getStaticPaths()` 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) If `true`, 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` `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` `resolvePath` is a helper function that connects path segments and deals with slashes. ## Examples Made by the author of `astro-loader-i18n`: - Test project ([Source](https://github.com/openscript/astro-loader-i18n/tree/main/apps/example)): Minimal example of how to use `astro-loader-i18n` with Astro. - Astro Theme International ([Demo](https://openscript.github.io/astro-theme-international/) / [Source](https://github.com/openscript/astro-theme-international)): A demo theme with the goal to be as international as possible. - r.obin.ch ([Demo](https://r.obin.ch) / [Source](https://github.com/openscript/r.obin.ch)): A personal website with a blog and projects, built with Astro and `astro-loader-i18n`. Made by the community: - eCamp3 ([Demo](https://www.ecamp3.ch) / [Source](https://github.com/ecamp/ecamp-site)): A website of an application for camp planning. ## Roadmap - [ ] Improve types of params returned by `i18nPropsAndParams` - [ ] Include a language switcher Astro component ## Wish list To make internationalization easier, **Astro** could offer the following features: - [ ] Provide routing information to `getStaticPaths()` such as the `routePattern` to avoid manual repetition. Also see this pull request: https://github.com/withastro/astro/pull/13520 - [ ] Allow to define custom parameters for `getStaticPaths()` like `paginate` from integrations and loaders. This makes integrating additional helpers for building `getStaticPaths()` 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 inside `getStaticPaths()` 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.