Custom translations in Eleventy with filters

I was working on a multi-language Eleventy site and I realised that configuring the translations from scratch (by writing a custom filter) would be easier than choosing an existing library.

This is a very bare-bones implementation that doesn’t support pluralization or interpolation, but it was enough for my site.

In .eleventy.js:

// set up the official EleventyI18nPlugin
import { EleventyI18nPlugin } from "@11ty/eleventy";

config.addPlugin(EleventyI18nPlugin, {
defaultLanguage: "en",
filters: {
url: "locale_url",
links: "locale_links",
},
errorMode: "strict",
});

// add a custom filter that takes a language as an argument
config.addFilter("t", function (key, lang) {
// specify the languages in the order of precedence
// lang is an optional argument: it will be used when specified
// otherwise the page language will be used
// english is used here as a default fallback language
const languages = [lang, this.page.lang, "en"];
return translate({ key, languages });
});

Then, in a separate file, I created the translate function:

import pl from "./pl.json" with { type: "json" };
import de from "./de.json" with { type: "json" };
import en from "./en.json" with { type: "json" };
import uk from "./uk.json" with { type: "json" };

const translations = { pl, de, en, uk };

const getNestedKey = (object, key) =>
key.split(".").reduce((a, b) => a?.[b], object);

export const translate = ({ key, languages }) => {
// iterate by the languages (specified in the order of precedence)
for (let i = 0; i < languages.length; i++) {
const language = languages[i];

if (!language) {
continue;
}

const object = translations[language];
const translation = getNestedKey(object, key);

if (translation) {
return translation;
}
}

// return the key if no translation is found
return key;
};

In the nunjucks template I would then use eg. {{ "actions.call" | t }} to use the translation in the page language or {{ "actions.call" | t("de") }} to use the translation in another language.