Tech blog

Catching translation issues early

Published 5 April 2022

Building applications in the European market often means that you sooner or later will have to translate your application to more than one language. This causes changes in workflows, processes around building/changing functionality, how you think about your UI and a lot of more things.

With this comes a certain set of challenges such as ensuring that you are consistently translating your UI, entities and make sure that you don’t forget it in a stressful moment.

At BoardClic we’re making use of the brilliant gettext package for Elixir (which is built into Phoenix). Except being a great tool in general it makes it possible for us to build automated tools that helps us ensure that we’re not forgetting to translate a string, causing subpar experiences for our users:

mix gettext.extract --check-up-to-date

Implemented in gettext 0.19.0, this little nifty mix task will make certain that all strings passed to any of the gettext-functions are properly extracted into the pot/po-files.

We implemented this as a part of our CI workflow upon release and it has worked wonders for us. The only thing to remember is that some strings (such as custom error messages in ecto) will have to be marked for extraction. It’s easily done by wrapping them in a dgettext_noop/2.

Test verifying that translations doesn’t have an empty msgstr

While it’s great to be able to ensure that all translations has been extracted successfully it’s also great if we could ensure that they are in fact also translated.

Our way of making sure of that is to have a test that looks at your translations and simply checks if any of them are missing:

defmodule TranslationIntegrityTest do
  use ExUnit.Case, async: true

  @locales Application.compile_env!(:myapp, [MyAppWeb.Gettext, :locales])
  @default_locale Application.compile_env!(:myapp, [MyAppWeb.Gettext, :default_locale])
  @translation_files ~w[default errors]

  for locale <- @locales -- [@default_locale] do
    describe "`#{locale}` translations" do
      for file_name <- @translation_files do
        @translation Gettext.PO.parse_file!("priv/gettext/#{locale}/LC_MESSAGES/#{file_name}.po")

        test "translates #{file_name} text" do
          untranslated_translations = Enum.filter(@translation.translations, &(&1.msgstr == [""]))

          unless Enum.empty?(untranslated_translations) do
            missing_translations =


  def build_missing_translations_error_string(untranslated_translations, file_name) do
    translation_str =
      Enum.map_join(untranslated_translations, "\n", fn translation ->

    The following strings were not translated:


In case there is a missing translation we’d end up with this helpful message:

1) test `sv_SE` translations translates errors text (TranslationIntegrityTest)
   The following strings were not translated:

   "is invalid"

   code: flunk(missing_translations)
     test/translation_integrity_test.exs:23: (test)

Automating translations

Since not everyone in our team are native Swedes we don’t want to enable them to build features on their own without the need to bring someone else into the mix whenever there is a string changed/added somewhere. To aid this we’ve built our own little auto-translator on top of gettext which adds a mix task that can enable a faster workflow by simply filling in the blanks.

While this isn’t a true solution and quality of the translations needs to be checked we’re still handling parts of this in pull requests. However it’s always nicer to have something that can be improved than being forced to add the translations yourself as a reviewer.

Since there is still improvements to be made on our auto-translator we’re going to smoothen the edges a bit before we share it.

Closing words

We hope that our workflow around translations can help you and your team even if it’s just inspiration. We’re very happy with the extra layer of quality assurance we get from the described tooling.

Johan Tell
Johan Tell