---
title: 'multilang'
description: 'Language infrastructure: normalises language config, attaches per-page locale data, builds translation collections, and exposes i18n filters. Opt-in.'
slug: 'multilang'
type: 'article'
date: 2026-04-15T00:00:00.000Z
lang: 'en'
url: 'https://www.eleventy-baseline.dev/docs/module/multilang/'
---

`INTERNAL_KEY`: '\_multilang'

---

## What it does

The multilang module wires Eleventy's built-in `I18nPlugin` (the locale-aware URL plugin), normalises your language config, attaches flat per-page language fields (`lang`, `locale`, `translationKey`, `isDefaultLang`), builds two `translations` collections, and registers three i18n filters for cross-language lookups in templates.

The translation map gets written into a runtime store; the head module reads it once the cascade closes to build hreflang alternates.

A translation key is the identifier you set on a page (`translationKey: 'about'` in front matter) so Baseline knows the English `/about/` and the French `/fr/a-propos/` are the same page in different languages. Hreflang is the HTML link relation that tells search engines about those alternates.

---

### Active when

All three of these must be set, otherwise the module exits early without registering anything:

- `options.multilingual: true`
- `settings.defaultLanguage` (a non-empty string)
- `settings.languages` (a non-empty object or array)

There is no inference. Setting only `defaultLanguage` and `languages` will not activate the module without the explicit `multilingual: true`.

---

### Lifecycle

- **Build-time.** Adds `I18nPlugin`, registers the i18n filters, registers the computed per-page language fields, normalises the language config through [`normalizeLanguageMap`](_baseline/core/utils/normalize-language-map.js).
- **Cascade-time.** The `translations` and `translationsMap` collection builders walk every page with a `translationKey`, build the per-key map, and write it to the translation-map store (the runtime store other modules read).

---

## How it works

{% stepsBlock %}

1. **Normalise the language config.** [`normalizeLanguageMap`](_baseline/core/utils/normalize-language-map.js) accepts an object map or an array of strings, lowercases the keys, drops invalid entries (logged when `verbose: true`), and returns the normalised object.
2. **Activate `I18nPlugin`.** Adds Eleventy's built-in plugin with the resolved `defaultLanguage` and `errorMode: 'allow-fallback'`.
3. **Compute the per-page language fields.** Four independent `eleventyComputed.page.*` registrations: `lang` (the short code), `locale` (the BCP 47 string), `translationKey`, and `isDefaultLang`.
4. **Build collections.** Both collections walk the same loop, build the `translationsMap` once, and write it to the translation-map store. Pages without a `translationKey` are skipped silently. Pages whose `lang` is outside the allowed set are logged.
5. **Register filters.** `i18nTranslationsFor`, `i18nTranslationIn`, `i18nDefaultTranslation`.

{% endstepsBlock %}

---

## Defaults

- **`errorMode`** for `I18nPlugin`: `'allow-fallback'`. Pages without a translation in the requested language fall back to the default-language version rather than 404.
- **Language resolution per page** (in order): `data.lang`, then `data.language`, then the language derived from `data.locale`, then `settings.defaultLanguage`.
- **Allowed-languages set.** Built from `settings.languages` keys. Pages whose `lang` is not in this set are logged and skipped during collection building.

`defaultLanguage` does not fall back to `'en'` automatically. If absent, the module stays inactive (see [Active when](#active-when)).

---

### Settings

The multilang module reads two parts of the `settings` argument. Full shape on [[site-settings | Site settings]].

{% tableBlock true %}

| Key                        | Type                 | Used for                                                                                                                        |
| -------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `settings.defaultLanguage` | `string`             | Default-language code. Pages in this language render at unprefixed URLs; others live under `/<lang>/`.                          |
| `settings.languages`       | `object \| string[]` | Map of language codes to per-language overrides, or a flat array of codes. Arrays are normalised to objects with empty entries. |

{% endtableBlock %}

---

### Options

{% tableBlock true %}

| Option         | Type      | Default | Meaning                                            |
| -------------- | --------- | ------- | -------------------------------------------------- |
| `multilingual` | `boolean` | `false` | Activate the module. Required, alongside settings. |

{% endtableBlock %}

---

## Per-page language fields

Every page receives four flat language fields on `page`, resolved during the cascade:

```js
export default {
	page: {
		lang: 'en', // short code, resolved and lowercased
		locale: 'en-US', // BCP 47 string, from `data.locale` or the language's `locale`
		translationKey: 'about', // from `data.translationKey`, or undefined
		isDefaultLang: true // lang === defaultLanguage
	}
};
```

`page.locale` is the BCP 47 string itself. The head module reads these fields for hreflang; the sitemap module reads `lang` for per-language partitioning.

---

## Collections

Two collections, both keyed by `translationKey`:

- **`translations`** is a flat list. Each entry is a safe copy of a page with the flat `lang`, `locale`, `translationKey`, and `isDefaultLang` fields attached.
- **`translationsMap`** is a nested map: `translationsMap[translationKey][lang]`. Each leaf carries `{ title, url, lang, isDefaultLang, data }`.

The map is also written to the translation-map store so transform-time consumers (the head module) can read it without going through `collections`.

---

## Filters

Three filters for cross-language lookups in templates. Full reference on [[filters | Filters]].

- `i18nTranslationsFor(page, collections.translations)`: every translation sibling of the current page.
- `i18nTranslationIn(page, collections.translations, lang)`: the specific-language variant, or `null`.
- `i18nDefaultTranslation(page, collections.translations)`: the default-language variant, or `null`.

---

## Tips

- Every localised page needs both `translationKey` and `lang` in its front matter. Without `translationKey`, the page does not appear in translation collections; without `lang` (or a default), the resolver falls back and may misclassify the page.
- Keep the default-language page present for every translation key. It powers the `x-default` alternate and the head module's fallback resolution.
- With multilang active, the sitemap module automatically emits per-language sitemaps plus an index. See [[sitemap | sitemap]].
- The "Unknown lang ..." log line is your signal that a page declared a language not in `settings.languages`. Add it to the languages map or fix the page's front matter.

---

### Rendering alternate links

The head module emits hreflang automatically when this module is active and the page has a `translationKey`. If you want to render alternates yourself (or override the default markup), `translationsMap` is the source:

{% raw %}

```nunjucks
{% set t = collections.translationsMap[translationKey] %}
{% if t %}
	{% for lang, entry in t %}
		<link rel="alternate" hreflang="{{ entry.lang }}" href="{{ entry.url }}">
		{% if entry.isDefaultLang %}
			<link rel="alternate" hreflang="x-default" href="{{ entry.url }}">
		{% endif %}
	{% endfor %}
{% endif %}
```

{% endraw %}

---

## Peer deps

None. Uses Eleventy's built-in `I18nPlugin`.

---

## See also

- [[site-settings | Site settings]] for `defaultLanguage` and `languages`.
- [[page-context | Page context]] for where the language fields surface on the per-page object.
- [[filters | Filters]] for the i18n filter signatures.
- [[head | head module]] for the hreflang consumer.
- [[sitemap | sitemap module]] for per-language sitemaps.
- [[multilingual-baseline-site | Tutorial: multilingual site]]
- [[multilingual-index | How-to: multilingual index]]
