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

Internationalization (I18n)

Contents

INFO:
This page is a companion to the Internationalization (I18n) plugin page.

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:

How to organize your files

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

  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

  • Pro: Every piece of content is uniquely addressable, linkable, and cacheable.
  • Pro: Easy to statically host and works with few (if-any) internal redirects.
  • Con: Internal link URLs must be normalized on shared content (navigation and footer links).
  • Con: When a URL mismatches with an end user’s language preference (as specified in a language chooser widget or the Accept-Language request header in the browser), a redirect is suggested (but not required!). This is a subtle but important point that when using URLs the ultimate control is left in the hands of the end user.
    • Use the locale_links filter from Eleventy’s Internationalization plugin to show the available relevant localized content for a specific file.

Content Negotiation

  • Pro: URLs don’t need to be transformed and the appropriate content is selected (via a rewrite) on the server.
  • Pro: Redirects are not necessary to respect end user preferences.
  • Con: Requires some server configuration to handle the Accept-Language header and rewrite correctly.
  • Con: To view another localized version of a piece of content, you will need to rely on the user’s web browser preferences (Accept-Language request header) or implement a language override widget. End users subsequently have less control.

Example Netlify Redirects

To implement the above methods, you can use Netlify’s Redirects and Rewrites features.

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

No language codes in URLs:

# 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

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.

# 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

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/).

/   /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

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).

# 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

From the Community

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


Other pages in Eleventy Projects: