sphinx/tests/test_locale.py
James Addison f82c3c9912
Disable localisation when `SOURCE_DATE_EPOCH` is set (#10949)
This commit disables Sphinx's localisation features when reproducible
builds are requested, as determined by a non-empty SOURCE_DATE_EPOCH_
environment variable.

The `Reproducible Builds`_ project aims to provide confidence to
consumers of packaged software that the artefacts they're downloading
and installing have not been altered by the environment they were
built in, and can be replicated at a later date if required.

Builds of localised documentation using Sphinx currently account for
a large category of reproducible build testing failures, because the
builders intentionally use varying environment locales at build-time.
This can affect the contents of the ``objects.inv`` file.

During investigation, it turned out that many ``gettext``-localised
values (particularly in Python modules under ``sphinx.domains``) were
being translated at module-load-time and would not subsequently be
re-localised.

This creates two unusual effects:

1. Attempting to write a test case to build the same application in
   two different languages was not initially possible, as the
   first-loaded translation catalogue (as found in the 
   ``sphinx.locale.translators`` global variable) would remain in-use
   for subsequent application builds under different locales.

2. Localisation of strings could vary depending on whether the
   relevant modules were loaded before or after the resource
   catalogues were populated.

We fix this by performing all translations lazily so that module
imports can occur in any order and localisation of inventory entries
should occur only when translations of those items are requested.

Localisation can then be disabled by configuring the ``gettext``
language to the ISO-639-3 'undetermined' code (``'und'``), as this
should not have an associated translation catalogue. We also want to
prevent ``gettext`` from  attempting to determine the host's locale
from environment variables (including ``LANGUAGE``).

.. _SOURCE_DATE_EPOCH: https://reproducible-builds.org/docs/source-date-epoch/
.. _Reproducible Builds: https://www.reproducible-builds.org/
2023-04-07 17:49:36 +01:00

99 lines
3.4 KiB
Python

"""Test locale."""
import pytest
from sphinx import locale
@pytest.fixture(autouse=True)
def _cleanup_translations():
yield
locale.translators.clear()
def test_init(rootdir):
# not initialized yet
_ = locale.get_translation('myext')
assert _('Hello world') == 'Hello world'
assert _('Hello sphinx') == 'Hello sphinx'
assert _('Hello reST') == 'Hello reST'
# load locale1
locale.init([rootdir / 'test-locale' / 'locale1'], 'en', 'myext')
_ = locale.get_translation('myext')
assert _('Hello world') == 'HELLO WORLD'
assert _('Hello sphinx') == 'Hello sphinx'
assert _('Hello reST') == 'Hello reST'
# load a catalog to unrelated namespace
locale.init([rootdir / 'test-locale' / 'locale2'], 'en', 'myext', 'mynamespace')
_ = locale.get_translation('myext')
assert _('Hello world') == 'HELLO WORLD'
assert _('Hello sphinx') == 'Hello sphinx' # nothing changed here
assert _('Hello reST') == 'Hello reST'
# load locale2 in addition
locale.init([rootdir / 'test-locale' / 'locale2'], 'en', 'myext')
_ = locale.get_translation('myext')
assert _('Hello world') == 'HELLO WORLD'
assert _('Hello sphinx') == 'HELLO SPHINX'
assert _('Hello reST') == 'Hello reST'
def test_init_with_unknown_language(rootdir):
locale.init([rootdir / 'test-locale' / 'locale1'], 'unknown', 'myext')
_ = locale.get_translation('myext')
assert _('Hello world') == 'Hello world'
assert _('Hello sphinx') == 'Hello sphinx'
assert _('Hello reST') == 'Hello reST'
def test_add_message_catalog(app, rootdir):
app.config.language = 'en'
app.add_message_catalog('myext', rootdir / 'test-locale' / 'locale1')
_ = locale.get_translation('myext')
assert _('Hello world') == 'HELLO WORLD'
assert _('Hello sphinx') == 'Hello sphinx'
assert _('Hello reST') == 'Hello reST'
def _empty_language_translation(rootdir):
locale_dirs, catalog = [rootdir / 'test-locale' / 'locale1'], 'myext'
locale.translators.clear()
locale.init(locale_dirs, language=None, catalog=catalog)
return locale.get_translation(catalog)
def test_init_environment_language(rootdir, monkeypatch):
with monkeypatch.context() as m:
m.setenv("LANGUAGE", "en_US:en")
_ = _empty_language_translation(rootdir)
assert _('Hello world') == 'HELLO WORLD'
with monkeypatch.context() as m:
m.setenv("LANGUAGE", "et_EE:et")
_ = _empty_language_translation(rootdir)
assert _('Hello world') == 'Tere maailm'
def test_init_reproducible_build_language(rootdir, monkeypatch):
with monkeypatch.context() as m:
m.setenv("SOURCE_DATE_EPOCH", "0")
m.setenv("LANGUAGE", "en_US:en")
_ = _empty_language_translation(rootdir)
sde_en_translation = str(_('Hello world')) # str cast to evaluate lazy method
with monkeypatch.context() as m:
m.setenv("SOURCE_DATE_EPOCH", "0")
m.setenv("LANGUAGE", "et_EE:et")
_ = _empty_language_translation(rootdir)
sde_et_translation = str(_('Hello world')) # str cast to evaluate lazy method
with monkeypatch.context() as m:
m.setenv("LANGUAGE", "et_EE:et")
_ = _empty_language_translation(rootdir)
loc_et_translation = str(_('Hello world')) # str cast to evaluate lazy method
assert sde_en_translation == sde_et_translation
assert sde_et_translation != loc_et_translation