Automating Internationalization Workflow in Emacs

A lot of projects we work on at MojoTech require the UI to be accessible in many different locales. Recently we have been working with a global retailer to build an internationalized React client for their B2B ordering platform. One of the core requirements was for this to be accessible throughout Europe and the United States. This includes translating strings to different languages, and formatting numbers, dates, and currencies to the user’s preferred locale.

We decided to use react-intl to handle internationalization in our React app. The formatting of dates, numbers, and currencies was relatively straightforward. With a user-specified locale preference, the react-intl APIs can easily format these. Handling the translation of strings to many different languages was a bit more involved.

Using react-intl to Render Strings

To render a string with react-intl, you specify a key in your UI component and map that key to a string in a Javascript object supplied to the react-intl IntlProvider. The IntlProvider wraps the app and uses React context for the components to hook into the translations provided. In our case we had separate JSON files per locale to hold the key-value pairs. Depending on the user’s locale preference, we would provide the corresponding JSON file to the IntlProvider. Here is an example of the work involved to render a string:

// dashboard.js
<div>
  ...
  <FormattedMessage id='dashboard.carts.quantity-label' />
  ...
</div>

// en-US.json
{ ..., "dashboard.carts.quantity-label": "Quantity", ... }

// nl-NL-google.json
{ ..., "dashboard.carts.quantity-label": "Aantal Stuks", ... }

For our global retailer client, we adopted the following process to handle new strings in the app:

  1. Render the FormattedMessage tag with a unique key
  2. Add the key and English string to the en-US.json file
  3. Use Google Translate to add the key and translated Dutch value to the nl-NL-google.json file.
  4. Down the line, a human translation service would translate these and move the final translations to nl-NL.json and the rest of the locales.

At all times the app would be presentable in English, and in Dutch via Google Translate. While the Google translations aren’t necessarily accurate, it’s a good first step to test the UI in multiple languages. Periodically the strings would be sent to a translation service to be translated to Dutch and the other languages.

Let’s explore how our workflow for managing translations evolved.

Adding Translations with Google Translate Web

As we started to build the UI, I got into the habit of always having Google Translate open, ready to take strings to translate to Dutch. It quickly became apparent though that it was somewhat time-consuming to switch between the browser and text editor, and to a few different files, to add each string in the app. It became a chore for something we do dozens of times a day.

null

Adding Translations with Google Translate in Emacs

A few of us on the project used Emacs and I was glad to stumble across google-translate, an “Emacs Interface to Google Translate”. With this I could highlight a string in visual mode, run the google-translate-at-point command (bound to SPC x g t in Spacemacs), and have the translation displayed in a new buffer. From there I could copy the translation and add it to the Dutch file, add the key and English string to the English file, and add the FormattedMessage tag to the UI component. This was a good improvement, I could stay within Emacs and have the translation part be done in a few keystrokes.

null

This became muscle memory after a while and adding a new string to the app would now take maybe 30 seconds. Not bad, but for something we do many times a day, it is still a chore. Three separate files need to be updated — the UI component and the two translation files. And I need to remember the key to use across all three files, and make sure the entries are sorted by key. In order to iterate quickly on the UI I would often leave the translating for the end as cleanup, mundane work. When work becomes frequent and predictable, chances are it can be simplified with some automation. I had already been using the google-translate-at-point command to do the translation, so I thought to handle the rest with Emacs as well.

Adding Translations with Custom Emacs Command

The process for adding a string to the UI could be expressed as a series of Emacs commands. Before, I could highlight a string in visual mode and have the translation open in a buffer. Now I wanted to be able to highlight the string in visual mode, run a command, and have the following actions performed:

  1. Prompt the user for a key
  2. Add the key and highlighted region to the English file
  3. Translate the highlighted region and add it to the Dutch file
  4. Replace the highlighted region with a FormattedMessage tag including the key

The root of the command implementation is shown below, written in Emacs Lisp (Elisp). The full source can be viewed here.

(defun ri-translate ()
  (interactive)
  (let ((key (read-string "Specify a key: ")))
    (message "Adding translation entries...")
    (ri-translate--add-english-entry key)
    (ri-translate--add-dutch-entry key)
    (ri-translate--insert-formatted-message key)
    (message "Added translation entries.")))

This roughly matches the process I described above. interactive allows the command to be run on the highlighted region. When we run the command it prompts for a key using read-string and we store that in the local variable key. Next we add the entry to the English file using the key, add the entry to the Dutch file using the key, and replace the highlighted region with the FormattedMessage tag containing the key. Additionally, we log a couple of messages to the mini buffer to show when the process starts and finishes.

To add the entry to the English file, we need to push the new key-value pair to the existing JSON in the file. To do this in Elisp we can read the JSON file into a hash, convert the hash to a list of key-value pairs, push the new key-value pair, sort the list by key, convert back to a hash, and write that back to the JSON file. This may seem like a lot of steps but it’s essentially what we did by hand before: take a key and value, open the translation file, insert the entry sorted by key, and save/prettify the file. The same commands used to do this by hand are the same used in the Elisp program: find-file, save-buffer, prettify-js, etc. I have some of these bound to key combinations, and you can quickly find the command of a key combination using describe-key.

Adding to the Dutch file we follow the same process, only first we need to translate the highlighted region. We can use the same google-translate-at-point command to open a buffer with the translation, switch to that buffer, parse it for the translation, close the buffer, and push the entry to the Dutch file. As mentioned, the full implementation of the functions can be viewed here.

Now when adding a new string to the UI, what used to take 30 seconds can now be done automatically in a few keystrokes. The only thing to think about now is what to name the key.

null

Let Your Text Editor Work for You

I find getting to know your text editor to be one of the biggest productivity boosts. Start with using key combinations for actions you perform often. If you notice things in your workflow that are frequent and have a predictable process, see if there are any editor plugins you can use to make that easier. While this post specifically referred to Emacs, most text editors have a plugin system with open source packages. And if an existing package can’t quite fit to your workflow, hopefully this post inspires you to try writing your own.

Having written this command to manage translations, I’m now more observant of my workflow, looking for common patterns that could benefit from automation. For example, I noticed that I’m always typing out PropTypes definitions, probably as often as managing string translations. Typing PropTypes.number.isRequired might not seem like a lot but it’s a decently long string with different casing that is prone to mistyping. Why not highlight the prop name, have Emacs prompt for a prop type from a list, ask if it should be required, and insert the definition. It’s also important not to get overwhelmed trying to cover every case — if a custom command can cover most cases and you have to do the more fine-grained ones by hand, that is still useful.

Have any custom editor plugins or ideas where one would be useful? Send us a tweet at @MojoTech!