In working on an upcoming opus how we use and don't use Django, I realized that despite the massive success we've had with our homegrown approach to localization, we haven't actually written about it anywhere.
Why roll our own at all?
First, why do we need a custom localization approach at all? To answer this, you need to know a little bit more about Buttondown's structure, which is that we use Django as the core application server for the author- and subscriber-facing app, but use a combination of a Vue SPA to power more interactive bits like the subscribe form itself rather than just vanilla Django. This means we have a multi-locale app which needs to gracefully and easily handle localization across a number of different surfaces: the Vue front end, the conventional Django backend, and arbitrary templates not rendered in the request/response lifecycle, such as transactional emails.
In addition, we wanted to very, very, very aggressively bias towards a solution that had two characteristics: 1, extreme maintainability, because the idea of increasing the difficulty of modifying the app by a significant amount would make localization probably a negative-ROI endeavor; and 2, make it impossible to break localization — to ship references to localization strings that didn't actually exist.
What we did
The solution we ended up with is a fairly conventional one. Look at the shape of the resultant string file. And yes, it is one big string file. And it is open sourced, where keys represent the namespace of each unique string, and the value is a dict of locale to the translation for that locale:
And all we need is a template tag on the backend:
and a wrapper component on the front end to pull from each of these:
Really nothing fancy. The backend tag and the frontend wrapper both read from the same file — the frontend imports it directly into the bundle, and the backend loads it once at import time into a module global, which is what lets a transactional email fired from a background worker use the exact same code path as a template rendered mid-request.
But what's nice about this unfancy approach is that, because you're not really doing anything magical, we can enforce our constraints just like we would enforce any other constraint in CI: no missing strings, no malformed strings, and so on.
My favorite is the check that nothing goes unused, because it's exactly as dumb as you'd hope: it concatenates the entire codebase into one blob and asserts every key appears somewhere in it.
JSON is portable and legible, and those two properties are increasingly underrated these days!
What's missing
There are two things we deliberately chose not to do as part of this:
- Buttondown separates its codebase into the subscriber-facing app (i.e., what subscribers see) and the author-facing app (i.e., the dashboard authors use to write emails and manage subscribers, and so on). We made a very conscious choice to not localize the latter. Not only because it is much larger than the former, but also because doing so would essentially signal that Buttondown was a great fit for non-English speaking authors. Frankly, this isn't true. We don't have localized docs, our support staff only speaks English, and so on. This was definitely the right choice to make, though it's often somewhat tricky to communicate to users that author-facing versus subscriber-facing dichotomy is one that is more obvious to us as a team than it is to an end user.
- We don't actually have any tooling to help situate the translation strings within the context in which they're shown to end users, which means that outside localizers kind of have to wing it and just make some assumptions based on the name of the key and the English translation. We've got some plans to build an internal tool to make this nicer (hi, Steph!)
What happened
My resistance to building out localization was a mistake, and I wish we had done it even sooner. At the time of this writing, 75% of paid authors specify a non-English locale, and over 40% of subscribers are based in non-English speaking countries. Locale expansion has been, to put on my MBA hat for a moment, a huge growth lever for the business.
What's more: because we can open source our translation strings, we get crowdsourced localization! In the past quarter alone we've received four new locales and two "bug fixes" in the form of better strings, because our users are just that lovely.
