npm package: @mielp/eleventy-plugin-i18n
Description: Simple translation plugin for 11ty v3.
Author: Mielp
Latest version: 1.0.0

# @mielp/eleventy-plugin-i18n

Very simple flat translation plugin for [Eleventy](https://www.11ty.dev/) v3.

This is an ESM-only package.

- [@mielp/eleventy-plugin-i18n](#mielpeleventy-plugin-i18n)
  - [Install](#install)
  - [Configure](#configure)
  - [Usage](#usage)
    - [Basic usage](#basic-usage)
    - [Translation templates](#translation-templates)
    - [Simple translation fallback](#simple-translation-fallback)
    - [Per-locale translation fallback](#per-locale-translation-fallback)
  - [Example](#example)
  - [Roadmap](#roadmap)

## Install

Install from [npm](https://www.npmjs.com/package/@mielp/eleventy-plugin-i18n).

```shell
npm i -D eleventy-plugin-i18n
```

## Configure

This plugin supports the following configuration options:

```typescript
{
  // Translation dictionaries, one for each supported locale
  translations: {
    [locale: string]: {
      [key: string]: string
    }
  },
  // Recommended: a default locale to fallback to when something is missing
  defaultFallback: string,
  // Recommended: an array of preferred fallbacks for each locale
  localeFallbacks: {
    [locale: string]: string[]
  },
  // Optional: the name of the filter, default is `t`
  filterName: string
}
```

> [!NOTE]  
> Dictionary translations are "flat". The key `a.b.c` will not refer to a nested `translations[page.lang].a.b.c` value, but directly to `translations[page.lang]["a.b.c"]`.
>
> This is different from what many other i18n plugins will do. It makes sense for my use case: on the websites I maintain, I use a CMS like Decap or Sveltia to manage translations in a flat per-language dictionary structure. It works really well for me.

## Usage

### Basic usage

From any template, call the `t` filter (or whatever you named it). It accepts 3 parameters:

```typescript
t(
  key: string, // The string to translate
  data?: object | array, // Optional data object for template interpolation
  localeOverride?: string // Optional locale to enforce instead of the page locale
);
```

Short syntax: `{{ t('hello') }}` and `{{ 'hello' | t }}` are equivalent.

The locale used to find the translation string will default to the value of `page.lang`, or one of the configured fallbacks if the key `hello` is not found in the `page.lang` translation dictionary.

Full syntax: `{{ t('hello', data, 'eo') }}` and `{{ 'hello' | t(data, 'eo') }}` are equivalent.

The locale used to find the translation string will be `eo`, or one of the configured fallbacks if the key `hello` is not found in the `eo` translation dictionary. The `data` object is passed directly to templite to resolve values for the [translation template](#translation-templates).

### Translation templates

This plugin uses [templite](https://github.com/lukeed/templite) under the hood. Translation messages are templates, in which `{{something}}` is a special syntax that denotes a token.

Tokens are replaced with value from the `data` object passed to the filter. The `data` object can be a standard object or an array, and can also be nested (see [templite's usage](https://github.com/lukeed/templite?tab=readme-ov-file#usage) for more information).

Example:

With the following dictionary:

```json
{ "current.year": "We are in {{y}}" }
```

The text `{{ 'current.year' | t({ y: 2025 }) }}` would be translated to `We are in 2025`.

### Simple translation fallback

This plugin supports configuring fallbacks through the `defaultFallback` option. It accepts a single locale, or an array of locales. In the case of an array, the first element has highest priority, and the last element has lowest priority.

Example:

With the following fallback:

```json
{ "defaultFallback": "en" }
```

When requesting a translation for the `fr` locale, if the key is not found in the `fr` dictionary, the plugin will try to find it in the `en` dictionary. A warning is logged to the console to help you identify and add the missing translations.

It is also possible to use an array:

```json
{ "defaultFallback": ["en", "es"] }
```

When requesting a translation for the `fr` locale, if the key is not found in the `fr` dictionary, the plugin will try to find it in the `en` dictionary. If it is not found, the plugin will try to find it in the `es` dictionary. A warning is logged to the console to help you identify and add the missing translations.

### Per-locale translation fallback

This plugin also supports configuring per-locale fallbacks through the `localeFallbacks` option. It accepts a object that maps an input locale to its fallbacks (a single string or an array of strings).

Example:

With the following fallbacks:

```json
{ "localeFallbacks": {
  "fr": ["eo", "es"]
} }
```

When requesting a translation for the `fr` locale, if the key is not found in the `fr` dictionary, the plugin will try to find it in the `eo` dictionary. If it is not found, the plugin will try to find it in the `es` dictionary. A warning is logged to the console to help you identify and add the missing translations.

If the key is not found in any of these dictionaries, the plugin will try the fallbacks set in the global `defaultFallback` option.

If the translation isn't found anywhere, an error is thrown.

## Example

Configuration for a multilingual website that supports Esperanto, Spanish, French and Toki Pona:

```js
import eleventyPluginI18n from "@mielp/eleventy-plugin-i18n";
// Fictional method that fetches a dictionary of translations
import fetchDictionary from "./your-code.js";

export default function (eleventyConfig) {
  eleventyConfig.addPlugin(eleventyPluginI18n, {
    translations: {
      "eo": fetchDictionary("esperanto"),
      "es-ES": fetchDictionary("spanish"),
      "fr-FR": fetchDictionary("french"),
      // Dictionary example: each dictionary is a key-value pair.
      "tok": {
        "welcome": "o kama pona!",
        "current.year": "lon tenpo {{year}}.",
      },
    },
    defaultFallback: "eo",
    localeFallbacks: {
      "eo": "es-ES", // Fallback Esperanto to Spanish
      "fr-FR": "es-ES", // Fallback French to Spanish
    },
  });
}
```

> [!WARNING]  
> The fallback locales are not transitive. In the above example, if a key for `eo` is missing, it will fallback to `es-ES`, as configured. If a key for `tok` is missing, it will fallback to `eo` as configured, but not to `es-ES` (which is in the fallback list for `eo`, but not for `tok`).

With the following Nunjucks template:

```txt
---
# We can define additional dictionary information in the frontmatter
i18n:
  en:
    "welcome": "Welcome!"
---
{{ 'welcome' | t }}
{{ 'welcome' | t({}, 'en') }}
{{ 'current.year' | t({ year: 2025 }) }}
{{ 'goodbye' | t }}
```

In each supported language, the above file would be translated to:

- French, assuming all translation keys exist:

  ```txt
  Bienvenue !
  Welcome!
  On est en 2025.
  Au revoir !
  ```

- Spanish, assuming "goodbye" is missing, will fallback to Esperanto, the main `defaultFallback`:

  ```txt
  ¡Bienvenido!
  Welcome!
  Es 2025.
  Ĝis!
  ```

- Esperanto, assuming "current.year" is missing, will fallback to Spanish, according to the rules in `localeFallbacks`:

  ```txt
  Bonvenon!
  Welcome!
  Es 2025.
  Ĝis!
  ```

- Toki pona, assuming "goodbye" is missing, will fallback to Esperanto, the main `defaultFallback`:

  ```txt
  o kama pona!
  Welcome!
  lon tenpo 2025.
  Ĝis!
  ```

## Roadmap

- Improve documentation
- Add integration tests for 11ty+Nunjucks and 11ty+Liquid
- Add an example of a working multilingual website