Skip to content

An Astro glob content loader for i18n files and folder structures. ๐ŸŒ

License

Notifications You must be signed in to change notification settings

openscript/astro-loader-i18n

Repository files navigation

astro-loader-i18n

codecov NPM Downloads npm bundle size

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.

Features

โœ… Automatic locale detection

  • 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
    

โœ… Translation mapping

  • Generates a translation identifier to easily match different language versions of content.

โœ… Schema support

  • Provides predefined schemas for content.config.ts
    • Loader schema: i18nLoaderSchema
    • In-file schema: i18nInfileSchema
  • Adds a translationId and locale to each content item.

โœ… 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 (and limax for slug generation):

    npm
    npm install astro-loader-i18n limax
    yarn
    yarn add astro-loader-i18n limax
    pnpm
    pnpm add astro-loader-i18n limax
  2. 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",
        },
      },
    };
  3. 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,
      },
    });
  4. Define collections using astro-loader-i18n in content.config.ts. Don't forget to use extendI18nLoaderSchema or extendI18nInfileSchema to extend the schema with the i18n specific properties:

    import { defineCollection, z } from "astro:content";
    import { extendI18nInfileSchema, extendI18nLoaderSchema, i18nLoader } from "astro-loader-i18n";
    import { glob } from "astro/loaders";
    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: z.object({
        title: z.string(),
      }),
    });
    const infileCollection = defineCollection({
      loader: glob({ pattern: "**/[^_]*.{yml,yaml}", base: "./src/content/infile" }),
      schema: extendI18nInfileSchema(
        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 in astro.config.ts defined locales.

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

    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:

    import { i18nPropsAndParams } from "astro-loader-i18n";
    
    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,
      });
    };
  8. Finally check Astro.props.translations to link the other pages.

Infile 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 standard glob() loader to load infile i18n content.

  1. Create a collection:

    ๐Ÿ“„ Infile collection example
    . (project root)
    โ””โ”€โ”€ src
       โ””โ”€โ”€ content
           โ””โ”€โ”€ navigation
               โ”œโ”€โ”€ footer.yml
               โ””โ”€โ”€ main.yml
    
    ๐Ÿ“„ Content of main.yml
    # src/content/navigation/main.yml
    de-CH:
      - path: /projekte
        title: Projekte
      - path: /ueber-mich
        title: รœber mich
    zh-CN:
      - path: /zh/projects
        title: ้กน็›ฎ
      - path: /zh/about-me
        title: ๅ…ณไบŽๆˆ‘
  2. Use extendI18nInfileSchema to define the schema:

    const infileCollection = defineCollection({
      loader: glob({ pattern: "**/[^_]*.{yml,yaml}", base: "./src/content/infile" }),
      schema: extendI18nInfileSchema(
        z.array(
          z.object({
            path: z.string(),
            title: z.string(),
          })
        ),
        C.LOCALES
      ),
    });

Virtual i18n collections

Sometimes you want to translate 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,
  });
};