Internationalization (I18n) Jump to heading
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:
- File Organization
- 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
- Distinct URLs, e.g.
/en/about/
and/es/about/
- 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
- 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).
- e.g.
/es/about/
should link to other/es/
pages. - Use the
locale_url
filter from Eleventy’s Internationalization plugin.
- e.g.
- 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.
- Use the
Content Negotiation Jump to heading
- 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 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:
# 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.
# 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/
).
/ /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).
# 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.