Translations

The panel has catalogs for English, Dutch (nl), German (de) and French (fr). It shows the language that the browser asks for, until the person selects a different language in the language list in the site header. The list is a plain form that posts to the set_language view of Django, and it needs no account and no script. The view stores the choice in a cookie.

The translations are gettext catalogs in the repository, one directory for each language:

panel/src/panel/locale/<language>/LC_MESSAGES/django.po
panel/src/panel/locale/<language>/LC_MESSAGES/djangojs.po

djangojs.po holds the strings of the JavaScript files in static/, and django.po holds all other strings.

The repository keeps only the .po files. The Nix package compiles them into the .mo files that Django reads, and the dev shell compiles them when it starts. Git ignores the .mo files.

A machine made the first Dutch, German and French translations. A native speaker must review them.

Add or change a string

Write the string in English in the source, and mark it for translation:

  • Templates: {% load i18n %}, then {% translate "Save" %}. For a string with values or HTML, use {% blocktranslate trimmed with name=object.name %}Configure {{ name }}{% endblocktranslate %}. Keep values that are not prose, for example a URL or a JSON example, outside of the string. A blocktranslate block cannot hold a different block tag: give a {% url %} value a name with as before the block.
  • Inline scripts: {{ _("Saved.")|escapejs }}. For a value, write {name} in the string, and give the value to window.panelNotice.fill in the script.
  • JavaScript files in static/: the global function gettext("Saved."). For a value, use interpolate(gettext("Open %(name)s."), { name: value }, true). base.html loads these functions from the javascript_catalog view before the scripts. A value that goes into HTML must be escaped first.
  • The script of the deployment form (panel/forms-island-adapter): a data-label-* attribute on the <body> of base.html, with a {% translate %} value. The translation must not have a double quote.
  • Python: gettext from django.utils.translation in views, and gettext_lazy for strings that Python evaluates at import time, for example form labels and choices.

The test panel/tests/test_unmarked_strings.py fails on text without a mark in the places where text goes to the user: the text and the visible attributes of a template, the output calls of JavaScript and HTML in a JavaScript string, and in Python the messages, the validation errors, the mails, and the labels and help texts. It does not see text that goes to the user through a variable. Text that must stay in English goes in NOT_TRANSLATED in that test, with its reason.

A management command has no request and thus no language of a person, so its mails are in English.

Then update the catalogs from the dev shell:

panel-makemessages

This adds the new strings to each .po file with an empty translation, and removes the strings that the source no longer has. Fill in the msgstr of each new string, and commit the .po files. When a string changes, panel-makemessages can keep the translation of a similar string and add a #, fuzzy line. Examine that translation and remove the line. A string with an empty msgstr or a #, fuzzy line shows in English.

Each translation must keep the placeholders (%(name)s) and the HTML tags of its English string.

The check panel-translations fails when the .po files are not the same as the output of panel-makemessages, and when an entry has an empty msgstr or a #, fuzzy line. Thus, run panel-makemessages after each change to a marked string, and fill in the translations of each language. The check ignores the line wrap, the POT-Creation-Date and the references to the source. It sees only marked strings: it does not find a string that is not marked yet.

panel-makemessages writes the files with --no-wrap: it breaks a line only at a newline in a string. A tool that edits the files must write them with the same setting, so that its changes do not wrap the lines again. In Weblate, set the file format parameter "Long lines wrapping" (po_line_wrap) of the component to "Only wrap lines at newlines".

Add a language

  1. Add the language code and its name, in that language, to LANGUAGES in panel/src/panel/settings.py.
  2. Make its directory: mkdir -p panel/src/panel/locale/<language>/LC_MESSAGES.
  3. Run panel-makemessages. This writes the two .po files of the language.
  4. In the header of each file, set Language, Language-Team and the Plural-Forms of the language, and remove the #, fuzzy line above msgid "".
  5. Translate the strings, and commit the files.

The tests test_each_language_has_a_compiled_catalog and test_each_language_has_a_script_catalog fail for a language in LANGUAGES that has no catalog.

What is not translated

The templates, the JavaScript files in static/, and the strings of the views and forms that a person reads, are marked for translation. The deployment service sends the titles, descriptions and confirmations of its actions, and the reason when it disables an action, in English. The file api/src/fediversity_api/procedures.py marks these strings with gettext_noop, panel-makemessages reads that file, and the panel translates the text when it shows the text. Thus the check panel-translations also fails when one of these strings changes and the catalogs do not. These strings show in English:

  • The strings of the deployment form (nix-module-form).
  • The labels and descriptions of the deployment options, and the text of the operations of an application. They come from the NixOS modules in core/.
  • The error that the deployment service sends when it refuses a request, and the results in the history of an account.
  • The strings of the administration interface, for example the help text of a model.
  • The strings that no person reads in the panel: the answers to the callbacks of Centrifugo and of the deployment service, and the log lines.