Stand with Ukraine 🇺🇦
Eleventy
The possum is Eleventy’s mascot

Eleventy Documentation

Menu

Internationalization (I18n) Jump to heading

This page is a companion to the Internationalization (I18n) plugin page.
Expand for contents

There are two big decisions you’ll need to make up front when working on an Eleventy project that serves localized content:

  1. File Organization
  2. URL Style

Note that Eleventy works with a variety of third-party JavaScript libraries for organizing and localizing strings, numbers, dates, etc. A few popular choices include eleventy-plugin-i18n, rosetta, i18next, y18n, intl-messageformat, and LinguiJS.

How to organize your files Jump to heading

Most folks like to create a folder for each locale they want to serve in their project. This is by-far the most popular approach for most folks (and works best with Eleventy’s Data Cascade and default permalink setup too).

This usually involves a directory structure something like this:

📁 en -> 📄 about.html
📁 es -> 📄 about.html
📁 de -> 📄 about.html
📁 ja -> 📄 about.html
📂 and so on…

This allows you to use Eleventy’s Data Cascade with directory data files to set data for the entire language directory. For example, /en/en.json with {"lang": "en"} and /es/es.json with {"lang": "es"} will make the lang variable available to all templates (even deeply nested) inside of the directory.

Alternatively (and much less popularly) some projects like to denote the language code in each individual file name:

📄 about.en.html
📄 about.es.html
📄 and so on…

The latter method is more unwieldy and not recommended (but still achievable with some permalink wrangling).

Choose your URL style Jump to heading

  1. Distinct URLs, e.g. /en/about/ and /es/about/
  2. Content negotiation, e.g. /about/

This choice is a bit more contentious. There are benefits and drawbacks to both methods. Some folks even mix the two approaches within a single project!

Distinct URLs Jump to heading

Content Negotiation Jump to heading

Example Netlify Redirects Jump to heading

To implement the above methods, you can use Netlify’s Redirects and Rewrites features (or Netlify Edge Functions for more advanced use cases).

In the examples below, English (en) is the default fallback language and Spanish (es) is an additionally supported language. To add more languages, repeat each entry for Spanish (es) and change the language code.

Content Negotiation on all pages Jump to heading

No language codes in URLs:

View: _redirects netlify.toml
# Redirect any URLs with the language code in them already
/es/*   /:splat     301!
/en/*   /:splat     301!

# Show the language-specific content file
/*      /es/:splat  200   Language=es
/*      /en/:splat  200
# Redirect any URLs with the language code in them already
[[redirects]]
from = "/es/*"
to = "/:splat"
status = 301
force = true

[[redirects]]
from = "/en/*"
to = "/:splat"
status = 301
force = true

# Show the language-specific content file
[[redirects]]
from = "/*"
to = "/es/:splat"
status = 200
conditions = {Language = ["es"]}

[[redirects]]
from = "/*"
to = "/en/:splat"
status = 200

Distinct URLs for all pages Jump to heading

URLs should always have language codes in them.

These redirects are specifically for content that is missing a language code in the URL (e.g. / redirect to /en/). To avoid a redirect on the home page (recommended) use Content Negotiation on / only.

View: _redirects netlify.toml
# Important: Per shadowing rules, URLs for the language-specific
# content files are served without redirects.

# Redirect for end-user’s browser preference override
/*  /es/:splat  302   Language=es

# Default
/*  /en/:splat  302
# Important: Per shadowing rules (force = false) URLs for the
# language-specific content files are served without redirects.

# Redirect for end-user’s browser preference override
[[redirects]]
from = "/*"
to = "/es/:splat"
status = 302
conditions = {Language = ["es"]}

# Default
[[redirects]]
from = "/*"
to = "/en/:splat"
status = 302

Make sure you read the Netlify shadowing rules to understand why /es/* and /en/* URLs are not redirected.

Content Negotiation on / only Jump to heading

Every URL but the home page should have a language codes in it.

This uses content negotiation for your home page and distinct URLs for everything else (it uses the redirects from both methods above). This mixed approach has the benefit of avoiding a top level redirect on your home page (e.g. from / to /en/).

View: _redirects netlify.toml
/   /es/        200   Language=es
/   /en/        200
/*  /es/:splat  302   Language=es
/*  /en/:splat  302
# Content negotiation for home page
[[redirects]]
from = "/"
to = "/es/"
status = 200
conditions = {Language = ["es"]}

# Content negotiation for home page
[[redirects]]
from = "/"
to = "/en/"
status = 200

# Redirect for end-user’s browser preference override
[[redirects]]
from = "/*"
to = "/es/:splat"
status = 302
conditions = {Language = ["es"]}

# Default
[[redirects]]
from = "/*"
to = "/en/:splat"
status = 302

Distinct URLs using Implied Default Language Jump to heading

Only non-default languages should include the language code in the URLs.

This approach leaves off the language code in URLs for the default language. Non-default languages include the language code in the URL (e.g. / for English and /es/ for Spanish).

View: _redirects netlify.toml
# Redirect any URLs with the language code in them already
/en/*   /:splat     301!

# Important: Per shadowing rules, URLs for the
# _non-default_ language-specific content files
# are served without redirects.

# Redirect for end-user’s browser preference override
/*      /es/:splat  302   Language=es
/*      /en/:splat  200
# Redirect any URLs with the language code in them already
[[redirects]]
from = "/en/*"
to = "/:splat"
status = 301
force = true

# Important: Per shadowing rules, URLs for the
# _non-default_ language-specific content files
# are served without redirects.

# Redirect for end-user’s browser preference override
[[redirects]]
from = "/*"
to = "/es/:splat"
status = 302
conditions = {Language = ["es"]}

# Default
[[redirects]]
from = "/*"
to = "/en/:splat"
status = 200

Community Resources Jump to heading

Internationalization has some really great community resources that served as the inspiration for both this and the official i18n Plugin.


Other pages in Working with Templates: