Translate Text in Sphinx Templates and Configurations

Weeks ago when I was playing around with the docs of EFB and the Crowdin translation widget, I realized that the default theme for Sphinx — Alabaster isn’t really doing well in term of translation. It seems like the author isn’t really confident on that (or simply didn’t care since 4 years ago).

As the theme itself is open source, and Sphinx is flexible enough, couldn’t we just translate it ourselves? It turns out that things are not that complicated.

Translate text in theme

This one is pretty straightforward. What we need to do is simply copy the template we want to change to our local directory, wrap the texts with {{ _("") }}, and then add it to a message catalog.

Taking example of the donate section in the sidebar:

alabaster/donate.html on GitHub

Copy this file to your documentation folder as _templates/donate.html and edit it. The only visible text here is the title: “Donate/support”. Wrapping it with the gettext function, we can get:

{% if theme_donate_url or theme_opencollective or theme_tidelift_url %}
<h3 class="donation">{{ _("Donate/support") }}</h3>
{% endif %}

Repeat this on all strings untranslated in your theme.

If this is the first time you include custom templates in the project, you may need to add the _templates folder into your Sphinx config for it to recognise and override your installed theme.

templates_path = ["_templates"]

Once we have everything we need to translate wrapped, sphinx-build -b gettext can automatically recognise these strings and put them in the sphinx catalog. We can then apply our normal translation workflow just like other catalogs.

Translate strings in configuration

Translating text in the configuration is relatively more complicated than the previous one. We need to hook into the workflow of Sphinx for everything to work properly.

Firstly, we need to choose a catalog for our strings in conf.py. It is better to have a new catalog to prevent unnecessary effort of merging catalogs. Reference to the catalog can be obtained through sphinx.locale.get_translation.

from sphinx.locale import get_translation

MESSAGE_CATALOG_NAME = "docs_conf"
_ = get_translation(MESSAGE_CATALOG_NAME)

Then you can use this function to replace strings.

project = _('My Sphinx project')
copyright = _('2020 Eana Hufwe')
author = _('Eana Hufwe')
docs_title = _('My Sphinx Documentation')
description = _('A demo project with Sphinx')

sphinx-build -b gettext cannot extract these strings, so we need to extract it manually.

xgettext -o _build/locale/docs_conf.pot conf.py

If you then translate the catalog and test it locally, everything just seems to work. But if you are using any CI for your docs like ReadTheDocs, these strings are not translated. That is because these CIs relies on Sphinx to build .mo files, and you cannot add extra catalog before .mo are built. To workaround this, we need to hook onto the point before HTML files are generated.

def html_page_context(self, pagename, templatename, context, doctree):
    if not self.catalog_added:
        package_dir = path.abspath(path.dirname(__file__))
        locale_dir = os.path.join(package_dir, 'locale')
        self.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir)
        self.catalog_added = True


def setup(self):
    self.catalog_added = False
    self.connect("html-page-context", html_page_context)

This hook will add the new catalog into the catalogs to consider, and build the .mo file if not available. The property self.catalog_added is added to ensure that the catalog is added only once per run instead of once per HTML file.


Both of the strategies are currently applied to EFB, and is working well both locally and on ReadTheDocs. You can see it live here: https://efb-docs.1a23.studio/


Edit 27 Mar 2020: Added details about templates_path in the config file.


Comments

3 responses to “Translate Text in Sphinx Templates and Configurations”

  1. Arthur Avatar

    Hi and thanks a lot for this tutorial.
    I’m trying to translate strings in my fork of the RTD theme.
    Somehow, strings using the gettext alias {{ _("") }} in my templates are not picked up by the gettext builder. I get all required PO files, but no sphinx.po. Any clue?

    1. Thanks for the comment. There’s one tiny detail that I forgot to mention in the article. If you are including overriding templates the first time in your project, you need to tell Sphinx where the templates are. For example, if your overridden template is _templates/sidebar.html, you would need to add templates_path = ["_templates"] into your conf.py.

      I have added these details into the post accordingly.

      If this still doesn’t work, you might want to pdb into Sphinx for some debugging. sphinx/builders/gettext.py#L253-L281 is where the logic for extracting strings from templates are.

      1. Arthur Avatar

        Thanks a lot for the details, it works now! 👍
        Such a shame that the way to understand Sphinx is to rely on ipdb 😀

Leave a Reply

Your email address will not be published. Required fields are marked *