Skip to navigation Skip to main content
Eleventy
Eleventy Documentation
Stable
3.0.0
Toggle Menu
Eleventy 1.93s
Astro 22.90s

Internationalization (I18n) Added in v2.0.0

Contents

INFO:
Required reading: the Eleventy docs page on Internationalization (I18n) provides crucial context on project organization and URL strategies for multi-language projects. Please review it before continuing on here.

Utilities to manage pages and linking between localized content on Eleventy projects.

Note that this plugin specifically helps you manage links between content but does not localize that content’s strings, numbers, dates, etc. You’ll likely want to pick a third-party library for this! A few popular choices include eleventy-plugin-i18n, rosetta, i18next, y18n, intl-messageformat, and LinguiJS.

Installation

The Internationalization (i18n) plugin is bundled with Eleventy and does not require separate installation. Available in version v2.0.0 or newer.

If you don’t yet have an Eleventy project, go through the Get Started Guide first and come back here when you’re done!

Add to your configuration file

eleventy.config.js
import { EleventyI18nPlugin } from "@11ty/eleventy";

export default function (eleventyConfig) {
eleventyConfig.addPlugin(EleventyI18nPlugin);
}
module.exports = async function (eleventyConfig) {
const { EleventyI18nPlugin } = await import("@11ty/eleventy");

eleventyConfig.addPlugin(EleventyI18nPlugin);
}
Expand to see the full list of advanced options
eleventy.config.js
import { EleventyI18nPlugin } from "@11ty/eleventy";

export default function (eleventyConfig) {
eleventyConfig.addPlugin(EleventyI18nPlugin, {
// any valid BCP 47-compatible language tag is supported
defaultLanguage: "", // Required, this site uses "en"

// Rename the default universal filter names
filters: {
// transform a URL with the current page’s locale code
url: "locale_url",

// find the other localized content for a specific input file
links: "locale_links",
},

// When to throw errors for missing localized content files
errorMode: "strict", // throw an error if content is missing at /en/slug
// errorMode: "allow-fallback", // only throw an error when the content is missing at both /en/slug and /slug
// errorMode: "never", // don’t throw errors for missing content
});
}
module.exports = async function (eleventyConfig) {
const { EleventyI18nPlugin } = await import("@11ty/eleventy");

eleventyConfig.addPlugin(EleventyI18nPlugin, {
// any valid BCP 47-compatible language tag is supported
defaultLanguage: "", // Required, this site uses "en"

// Rename the default universal filter names
filters: {
// transform a URL with the current page’s locale code
url: "locale_url",

// find the other localized content for a specific input file
links: "locale_links",
},

// When to throw errors for missing localized content files
errorMode: "strict", // throw an error if content is missing at /en/slug
// errorMode: "allow-fallback", // only throw an error when the content is missing at both /en/slug and /slug
// errorMode: "never", // don’t throw errors for missing content
});
}

Usage

This plugin provides two universal filters (Nunjucks, Liquid, 11ty.js) and one addition to the page variable.

page.lang

Adding the i18n plugin to your project will make page.lang available to your templates. This represents the language tag for the current page template, and will default to the value you’ve passed to the plugin via defaultLanguage above.

Check out the rest of the data available on the page object.

locale_url Filter

Accepts any arbitrary URL string and transforms it using the current page’s locale. Works as expected if the URL already contains a language code. This is most useful in any shared code used by internationalized content (layouts, partials, includes, etc).

Filename /en/index.njk
<a href="{{ "/blog/" | locale_url }}">Blog</a>
<!-- <a href="/en/blog/">Blog</a> -->
Filename /es/index.njk
<a href="{{ "/blog/" | locale_url }}">Blog</a>
<!-- <a href="/es/blog/">Blog</a> -->
Filename /en/index.liquid
<a href="{{ "/blog/" | locale_url }}">Blog</a>
<!-- <a href="/en/blog/">Blog</a> -->
Filename /es/index.liquid
<a href="{{ "/blog/" | locale_url }}">Blog</a>
<!-- <a href="/es/blog/">Blog</a> -->
Filename /en/index.11ty.js
export default function (data) {
return `<a href="${this.locale_url("/blog/")}">Blog</a>`;
// returns <a href="/en/blog/">Blog</a>
};
Filename /es/index.11ty.js
export default function (data) {
return `<a href="${this.locale_url("/blog/")}">Blog</a>`;
// returns <a href="/es/blog/">Blog</a>
};
Filename /en/index.11ty.cjs
module.exports = function (data) {
return `<a href="${this.locale_url("/blog/")}">Blog</a>`;
// returns <a href="/en/blog/">Blog</a>
};
Filename /es/index.11ty.cjs
module.exports = function (data) {
return `<a href="${this.locale_url("/blog/")}">Blog</a>`;
// returns <a href="/es/blog/">Blog</a>
};

