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:
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.
Leave a Reply