Localizing an Eleventy website

I’m currently working on a project at work for which we setup a super simple landing page with a few action items, a form to join our beta and some other niceties. It’s a simple page with pure HTML, CSS and a little bit of javascript. There are no images, we just inlined some SVG icons. The deployment is automatic, because I’m me, and the whole process involves copying 3 files to a server that runs nginx. This week I needed to make it available in chinese and it ended up being a pretty fun day at work.

After a quick chat, we decided to avoid fancy server-side shenanigans using the Accept-Language header. I actually don’t think it’s recommended and I personnaly don’t like it. Instead, we wanted to render the landing page in english for the / and /en paths and in chinese at the /cn path. User will be able to switch between the 2 at the bottom of the page with a flag icon or something like that.

For the localization, since the content of the website already existed, my wonderful colleage extracted all of the different strings, grouped them by section and wrote everything into a spreadsheet with the following columns: section, key, english, chinese, comments (to add context for the person in charge of translating). From that, I needed to replace all of the hardcoded content with those keys. I told my colleagues that the only thing that mattered is that the section and the key didn’t change, or I would simply start crying and it would be embarrassing for everybody involved.

The existing version of the website was extremely simple, so every time I had to make some changes to it, I would run npx serve ., which is my go-to solution to spawn a local HTTP server. This works, but it requires to manually refresh the page every time I wanted to check the result of a code change. This was fine for simple modifications, but with 100 different strings to add, I knew it would be annoying. The hot reload feature is not enough on its own to switch to using Eleventy, but I knew that adding a localization mechanism would involve some kind of templating engine, a way to change the rendered content based on a variable, and so on. On top of all that, we used Eleventy to build the Sound Games website and I used it for my dog count website, so I wouldn’t have to learn a new tool.

The first thing I did was writing a little script that would parse the CSV containing all the strings and generate a single JSON file with the following shape:

{
	"en": {
		"nav-by-line": "by Sound Games",
		"nav-badge": "Open Beta",
    },
    "cn": {
		"nav-by-line": "Sound Games出品",
		"nav-badge": "开放测试",
    }
}

With that file ready, I created a custom template loc filter, which also parses markdown using markdown-it to automatically converts inline tags for emphasis and links.

eleventyConfig.addFilter("loc", function (key) {
    const lang = this.ctx.locale || defaultLocale
    
    const translated = this.ctx.localizations[lang][key] || this.ctx.localizations[defaultLocale][key] 
    if (translated) {
        return md.renderInline(translated)
    }
    
    return `##Missing Key ${key}##`
});

You’ll notice that the filter doesn’t throw an error when a key is missing, because it felt more appropriate to see it in the page directly. I’m also using this “locale” variable that is undefined for most pages, in which case we default to english. With that done, all I had to do was creating the missing pages. For this, I moved the content of the homepage into a base template and I created an index.njk the root of the project, another one in a folder titled en and a last one in a folder titled cn, all of which extending that template. In that last folder, I added a cn.json file (which is the 11ty way of defining/overriding data) with the following content:

{ "locale": "cn" }

Finally, I added links at the bottom of the page to allow visitors to switch between english and chinese and… voilà. Because I’d already setup a pipeline for continuous deployment, all that was left to do was adding a step to build the project, configuring the caching of node modules to save 7 seconds of build time and updating the rsync command to copy the content of the _site directory.

I don’t know if that’s how I would approach the localization of a website with more content but if I did, I would probably look into the official documentation since they provide some additional helpers for localized URLs, which we didn’t need here. Overall, it was a fun couple of hours spent on something different and I enjoyed it. I’ll update this post once we’re ready to share our landing page!