jbscript.dev

Home

Updating browser extension translation strings

After adding support for Internationalization to a browser extension, if you’re lucky — and if it’s open source — users who like it might submit a translation for their language.

That’s great (and thanks!), but now when adding new features, you’ll need to create translations for each language your extension supports and put them in the correct messages.json file under the correct name, which involves a lot of jumping between files and copy-pasting JSON and translations.

At the time of writing, Control Panel for Twitter supports 6 languages, with a little over 100 translation strings, and has had a few releases with missed translations or mistranslations due to manual editing errors (thankfully, web-ext’s validation can catch more serious issues with your extension’s locale JSON files before you publish a bad version).

Automating the error-prone part

I’ve written a Node.js script to automate the most error-prone part of this process; given a JSON file with new or changed translations, the script will plug them into the appropriate messages.json (and sort the translation keys alphabetically while it’s in there, because why not?).

An example of a translation.jsons file, where each top-level object contains the translation strings for each langauge:

{
  "showBlueReplyVerifiedAccountsLabel": {
    "en": "Show \"verified\" accounts",
    "es": "Mostrar cuentas \"verificadas\"",
    "fr": "Afficher les comptes \"vérifiés\"",
    "ja": "「認証済み」アカウントを表示",
    "ko": "\"인증됨\" 계정 표시",
    "zh_CN": "显示\"已验证\"账号"
  }
}

The Node.js script (also available here on GitHub):

const fs = require('fs')

function sortProperties(locale) {
  let entries = Object.entries(locale)
  entries.sort(([a], [b]) => {
    if (a < b) return -1
    if (a > b) return 1
    return 0
  })
  return Object.fromEntries(entries)
}

let translationsJson = JSON.parse(fs.readFileSync('./translations.json', 'utf-8'))
let localeMessagesJson = new Map()

for (let [messageProp, translations] of Object.entries(translationsJson)) {
  for (let [localeCode, message] of Object.entries(translations)) {
    if (!localeMessagesJson.has(localeCode)) {
      localeMessagesJson.set(
        localeCode,
        JSON.parse(
          fs.readFileSync(`../_locales/${localeCode}/messages.json`, 'utf-8')
        )
      )
    }
    let messagesJson = localeMessagesJson.get(localeCode)
    messagesJson[messageProp] = {...messagesJson[messageProp], message}
  }
}

for (let [localeCode, messagesJson] of localeMessagesJson.entries()) {
  fs.writeFileSync(
    `../_locales/${localeCode}/messages.json`,
    JSON.stringify(sortProperties(messagesJson), null, 2),
    'utf8'
  )
}

Generating new translations with ChatGPT

I’ve had mixed experiences using ChatGPT for translations in the past (like it hallucinating new keys and strings when given an entire JSON file to translate), but generating initial versions in bulk, checking them against other translation services and having them all in one file is still a win.

Here’s the ChatGPT prompt I used to generate the example above:

Translate the “en” props in the following JSON from English to the languages represented by the following additional language codes:

es, fr, ja, ko, zh_CN

Put the translated versions in their corresponding language code alongside the “en” version.

This is the JSON:

{
  "showBlueReplyVerifiedAccountsLabel": {
    "en": "Show \"verified\" accounts"
  }
}

And the completion:

Here is the JSON with the “en” prop translated into the specified languages:

{
  "showBlueReplyVerifiedAccountsLabel": {
    "en": "Show \"verified\" accounts",
    "es": "Mostrar cuentas \"verificadas\"",
    "fr": "Afficher les comptes \"vérifiés\"",
    "ja": "「認証済み」アカウントを表示",
    "ko": "\"인증됨\" 계정 표시",
    "zh_CN": "显示\"已验证\"账号"
  }
}

I’ve added the translations for “es” (Spanish), “fr” (French), “ja” (Japanese), “ko” (Korean), and “zh_CN” (Simplified Chinese) alongside the “en” version.

created