If the link argument already has a valid language code, it will be swapped. The following all return /en/blog/ when rendered in /en/* templates (or /es/blog/ in /es/* templates):

  • {{ "/blog/" | locale_url }}
  • {{ "/en/blog/" | locale_url }}
  • {{ "/es/blog/" | locale_url }}
INFO:
It’s important to note that this filter checks for the existence of the file in the target locale. If the specific content file in the target locale does not exist, an error will be thrown! You can change this behavior using the plugin’s errorMode option (see advanced usage above).

It’s unlikely that you’ll need to but you can override the root locale with a second argument:

Filename /en/index.njk
<a href="{{ "/blog/" | locale_url("es") }}">Blog</a>
<!-- <a href="/es/blog/">Blog</a> -->
Filename /en/index.liquid
<a href="{{ "/blog/" | locale_url: "es" }}">Blog</a>
<!-- <a href="/es/blog/">Blog</a> -->
Filename /en/index.11ty.js
export default function (data) {
return `<a href="${this.locale_url("/blog/", "es")}">Blog</a>`;
// returns <a href="/es/blog/">Blog</a>
};
Filename /en/index.11ty.cjs
module.exports = function (data) {
return `<a href="${this.locale_url("/blog/", "es")}">Blog</a>`;
// returns <a href="/es/blog/">Blog</a>
};

Returns an array of the relevant alternative content for a specified URL (or, defaults to the current page). The original page passed to the filter is not included in the results. Each array entry is an object with url, lang, and (localized) label properties, for example:

[{ "url": "/es/blog/", "lang": "es", "label": "Español" }]

“This page also available in:” Example

Renders as:

This page is also available in <a href="/es/blog/" lang="es" hreflang="es">Español</a>

<link rel="alternate"> Example

Here’s another example in a layout file.

The href attributes here must be fully qualified (include the full domain with the protocol). Read more on the Google Search Central documentation.

INFO:
The top level lang data property used here is most commonly set by you in the data cascade. For example: /en/en.json with {"lang": "en"} and /es/es.json with {"lang": "es"}.
Filename _includes/mylayout.njk
{# `{{lang}}` must be set by you in the data cascade, see above note #}
<!doctype html>
<html lang="{{lang}}">
<head>
<link rel="alternate" hreflang="{{lang}}" href="{{page.url}}">
{% for link in page.url | locale_links %}
<link rel="alternate" hreflang="{{link.lang}}" href="https://www.11ty.dev{{link.url}}">
{% endfor %}
Filename _includes/mylayout.njk
<!doctype html>
{% comment %} `{{lang}}` must be set by you in the data cascade, see above note {% endcomment %}
<html lang="{{lang}}">
<head>
<link rel="alternate" hreflang="{{lang}}" href="{{page.url}}">
{% assign links = page.url | locale_links %}
{%- for link in links %}
<link rel="alternate" hreflang="{{link.lang}}" href="https://www.11ty.dev{{link.url}}">
{%- endfor -%}
Filename /_includes/mylayout.11ty.js
export default function (data) {
let links = this.locale_links(data.page.url);
// side note: url argument is optional for current page

// `${data.lang}` must be set by you in the data cascade, see above note
return `
<!doctype html>
<html lang="
${data.lang}">
<head>
<link rel="alternate" hreflang="
${data.lang}" href="{{data.page.url}}">
${links
.map((link) => {
return ` <link rel="alternate" hreflang="${link.lang}" href="https://www.11ty.dev${link.url}">`;
})
.join("\n")}

`
;
};
Filename /_includes/mylayout.11ty.cjs
module.exports = function (data) {
let links = this.locale_links(data.page.url);
// side note: url argument is optional for current page

// `${data.lang}` must be set by you in the data cascade, see above note
return `
<!doctype html>
<html lang="
${data.lang}">
<head>
<link rel="alternate" hreflang="
${data.lang}" href="{{data.page.url}}">
${links
.map((link) => {
return ` <link rel="alternate" hreflang="${link.lang}" href="https://www.11ty.dev${link.url}">`;
})
.join("\n")}

`
;
};

Using with get*CollectionItem filters

The getPreviousCollectionItem, getNextCollectionItem and getCollectionItem filters all provide a mechanism to retrieve a specific collection item from a collection.

The i18n plugin modifies the behavior of these filters to prefer a collection item in the current page language’s without requiring any changes to your project.

For example, assume that English (en) is the default language for your project. Assume we’ve configured all of the blog posts in /en/blog/*.md to have the post tag, placing them into a post collection. Now you want to provide alternative localized versions of this blog post, so you create the following files:

  • /es/blog/my-blog-post.md
  • /ja/blog/my-blog-post.md

Using the above filters on these localized templates will automatically prefer /en/blog/my-blog-post.md as the root collection item when navigating the collection. This allows you to do things like:

Syntax Nunjucks
{%- set nextPost = collections.post | getNextCollectionItem %}
{%- if nextPost %}<a href="{{ nextPost.url | locale_url }}">Next post</a>{% endif %}

This will prefer a localized version of the next post’s URL (Spanish pages will prefer linking to other pages in Spanish, when available). If a localized version does not exist, it will fall back to the default language instead.


Other pages in Official Plugins: