summaryrefslogtreecommitdiffstats
path: root/tests/test_intl
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_intl')
-rw-r--r--tests/test_intl/__init__.py0
-rw-r--r--tests/test_intl/test_catalogs.py74
-rw-r--r--tests/test_intl/test_intl.py1672
-rw-r--r--tests/test_intl/test_locale.py76
4 files changed, 1822 insertions, 0 deletions
diff --git a/tests/test_intl/__init__.py b/tests/test_intl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/test_intl/__init__.py
diff --git a/tests/test_intl/test_catalogs.py b/tests/test_intl/test_catalogs.py
new file mode 100644
index 0000000..b7fd7be
--- /dev/null
+++ b/tests/test_intl/test_catalogs.py
@@ -0,0 +1,74 @@
+"""Test the base build process."""
+import shutil
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture()
+def _setup_test(app_params):
+ assert isinstance(app_params.kwargs['srcdir'], Path)
+ srcdir = app_params.kwargs['srcdir']
+ src_locale_dir = srcdir / 'xx' / 'LC_MESSAGES'
+ dest_locale_dir = srcdir / 'locale'
+ # copy all catalogs into locale layout directory
+ for po in src_locale_dir.rglob('*.po'):
+ copy_po = (dest_locale_dir / 'en' / 'LC_MESSAGES' / po.relative_to(src_locale_dir))
+ if not copy_po.parent.exists():
+ copy_po.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy(po, copy_po)
+
+ yield
+
+ # delete remnants left over after failed build
+ shutil.rmtree(dest_locale_dir, ignore_errors=True)
+ shutil.rmtree(srcdir / '_build', ignore_errors=True)
+
+
+@pytest.mark.usefixtures('_setup_test')
+@pytest.mark.test_params(shared_result='test-catalogs')
+@pytest.mark.sphinx(
+ 'html', testroot='intl',
+ confoverrides={'language': 'en', 'locale_dirs': ['./locale']})
+def test_compile_all_catalogs(app, status, warning):
+ app.builder.compile_all_catalogs()
+
+ locale_dir = app.srcdir / 'locale'
+ catalog_dir = locale_dir / app.config.language / 'LC_MESSAGES'
+ expect = {x.with_suffix('.mo') for x in catalog_dir.rglob('*.po')}
+ actual = set(catalog_dir.rglob('*.mo'))
+ assert actual # not empty
+ assert actual == expect
+
+
+@pytest.mark.usefixtures('_setup_test')
+@pytest.mark.test_params(shared_result='test-catalogs')
+@pytest.mark.sphinx(
+ 'html', testroot='intl',
+ confoverrides={'language': 'en', 'locale_dirs': ['./locale']})
+def test_compile_specific_catalogs(app, status, warning):
+ locale_dir = app.srcdir / 'locale'
+ catalog_dir = locale_dir / app.config.language / 'LC_MESSAGES'
+
+ actual_on_boot = set(catalog_dir.rglob('*.mo')) # sphinx.mo might be included
+ app.builder.compile_specific_catalogs([app.srcdir / 'admonitions.txt'])
+ actual = {str(x.relative_to(catalog_dir))
+ for x in catalog_dir.rglob('*.mo')
+ if x not in actual_on_boot}
+ assert actual == {'admonitions.mo'}
+
+
+@pytest.mark.usefixtures('_setup_test')
+@pytest.mark.test_params(shared_result='test-catalogs')
+@pytest.mark.sphinx(
+ 'html', testroot='intl',
+ confoverrides={'language': 'en', 'locale_dirs': ['./locale']})
+def test_compile_update_catalogs(app, status, warning):
+ app.builder.compile_update_catalogs()
+
+ locale_dir = app.srcdir / 'locale'
+ catalog_dir = locale_dir / app.config.language / 'LC_MESSAGES'
+ expect = {x.with_suffix('.mo') for x in set(catalog_dir.rglob('*.po'))}
+ actual = set(catalog_dir.rglob('*.mo'))
+ assert actual # not empty
+ assert actual == expect
diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py
new file mode 100644
index 0000000..6b1e9ba
--- /dev/null
+++ b/tests/test_intl/test_intl.py
@@ -0,0 +1,1672 @@
+"""Test message patching for internationalization purposes.
+
+Runs the text builder in the test root.
+"""
+
+import os
+import os.path
+import re
+import shutil
+import time
+
+import pytest
+from babel.messages import mofile, pofile
+from babel.messages.catalog import Catalog
+from docutils import nodes
+
+from sphinx import locale
+from sphinx.testing.util import assert_node, etree_parse
+from sphinx.util.console import strip_colors
+from sphinx.util.nodes import NodeMatcher
+
+_CATALOG_LOCALE = 'xx'
+
+sphinx_intl = pytest.mark.sphinx(
+ testroot='intl',
+ confoverrides={
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
+ 'gettext_compact': False,
+ },
+)
+
+
+def read_po(pathname):
+ with open(pathname, encoding='utf-8') as f:
+ return pofile.read_po(f)
+
+
+def write_mo(pathname, po):
+ with open(pathname, 'wb') as f:
+ return mofile.write_mo(f, po)
+
+
+def _set_mtime_ns(target, value):
+ os.utime(target, ns=(value, value))
+ return os.stat(target).st_mtime_ns
+
+
+def _get_bom_intl_path(app):
+ basedir = app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES'
+ return basedir / 'bom.po', basedir / 'bom.mo'
+
+
+def _get_update_targets(app):
+ app.env.find_files(app.config, app.builder)
+ added, changed, removed = app.env.get_outdated_files(config_changed=False)
+ return added, changed, removed
+
+
+@pytest.fixture(autouse=True)
+def _info(app):
+ yield
+ print('# language:', app.config.language)
+ print('# locale_dirs:', app.config.locale_dirs)
+
+
+def elem_gettexts(elem):
+ return [_f for _f in [s.strip() for s in elem.itertext()] if _f]
+
+
+def elem_getref(elem):
+ return elem.attrib.get('refid') or elem.attrib.get('refuri')
+
+
+def assert_elem(elem, texts=None, refs=None, names=None):
+ if texts is not None:
+ _texts = elem_gettexts(elem)
+ assert _texts == texts
+ if refs is not None:
+ _refs = [elem_getref(x) for x in elem.findall('reference')]
+ assert _refs == refs
+ if names is not None:
+ _names = elem.attrib.get('names').split()
+ assert _names == names
+
+
+def assert_count(expected_expr, result, count):
+ find_pair = (expected_expr, result)
+ assert len(re.findall(*find_pair)) == count, find_pair
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_emit_warnings(app, warning):
+ app.build()
+ # test warnings in translation
+ warnings = getwarning(warning)
+ warning_expr = ('.*/warnings.txt:4:<translated>:1: '
+ 'WARNING: Inline literal start-string without end-string.\n')
+ assert re.search(warning_expr, warnings), f'{warning_expr!r} did not match {warnings!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_warning_node(app):
+ app.build()
+ # test warnings in translation
+ result = (app.outdir / 'warnings.txt').read_text(encoding='utf8')
+ expect = ("3. I18N WITH REST WARNINGS"
+ "\n**************************\n"
+ "\nLINE OF >>``<<BROKEN LITERAL MARKUP.\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_title_underline(app):
+ app.build()
+ # --- simple translation; check title underlines
+ result = (app.outdir / 'bom.txt').read_text(encoding='utf8')
+ expect = ("2. Datei mit UTF-8"
+ "\n******************\n" # underline matches new translation
+ "\nThis file has umlauts: äöü.\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_subdirs(app):
+ app.build()
+ # --- check translation in subdirs
+ result = (app.outdir / 'subdir' / 'index.txt').read_text(encoding='utf8')
+ assert result.startswith('1. subdir contents\n******************\n')
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_inconsistency_warnings(app, warning):
+ app.build()
+ # --- check warnings for inconsistency in number of references
+ result = (app.outdir / 'refs_inconsistency.txt').read_text(encoding='utf8')
+ expect = ("8. I18N WITH REFS INCONSISTENCY"
+ "\n*******************************\n"
+ "\n* FOR CITATION [ref3].\n"
+ "\n* reference FOR reference.\n"
+ "\n* ORPHAN REFERENCE: I18N WITH REFS INCONSISTENCY.\n"
+ "\n[1] THIS IS A AUTO NUMBERED FOOTNOTE.\n"
+ "\n[ref2] THIS IS A CITATION.\n"
+ "\n[100] THIS IS A NUMBERED FOOTNOTE.\n")
+ assert result == expect
+
+ warnings = getwarning(warning)
+ warning_fmt = ('.*/refs_inconsistency.txt:\\d+: '
+ 'WARNING: inconsistent %(reftype)s in translated message.'
+ ' original: %(original)s, translated: %(translated)s\n')
+ expected_warning_expr = (
+ warning_fmt % {
+ 'reftype': 'footnote references',
+ 'original': "\\['\\[#\\]_'\\]",
+ 'translated': "\\[\\]",
+ } +
+ warning_fmt % {
+ 'reftype': 'footnote references',
+ 'original': "\\['\\[100\\]_'\\]",
+ 'translated': "\\[\\]",
+ } +
+ warning_fmt % {
+ 'reftype': 'references',
+ 'original': "\\['reference_'\\]",
+ 'translated': "\\['reference_', 'reference_'\\]",
+ } +
+ warning_fmt % {
+ 'reftype': 'references',
+ 'original': "\\[\\]",
+ 'translated': "\\['`I18N WITH REFS INCONSISTENCY`_'\\]",
+ })
+ assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}'
+
+ expected_citation_warning_expr = (
+ '.*/refs_inconsistency.txt:\\d+: WARNING: Citation \\[ref2\\] is not referenced.\n' +
+ '.*/refs_inconsistency.txt:\\d+: WARNING: citation not found: ref3')
+ assert re.search(expected_citation_warning_expr, warnings), f'{expected_citation_warning_expr!r} did not match {warnings!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_noqa(app, warning):
+ app.build()
+ result = (app.outdir / 'noqa.txt').read_text(encoding='utf8')
+ expect = r"""FIRST SECTION
+*************
+
+TRANSLATED TEXT WITHOUT REFERENCE.
+
+TEST noqa WHITESPACE INSENSITIVITY.
+
+"#noqa" IS ESCAPED AT THE END OF THIS STRING. #noqa
+
+
+NEXT SECTION WITH PARAGRAPH TO TEST BARE noqa
+*********************************************
+
+Some text, again referring to the section: NEXT SECTION WITH PARAGRAPH
+TO TEST BARE noqa.
+"""
+ assert result == expect
+ assert "next-section" not in getwarning(warning)
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_literalblock_warnings(app, warning):
+ app.build()
+ # --- check warning for literal block
+ result = (app.outdir / 'literalblock.txt').read_text(encoding='utf8')
+ expect = ("9. I18N WITH LITERAL BLOCK"
+ "\n**************************\n"
+ "\nCORRECT LITERAL BLOCK:\n"
+ "\n this is"
+ "\n literal block\n"
+ "\nMISSING LITERAL BLOCK:\n"
+ "\n<SYSTEM MESSAGE:")
+ assert result.startswith(expect)
+
+ warnings = getwarning(warning)
+ expected_warning_expr = ('.*/literalblock.txt:\\d+: '
+ 'WARNING: Literal block expected; none found.')
+ assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_definition_terms(app):
+ app.build()
+ # --- definition terms: regression test for #975, #2198, #2205
+ result = (app.outdir / 'definition_terms.txt').read_text(encoding='utf8')
+ expect = ("13. I18N WITH DEFINITION TERMS"
+ "\n******************************\n"
+ "\nSOME TERM"
+ "\n THE CORRESPONDING DEFINITION\n"
+ "\nSOME *TERM* WITH LINK"
+ "\n THE CORRESPONDING DEFINITION #2\n"
+ "\nSOME **TERM** WITH : CLASSIFIER1 : CLASSIFIER2"
+ "\n THE CORRESPONDING DEFINITION\n"
+ "\nSOME TERM WITH : CLASSIFIER[]"
+ "\n THE CORRESPONDING DEFINITION\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_glossary_term(app, warning):
+ app.build()
+ # --- glossary terms: regression test for #1090
+ result = (app.outdir / 'glossary_terms.txt').read_text(encoding='utf8')
+ expect = (r"""18. I18N WITH GLOSSARY TERMS
+****************************
+
+SOME NEW TERM
+ THE CORRESPONDING GLOSSARY
+
+SOME OTHER NEW TERM
+ THE CORRESPONDING GLOSSARY #2
+
+LINK TO *SOME NEW TERM*.
+
+TRANSLATED GLOSSARY SHOULD BE SORTED BY TRANSLATED TERMS:
+
+TRANSLATED TERM XXX
+ DEFINE XXX
+
+TRANSLATED TERM YYY
+ DEFINE YYY
+
+TRANSLATED TERM ZZZ
+VVV
+ DEFINE ZZZ
+""")
+ assert result == expect
+ warnings = getwarning(warning)
+ assert 'term not in glossary' not in warnings
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_glossary_term_inconsistencies(app, warning):
+ app.build()
+ # --- glossary term inconsistencies: regression test for #1090
+ result = (app.outdir / 'glossary_terms_inconsistency.txt').read_text(encoding='utf8')
+ expect = ("19. I18N WITH GLOSSARY TERMS INCONSISTENCY"
+ "\n******************************************\n"
+ "\n1. LINK TO *SOME NEW TERM*.\n")
+ assert result == expect
+
+ warnings = getwarning(warning)
+ expected_warning_expr = (
+ '.*/glossary_terms_inconsistency.txt:\\d+: '
+ 'WARNING: inconsistent term references in translated message.'
+ " original: \\[':term:`Some term`', ':term:`Some other term`'\\],"
+ " translated: \\[':term:`SOME NEW TERM`'\\]\n")
+ assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_section(app):
+ app.build()
+ # --- section
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po')
+ actual = read_po(app.outdir / 'section.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_section(app):
+ app.build()
+ # --- section
+ result = (app.outdir / 'section.txt').read_text(encoding='utf8')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.string in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_seealso(app):
+ app.build()
+ # --- seealso
+ result = (app.outdir / 'seealso.txt').read_text(encoding='utf8')
+ expect = ("12. I18N WITH SEEALSO"
+ "\n*********************\n"
+ "\nSee also: SHORT TEXT 1\n"
+ "\nSee also: LONG TEXT 1\n"
+ "\nSee also:\n"
+ "\n SHORT TEXT 2\n"
+ "\n LONG TEXT 2\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_figure_captions(app):
+ app.build()
+ # --- figure captions: regression test for #940
+ result = (app.outdir / 'figure.txt').read_text(encoding='utf8')
+ expect = ("14. I18N WITH FIGURE CAPTION"
+ "\n****************************\n"
+ "\n [image]MY CAPTION OF THE FIGURE\n"
+ "\n MY DESCRIPTION PARAGRAPH1 OF THE FIGURE.\n"
+ "\n MY DESCRIPTION PARAGRAPH2 OF THE FIGURE.\n"
+ "\n"
+ "\n14.1. FIGURE IN THE BLOCK"
+ "\n=========================\n"
+ "\nBLOCK\n"
+ "\n [image]MY CAPTION OF THE FIGURE\n"
+ "\n MY DESCRIPTION PARAGRAPH1 OF THE FIGURE.\n"
+ "\n MY DESCRIPTION PARAGRAPH2 OF THE FIGURE.\n"
+ "\n"
+ "\n"
+ "14.2. IMAGE URL AND ALT\n"
+ "=======================\n"
+ "\n"
+ "[image: I18N -> IMG][image]\n"
+ "\n"
+ " [image: IMG -> I18N][image]\n"
+ "\n"
+ "\n"
+ "14.3. IMAGE ON SUBSTITUTION\n"
+ "===========================\n"
+ "\n"
+ "\n"
+ "14.4. IMAGE UNDER NOTE\n"
+ "======================\n"
+ "\n"
+ "Note:\n"
+ "\n"
+ " [image: i18n under note][image]\n"
+ "\n"
+ " [image: img under note][image]\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_rubric(app):
+ app.build()
+ # --- rubric: regression test for pull request #190
+ result = (app.outdir / 'rubric.txt').read_text(encoding='utf8')
+ expect = ("I18N WITH RUBRIC"
+ "\n****************\n"
+ "\n-[ RUBRIC TITLE ]-\n"
+ "\n"
+ "\nRUBRIC IN THE BLOCK"
+ "\n===================\n"
+ "\nBLOCK\n"
+ "\n -[ RUBRIC TITLE ]-\n")
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_docfields(app):
+ app.build()
+ # --- docfields
+ result = (app.outdir / 'docfields.txt').read_text(encoding='utf8')
+ expect = ("21. I18N WITH DOCFIELDS"
+ "\n***********************\n"
+ "\nclass Cls1\n"
+ "\n Parameters:"
+ "\n **param** -- DESCRIPTION OF PARAMETER param\n"
+ "\nclass Cls2\n"
+ "\n Parameters:"
+ "\n * **foo** -- DESCRIPTION OF PARAMETER foo\n"
+ "\n * **bar** -- DESCRIPTION OF PARAMETER bar\n"
+ "\nclass Cls3(values)\n"
+ "\n Raises:"
+ "\n **ValueError** -- IF THE VALUES ARE OUT OF RANGE\n"
+ "\nclass Cls4(values)\n"
+ "\n Raises:"
+ "\n * **TypeError** -- IF THE VALUES ARE NOT VALID\n"
+ "\n * **ValueError** -- IF THE VALUES ARE OUT OF RANGE\n"
+ "\nclass Cls5\n"
+ "\n Returns:"
+ '\n A NEW "Cls3" INSTANCE\n')
+ assert result == expect
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_admonitions(app):
+ app.build()
+ # --- admonitions
+ # #1206: gettext did not translate admonition directive's title
+ # seealso: https://docutils.sourceforge.io/docs/ref/rst/directives.html#admonitions
+ result = (app.outdir / 'admonitions.txt').read_text(encoding='utf8')
+ directives = (
+ "attention", "caution", "danger", "error", "hint",
+ "important", "note", "tip", "warning", "admonition")
+ for d in directives:
+ assert d.upper() + " TITLE" in result
+ assert d.upper() + " BODY" in result
+
+ # for #4938 `1. ` prefixed admonition title
+ assert "1. ADMONITION TITLE" in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_toctree(app):
+ app.build()
+ # --- toctree (index.rst)
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'index.po')
+ actual = read_po(app.outdir / 'index.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+ # --- toctree (toctree.rst)
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po')
+ actual = read_po(app.outdir / 'toctree.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_table(app):
+ app.build()
+ # --- toctree
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po')
+ actual = read_po(app.outdir / 'table.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_table(app):
+ app.build()
+ # --- toctree
+ result = (app.outdir / 'table.txt').read_text(encoding='utf8')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.string in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_toctree(app):
+ app.build()
+ # --- toctree (index.rst)
+ # Note: index.rst contains contents that is not shown in text.
+ result = (app.outdir / 'index.txt').read_text(encoding='utf8')
+ assert 'CONTENTS' in result
+ assert 'TABLE OF CONTENTS' in result
+ # --- toctree (toctree.rst)
+ result = (app.outdir / 'toctree.txt').read_text(encoding='utf8')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po')
+ for expect_msg in (m for m in expect if m.id):
+ assert expect_msg.string in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_topic(app):
+ app.build()
+ # --- topic
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po')
+ actual = read_po(app.outdir / 'topic.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_topic(app):
+ app.build()
+ # --- topic
+ result = (app.outdir / 'topic.txt').read_text(encoding='utf8')
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.string in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_definition_terms(app):
+ app.build()
+ # --- definition terms: regression test for #2198, #2205
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'definition_terms.po')
+ actual = read_po(app.outdir / 'definition_terms.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_glossary_terms(app, warning):
+ app.build()
+ # --- glossary terms: regression test for #1090
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms.po')
+ actual = read_po(app.outdir / 'glossary_terms.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+ warnings = warning.getvalue().replace(os.sep, '/')
+ assert 'term not in glossary' not in warnings
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_glossary_term_inconsistencies(app):
+ app.build()
+ # --- glossary term inconsistencies: regression test for #1090
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po')
+ actual = read_po(app.outdir / 'glossary_terms_inconsistency.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_literalblock(app):
+ app.build()
+ # --- gettext builder always ignores ``only`` directive
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'literalblock.po')
+ actual = read_po(app.outdir / 'literalblock.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ if len(expect_msg.id.splitlines()) == 1:
+ # compare translations only labels
+ assert expect_msg.id in [m.id for m in actual if m.id]
+ else:
+ pass # skip code-blocks and literalblocks
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext')
+@pytest.mark.test_params(shared_result='test_intl_gettext')
+def test_gettext_buildr_ignores_only_directive(app):
+ app.build()
+ # --- gettext builder always ignores ``only`` directive
+ expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'only.po')
+ actual = read_po(app.outdir / 'only.pot')
+ for expect_msg in [m for m in expect if m.id]:
+ assert expect_msg.id in [m.id for m in actual if m.id]
+
+
+@sphinx_intl
+def test_node_translated_attribute(app):
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
+
+ doctree = app.env.get_doctree('translation_progress')
+
+ translated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=True)))
+ assert translated_nodes == 10 + 1 # 10 lines + title
+
+ untranslated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=False)))
+ assert untranslated_nodes == 2 + 2 + 1 # 2 lines + 2 lines + substitution reference
+
+
+@sphinx_intl
+def test_translation_progress_substitution(app):
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
+
+ doctree = app.env.get_doctree('translation_progress')
+
+ assert doctree[0][19][0] == '68.75%' # 11 out of 16 lines are translated
+
+
+@pytest.mark.sphinx(testroot='intl', freshenv=True, confoverrides={
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
+ 'gettext_compact': False,
+ 'translation_progress_classes': True,
+})
+def test_translation_progress_classes_true(app):
+ app.build(filenames=[app.srcdir / 'translation_progress.txt'])
+
+ doctree = app.env.get_doctree('translation_progress')
+
+ # title
+ assert 'translated' in doctree[0][0]['classes']
+
+ # translated lines
+ assert 'translated' in doctree[0][1]['classes']
+ assert 'translated' in doctree[0][2]['classes']
+ assert 'translated' in doctree[0][3]['classes']
+ assert 'translated' in doctree[0][4]['classes']
+ assert 'translated' in doctree[0][5]['classes']
+ assert 'translated' in doctree[0][6]['classes']
+ assert 'translated' in doctree[0][7]['classes']
+ assert 'translated' in doctree[0][8]['classes']
+
+ assert doctree[0][9]['classes'] == [] # comment node
+
+ # idempotent
+ assert 'translated' in doctree[0][10]['classes']
+ assert 'translated' in doctree[0][11]['classes']
+
+ assert doctree[0][12]['classes'] == [] # comment node
+
+ # untranslated
+ assert 'untranslated' in doctree[0][13]['classes']
+ assert 'untranslated' in doctree[0][14]['classes']
+
+ assert doctree[0][15]['classes'] == [] # comment node
+
+ # missing
+ assert 'untranslated' in doctree[0][16]['classes']
+ assert 'untranslated' in doctree[0][17]['classes']
+
+ assert doctree[0][18]['classes'] == [] # comment node
+
+ # substitution reference
+ assert 'untranslated' in doctree[0][19]['classes']
+
+ assert len(doctree[0]) == 20
+
+
+class _MockClock:
+ """Object for mocking :func:`time.time_ns` (if needed).
+
+ Use :meth:`sleep` to make this specific clock sleep for some time.
+ """
+
+ def time(self) -> int:
+ """Nanosecond since 'fake' epoch."""
+ raise NotImplementedError
+
+ def sleep(self, ds: float) -> None:
+ """Sleep *ds* seconds."""
+ raise NotImplementedError
+
+
+class _MockWindowsClock(_MockClock):
+ """Object for mocking :func:`time.time_ns` on Windows platforms.
+
+ The result is in 'nanoseconds' but with a microsecond resolution
+ so that the division by 1_000 does not cause rounding issues.
+ """
+
+ def __init__(self) -> None:
+ self.us: int = 0 # current microsecond 'tick'
+
+ def time(self) -> int:
+ ret = 1_000 * self.us
+ self.us += 1
+ return ret
+
+ def sleep(self, ds: float) -> None:
+ self.us += int(ds * 1e6)
+
+
+class _MockUnixClock(_MockClock):
+ """Object for mocking :func:`time.time_ns` on Unix platforms.
+
+ Since nothing is needed for Unix platforms, this object acts as
+ a proxy so that the API is the same as :class:`_MockWindowsClock`.
+ """
+
+ def time(self) -> int:
+ return time.time_ns()
+
+ def sleep(self, ds: float) -> None:
+ time.sleep(ds)
+
+
+@pytest.fixture()
+def mock_time_and_i18n(
+ monkeypatch: pytest.MonkeyPatch,
+) -> tuple[pytest.MonkeyPatch, _MockClock]:
+ from sphinx.util.i18n import CatalogInfo
+
+ # save the 'original' definition
+ catalog_write_mo = CatalogInfo.write_mo
+
+ def mock_write_mo(self, locale, use_fuzzy=False):
+ catalog_write_mo(self, locale, use_fuzzy)
+ # ensure that the .mo file being written has a correct fake timestamp
+ _set_mtime_ns(self.mo_path, time.time_ns())
+
+ # see: https://github.com/pytest-dev/pytest/issues/363
+ with pytest.MonkeyPatch.context() as mock:
+ if os.name == 'posix':
+ clock = _MockUnixClock()
+ else:
+ # When using pytest.mark.parametrize() to emulate test repetition,
+ # the teardown phase on Windows fails due to an error apparently in
+ # the colorama.ansitowin32 module, so we forcibly disable colors.
+ mock.setenv('NO_COLOR', '1')
+ # apply the patch only for Windows
+ clock = _MockWindowsClock()
+ mock.setattr('time.time_ns', clock.time)
+ # Use clock.sleep() to emulate time.sleep() but do not try
+ # to mock the latter since this might break other libraries.
+ mock.setattr('sphinx.util.i18n.CatalogInfo.write_mo', mock_write_mo)
+ yield mock, clock
+
+
+@sphinx_intl
+# use the same testroot as 'gettext' since the latter contains less PO files
+@pytest.mark.sphinx('dummy', testroot='builder-gettext-dont-rebuild-mo', freshenv=True)
+def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params):
+ mock, clock = mock_time_and_i18n
+ assert os.name == 'posix' or clock.time() == 0
+
+ args, kwargs = app_params
+ app = make_app(*args, **kwargs)
+ po_path, mo_path = _get_bom_intl_path(app)
+
+ # creation time of the those files (order does not matter)
+ bom_rst = app.srcdir / 'bom.rst'
+ bom_rst_time = time.time_ns()
+
+ index_rst = app.srcdir / 'index.rst'
+ index_rst_time = time.time_ns()
+ po_time = time.time_ns()
+
+ # patch the 'creation time' of the source files
+ assert _set_mtime_ns(po_path, po_time) == po_time
+ assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time
+ assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time
+
+ assert not mo_path.exists()
+ # when writing mo files, the counter is updated by calling
+ # patch_write_mo which is called to create .mo files (and
+ # thus the timestamp of the files are not those given by
+ # the OS but our fake ones)
+ app.build()
+ assert mo_path.exists()
+ # Do a real sleep on POSIX, or simulate a sleep on Windows
+ # to ensure that calls to time.time_ns() remain consistent.
+ clock.sleep(0.1 if os.name == 'posix' else 1)
+
+ # check that the source files were not modified
+ assert bom_rst.stat().st_mtime_ns == bom_rst_time
+ assert index_rst.stat().st_mtime_ns == index_rst_time
+ # check that the 'bom' document is discovered after the .mo
+ # file has been written on the disk (i.e., read_doc() is called
+ # after the creation of the .mo files)
+ assert app.env.all_docs['bom'] > mo_path.stat().st_mtime_ns // 1000
+
+ # Since it is after the build, the number of documents to be updated is 0
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
+ # When rewriting the timestamp of mo file, the number of documents to be
+ # updated will be changed.
+ new_mo_time = time.time_ns()
+ assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == {'bom'}
+ mock.undo() # explicit call since it's not a context
+
+ # remove all sources for the next test
+ shutil.rmtree(app.srcdir, ignore_errors=True)
+ time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep
+
+
+@sphinx_intl
+@pytest.mark.sphinx('gettext', testroot='builder-gettext-dont-rebuild-mo', freshenv=True)
+def test_gettext_dont_rebuild_mo(mock_time_and_i18n, app):
+ mock, clock = mock_time_and_i18n
+ assert os.name == 'posix' or clock.time() == 0
+
+ assert app.srcdir.exists()
+
+ # patch the 'creation time' of the source files
+ bom_rst = app.srcdir / 'bom.rst'
+ bom_rst_time = time.time_ns()
+ assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time
+
+ index_rst = app.srcdir / 'index.rst'
+ index_rst_time = time.time_ns()
+ assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time
+
+ # phase 1: create fake MO file in the src directory
+ po_path, mo_path = _get_bom_intl_path(app)
+ write_mo(mo_path, read_po(po_path))
+ po_time = time.time_ns()
+ assert _set_mtime_ns(po_path, po_time) == po_time
+
+ # phase 2: build document with gettext builder.
+ # The mo file in the srcdir directory is retained.
+ app.build()
+ # Do a real sleep on POSIX, or simulate a sleep on Windows
+ # to ensure that calls to time.time_ns() remain consistent.
+ clock.sleep(0.5 if os.name == 'posix' else 1)
+ # Since it is after the build, the number of documents to be updated is 0
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
+ # Even if the timestamp of the mo file is updated, the number of documents
+ # to be updated is 0. gettext builder does not rebuild because of mo update.
+ new_mo_time = time.time_ns()
+ assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time
+ update_targets = _get_update_targets(app)
+ assert update_targets[1] == set()
+ mock.undo() # remove the patch
+
+ # remove all sources for the next test
+ shutil.rmtree(app.srcdir, ignore_errors=True)
+ time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_meta(app):
+ app.build()
+ # --- test for meta
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+ expected_expr = '<meta content="TESTDATA FOR I18N" name="description" translated="True" />'
+ assert expected_expr in result
+ expected_expr = '<meta content="I18N, SPHINX, MARKUP" name="keywords" translated="True" />'
+ assert expected_expr in result
+ expected_expr = '<p class="caption" role="heading"><span class="caption-text">HIDDEN TOC</span></p>'
+ assert expected_expr in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_footnotes(app):
+ app.build()
+ # --- test for #955 cant-build-html-with-footnotes-when-using
+ # expect no error by build
+ (app.outdir / 'footnote.html').read_text(encoding='utf8')
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_undefined_refs(app):
+ app.build()
+ # --- links to undefined reference
+ result = (app.outdir / 'refs_inconsistency.html').read_text(encoding='utf8')
+
+ expected_expr = ('<a class="reference external" '
+ 'href="https://www.example.com">reference</a>')
+ assert len(re.findall(expected_expr, result)) == 2
+
+ expected_expr = ('<a class="reference internal" '
+ 'href="#reference">reference</a>')
+ assert len(re.findall(expected_expr, result)) == 0
+
+ expected_expr = ('<a class="reference internal" '
+ 'href="#i18n-with-refs-inconsistency">I18N WITH '
+ 'REFS INCONSISTENCY</a>')
+ assert len(re.findall(expected_expr, result)) == 1
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_index_entries(app):
+ app.build()
+ # --- index entries: regression test for #976
+ result = (app.outdir / 'genindex.html').read_text(encoding='utf8')
+
+ def wrap(tag, keyword):
+ start_tag = "<%s[^>]*>" % tag
+ end_tag = "</%s>" % tag
+ return fr"{start_tag}\s*{keyword}\s*{end_tag}"
+
+ def wrap_nest(parenttag, childtag, keyword):
+ start_tag1 = "<%s[^>]*>" % parenttag
+ start_tag2 = "<%s[^>]*>" % childtag
+ return fr"{start_tag1}\s*{keyword}\s*{start_tag2}"
+ expected_exprs = [
+ wrap('a', 'NEWSLETTER'),
+ wrap('a', 'MAILING LIST'),
+ wrap('a', 'RECIPIENTS LIST'),
+ wrap('a', 'FIRST SECOND'),
+ wrap('a', 'SECOND THIRD'),
+ wrap('a', 'THIRD, FIRST'),
+ wrap_nest('li', 'ul', 'ENTRY'),
+ wrap_nest('li', 'ul', 'SEE'),
+ ]
+ for expr in expected_exprs:
+ assert re.search(expr, result, re.MULTILINE), f'{expr!r} did not match {result!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_versionchanges(app):
+ app.build()
+ # --- versionchanges
+ result = (app.outdir / 'versionchange.html').read_text(encoding='utf8')
+
+ def get_content(result, name):
+ matched = re.search(r'<div class="%s">\n*(.*?)</div>' % name,
+ result, re.DOTALL)
+ if matched:
+ return matched.group(1)
+ else:
+ return ''
+
+ expect1 = (
+ """<p><span class="versionmodified deprecated">Deprecated since version 1.0: </span>"""
+ """THIS IS THE <em>FIRST</em> PARAGRAPH OF DEPRECATED.</p>\n"""
+ """<p>THIS IS THE <em>SECOND</em> PARAGRAPH OF DEPRECATED.</p>\n""")
+ matched_content = get_content(result, "deprecated")
+ assert expect1 == matched_content
+
+ expect2 = (
+ """<p><span class="versionmodified added">Added in version 1.0: </span>"""
+ """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONADDED.</p>\n""")
+ matched_content = get_content(result, "versionadded")
+ assert expect2 == matched_content
+
+ expect3 = (
+ """<p><span class="versionmodified changed">Changed in version 1.0: </span>"""
+ """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONCHANGED.</p>\n""")
+ matched_content = get_content(result, "versionchanged")
+ assert expect3 == matched_content
+
+ expect4 = (
+ """<p><span class="versionmodified removed">Removed in version 1.0: </span>"""
+ """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONREMOVED.</p>\n""")
+ matched_content = get_content(result, "versionremoved")
+ assert expect4 == matched_content
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_docfields(app):
+ app.build()
+ # --- docfields
+ # expect no error by build
+ (app.outdir / 'docfields.html').read_text(encoding='utf8')
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_template(app):
+ app.build()
+ # --- gettext template
+ result = (app.outdir / 'contents.html').read_text(encoding='utf8')
+ assert "WELCOME" in result
+ assert "SPHINX 2013.120" in result
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_html_rebuild_mo(app):
+ app.build()
+ # --- rebuild by .mo mtime
+ app.build()
+ _, updated, _ = _get_update_targets(app)
+ assert updated == set()
+
+ _, bom_file = _get_bom_intl_path(app)
+ old_mtime = bom_file.stat().st_mtime
+ new_mtime = old_mtime + (dt := 5)
+ os.utime(bom_file, (new_mtime, new_mtime))
+ assert old_mtime + dt == new_mtime, (old_mtime + dt, new_mtime)
+ _, updated, _ = _get_update_targets(app)
+ assert updated == {'bom'}
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_footnotes(app, warning):
+ app.build()
+ # --- footnotes: regression test for fix #955, #1176
+ et = etree_parse(app.outdir / 'footnote.xml')
+ secs = et.findall('section')
+
+ para0 = secs[0].findall('paragraph')
+ assert_elem(
+ para0[0],
+ ['I18N WITH FOOTNOTE', 'INCLUDE THIS CONTENTS',
+ '2', '[ref]', '1', '100', '*', '. SECOND FOOTNOTE_REF', '100', '.'],
+ ['i18n-with-footnote', 'ref'])
+
+ # check node_id for footnote_references which refer same footnote (refs: #3002)
+ assert para0[0][4].text == para0[0][6].text == '100'
+ assert para0[0][4].attrib['ids'] != para0[0][6].attrib['ids']
+
+ footnote0 = secs[0].findall('footnote')
+ assert_elem(
+ footnote0[0],
+ ['1', 'THIS IS A AUTO NUMBERED FOOTNOTE.'],
+ None,
+ ['1'])
+ assert_elem(
+ footnote0[1],
+ ['100', 'THIS IS A NUMBERED FOOTNOTE.'],
+ None,
+ ['100'])
+ assert_elem(
+ footnote0[2],
+ ['2', 'THIS IS A AUTO NUMBERED NAMED FOOTNOTE.'],
+ None,
+ ['named'])
+ assert_elem(
+ footnote0[3],
+ ['*', 'THIS IS A AUTO SYMBOL FOOTNOTE.'],
+ None,
+ None)
+
+ citation0 = secs[0].findall('citation')
+ assert_elem(
+ citation0[0],
+ ['ref', 'THIS IS A NAMED FOOTNOTE.'],
+ None,
+ ['ref'])
+
+ warnings = getwarning(warning)
+ warning_expr = '.*/footnote.xml:\\d*: SEVERE: Duplicate ID: ".*".\n'
+ assert not re.search(warning_expr, warnings), f'{warning_expr!r} did match {warnings!r}'
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_footnote_backlinks(app):
+ app.build()
+ # --- footnote backlinks: i18n test for #1058
+ et = etree_parse(app.outdir / 'footnote.xml')
+ secs = et.findall('section')
+
+ para0 = secs[0].findall('paragraph')
+ refs0 = para0[0].findall('footnote_reference')
+ refid2id = {r.attrib.get('refid'): r.attrib.get('ids') for r in refs0}
+
+ footnote0 = secs[0].findall('footnote')
+ for footnote in footnote0:
+ ids = footnote.attrib.get('ids')
+ backrefs = footnote.attrib.get('backrefs').split()
+ assert refid2id[ids] in backrefs
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_refs_in_python_domain(app):
+ app.build()
+ # --- refs in the Python domain
+ et = etree_parse(app.outdir / 'refs_python_domain.xml')
+ secs = et.findall('section')
+
+ # regression test for fix #1363
+ para0 = secs[0].findall('paragraph')
+ assert_elem(
+ para0[0],
+ ['SEE THIS DECORATOR:', 'sensitive_variables()', '.'],
+ ['sensitive.sensitive_variables'])
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_keep_external_links(app):
+ app.build()
+ # --- keep external links: regression test for #1044
+ et = etree_parse(app.outdir / 'external_links.xml')
+ secs = et.findall('section')
+
+ para0 = secs[0].findall('paragraph')
+ # external link check
+ assert_elem(
+ para0[0],
+ ['EXTERNAL LINK TO', 'Python', '.'],
+ ['https://python.org/index.html'])
+
+ # internal link check
+ assert_elem(
+ para0[1],
+ ['EXTERNAL LINKS', 'IS INTERNAL LINK.'],
+ ['i18n-with-external-links'])
+
+ # inline link check
+ assert_elem(
+ para0[2],
+ ['INLINE LINK BY', 'THE SPHINX SITE', '.'],
+ ['https://sphinx-doc.org'])
+
+ # unnamed link check
+ assert_elem(
+ para0[3],
+ ['UNNAMED', 'LINK', '.'],
+ ['https://google.com'])
+
+ # link target swapped translation
+ para1 = secs[1].findall('paragraph')
+ assert_elem(
+ para1[0],
+ ['LINK TO', 'external2', 'AND', 'external1', '.'],
+ ['https://www.google.com/external2',
+ 'https://www.google.com/external1'])
+ assert_elem(
+ para1[1],
+ ['LINK TO', 'THE PYTHON SITE', 'AND', 'THE SPHINX SITE', '.'],
+ ['https://python.org', 'https://sphinx-doc.org'])
+
+ # multiple references in the same line
+ para2 = secs[2].findall('paragraph')
+ assert_elem(
+ para2[0],
+ ['LINK TO', 'EXTERNAL LINKS', ',', 'Python', ',',
+ 'THE SPHINX SITE', ',', 'UNNAMED', 'AND',
+ 'THE PYTHON SITE', '.'],
+ ['i18n-with-external-links', 'https://python.org/index.html',
+ 'https://sphinx-doc.org', 'https://google.com',
+ 'https://python.org'])
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_role_xref(app):
+ app.build()
+ # --- role xref: regression test for #1090, #1193
+ et = etree_parse(app.outdir / 'role_xref.xml')
+ sec1, sec2 = et.findall('section')
+
+ para1, = sec1.findall('paragraph')
+ assert_elem(
+ para1,
+ ['LINK TO', "I18N ROCK'N ROLE XREF", ',', 'CONTENTS', ',',
+ 'SOME NEW TERM', '.'],
+ ['i18n-role-xref', 'index',
+ 'glossary_terms#term-Some-term'])
+
+ para2 = sec2.findall('paragraph')
+ assert_elem(
+ para2[0],
+ ['LINK TO', 'SOME OTHER NEW TERM', 'AND', 'SOME NEW TERM', '.'],
+ ['glossary_terms#term-Some-other-term',
+ 'glossary_terms#term-Some-term'])
+ assert_elem(
+ para2[1],
+ ['LINK TO', 'LABEL', 'AND',
+ 'SAME TYPE LINKS', 'AND', 'SAME TYPE LINKS', '.'],
+ ['i18n-role-xref', 'same-type-links', 'same-type-links'])
+ assert_elem(
+ para2[2],
+ ['LINK TO', 'I18N WITH GLOSSARY TERMS', 'AND', 'CONTENTS', '.'],
+ ['glossary_terms', 'index'])
+ assert_elem(
+ para2[3],
+ ['LINK TO', '--module', 'AND', '-m', '.'],
+ ['cmdoption-module', 'cmdoption-m'])
+ assert_elem(
+ para2[4],
+ ['LINK TO', 'env2', 'AND', 'env1', '.'],
+ ['envvar-env2', 'envvar-env1'])
+ assert_elem(
+ para2[5],
+ ['LINK TO', 'token2', 'AND', 'token1', '.'],
+ []) # TODO: how do I link token role to productionlist?
+ assert_elem(
+ para2[6],
+ ['LINK TO', 'same-type-links', 'AND', "i18n-role-xref", '.'],
+ ['same-type-links', 'i18n-role-xref'])
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_warnings(app, warning):
+ app.build()
+ # warnings
+ warnings = getwarning(warning)
+ assert 'term not in glossary' not in warnings
+ assert 'undefined label' not in warnings
+ assert 'unknown document' not in warnings
+
+
+@sphinx_intl
+@pytest.mark.sphinx('xml')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_xml_label_targets(app):
+ app.build()
+ # --- label targets: regression test for #1193, #1265
+ et = etree_parse(app.outdir / 'label_target.xml')
+ secs = et.findall('section')
+
+ para0 = secs[0].findall('paragraph')
+ assert_elem(
+ para0[0],
+ ['X SECTION AND LABEL', 'POINT TO', 'implicit-target', 'AND',
+ 'X SECTION AND LABEL', 'POINT TO', 'section-and-label', '.'],
+ ['implicit-target', 'section-and-label'])
+
+ para1 = secs[1].findall('paragraph')
+ assert_elem(
+ para1[0],
+ ['X EXPLICIT-TARGET', 'POINT TO', 'explicit-target', 'AND',
+ 'X EXPLICIT-TARGET', 'POINT TO DUPLICATED ID LIKE', 'id1',
+ '.'],
+ ['explicit-target', 'id1'])
+
+ para2 = secs[2].findall('paragraph')
+ assert_elem(
+ para2[0],
+ ['X IMPLICIT SECTION NAME', 'POINT TO',
+ 'implicit-section-name', '.'],
+ ['implicit-section-name'])
+
+ sec2 = secs[2].findall('section')
+
+ para2_0 = sec2[0].findall('paragraph')
+ assert_elem(
+ para2_0[0],
+ ['`X DUPLICATED SUB SECTION`_', 'IS BROKEN LINK.'],
+ [])
+
+ para3 = secs[3].findall('paragraph')
+ assert_elem(
+ para3[0],
+ ['X', 'bridge label',
+ 'IS NOT TRANSLATABLE BUT LINKED TO TRANSLATED ' +
+ 'SECTION TITLE.'],
+ ['label-bridged-target-section'])
+ assert_elem(
+ para3[1],
+ ['X', 'bridge label', 'POINT TO',
+ 'LABEL BRIDGED TARGET SECTION', 'AND', 'bridge label2',
+ 'POINT TO', 'SECTION AND LABEL', '. THE SECOND APPEARED',
+ 'bridge label2', 'POINT TO CORRECT TARGET.'],
+ ['label-bridged-target-section',
+ 'section-and-label',
+ 'section-and-label'])
+
+
+@sphinx_intl
+@pytest.mark.sphinx('html')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_additional_targets_should_not_be_translated(app):
+ app.build()
+ # [literalblock.txt]
+ result = (app.outdir / 'literalblock.html').read_text(encoding='utf8')
+
+ # title should be translated
+ expected_expr = 'CODE-BLOCKS'
+ assert_count(expected_expr, result, 2)
+
+ # ruby code block should not be translated but be highlighted
+ expected_expr = """<span class="s1">&#39;result&#39;</span>"""
+ assert_count(expected_expr, result, 1)
+
+ # C code block without lang should not be translated and *ruby* highlighted
+ expected_expr = """<span class="c1">#include &lt;stdlib.h&gt;</span>"""
+ assert_count(expected_expr, result, 1)
+
+ # C code block with lang should not be translated but be *C* highlighted
+ expected_expr = ("""<span class="cp">#include</span>"""
+ """<span class="w"> </span>"""
+ """<span class="cpf">&lt;stdio.h&gt;</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # literal block in list item should not be translated
+ expected_expr = ("""<span class="n">literal</span>"""
+ """<span class="o">-</span>"""
+ """<span class="n">block</span>\n"""
+ """<span class="k">in</span>"""
+ """<span class="w"> </span>"""
+ """<span class="n">list</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # doctest block should not be translated but be highlighted
+ expected_expr = (
+ """<span class="gp">&gt;&gt;&gt; </span>"""
+ """<span class="kn">import</span> <span class="nn">sys</span> """
+ """<span class="c1"># sys importing</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # [raw.txt]
+
+ result = (app.outdir / 'raw.html').read_text(encoding='utf8')
+
+ # raw block should not be translated
+ expected_expr = """<iframe src="https://sphinx-doc.org"></iframe></section>"""
+ assert_count(expected_expr, result, 1)
+
+ # [figure.txt]
+
+ result = (app.outdir / 'figure.html').read_text(encoding='utf8')
+
+ # src for image block should not be translated (alt is translated)
+ expected_expr = """<img alt="I18N -&gt; IMG" src="_images/i18n.png" />"""
+ assert_count(expected_expr, result, 1)
+
+ # src for figure block should not be translated (alt is translated)
+ expected_expr = """<img alt="IMG -&gt; I18N" src="_images/img.png" />"""
+ assert_count(expected_expr, result, 1)
+
+
+@sphinx_intl
+@pytest.mark.sphinx(
+ 'html',
+ srcdir='test_additional_targets_should_be_translated',
+ confoverrides={
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
+ 'gettext_compact': False,
+ 'gettext_additional_targets': [
+ 'index',
+ 'literal-block',
+ 'doctest-block',
+ 'raw',
+ 'image',
+ ],
+ },
+)
+def test_additional_targets_should_be_translated(app):
+ app.build()
+ # [literalblock.txt]
+ result = (app.outdir / 'literalblock.html').read_text(encoding='utf8')
+
+ # title should be translated
+ expected_expr = 'CODE-BLOCKS'
+ assert_count(expected_expr, result, 2)
+
+ # ruby code block should be translated and be highlighted
+ expected_expr = """<span class="s1">&#39;RESULT&#39;</span>"""
+ assert_count(expected_expr, result, 1)
+
+ # C code block without lang should be translated and *ruby* highlighted
+ expected_expr = """<span class="c1">#include &lt;STDLIB.H&gt;</span>"""
+ assert_count(expected_expr, result, 1)
+
+ # C code block with lang should be translated and be *C* highlighted
+ expected_expr = ("""<span class="cp">#include</span>"""
+ """<span class="w"> </span>"""
+ """<span class="cpf">&lt;STDIO.H&gt;</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # literal block in list item should be translated
+ expected_expr = ("""<span class="no">LITERAL</span>"""
+ """<span class="o">-</span>"""
+ """<span class="no">BLOCK</span>\n"""
+ """<span class="no">IN</span>"""
+ """<span class="w"> </span>"""
+ """<span class="no">LIST</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # doctest block should not be translated but be highlighted
+ expected_expr = (
+ """<span class="gp">&gt;&gt;&gt; </span>"""
+ """<span class="kn">import</span> <span class="nn">sys</span> """
+ """<span class="c1"># SYS IMPORTING</span>""")
+ assert_count(expected_expr, result, 1)
+
+ # '#noqa' should remain in literal blocks.
+ assert_count("#noqa", result, 1)
+
+ # [raw.txt]
+
+ result = (app.outdir / 'raw.html').read_text(encoding='utf8')
+
+ # raw block should be translated
+ expected_expr = """<iframe src="HTTPS://SPHINX-DOC.ORG"></iframe></section>"""
+ assert_count(expected_expr, result, 1)
+
+ # [figure.txt]
+
+ result = (app.outdir / 'figure.html').read_text(encoding='utf8')
+
+ # alt and src for image block should be translated
+ expected_expr = """<img alt="I18N -&gt; IMG" src="_images/img.png" />"""
+ assert_count(expected_expr, result, 1)
+
+ # alt and src for figure block should be translated
+ expected_expr = """<img alt="IMG -&gt; I18N" src="_images/i18n.png" />"""
+ assert_count(expected_expr, result, 1)
+
+
+@pytest.mark.sphinx(
+ 'html',
+ testroot='intl_substitution_definitions',
+ confoverrides={
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
+ 'gettext_compact': False,
+ 'gettext_additional_targets': [
+ 'index',
+ 'literal-block',
+ 'doctest-block',
+ 'raw',
+ 'image',
+ ],
+ },
+)
+def test_additional_targets_should_be_translated_substitution_definitions(app):
+ app.build(force_all=True)
+
+ # [prolog_epilog_substitution.txt]
+
+ result = (app.outdir / 'prolog_epilog_substitution.html').read_text(encoding='utf8')
+
+ # alt and src for image block should be translated
+ expected_expr = """<img alt="SUBST_PROLOG_2 TRANSLATED" src="_images/i18n.png" />"""
+ assert_count(expected_expr, result, 1)
+
+ # alt and src for image block should be translated
+ expected_expr = """<img alt="SUBST_EPILOG_2 TRANSLATED" src="_images/img.png" />"""
+ assert_count(expected_expr, result, 1)
+
+
+@sphinx_intl
+@pytest.mark.sphinx('text')
+@pytest.mark.test_params(shared_result='test_intl_basic')
+def test_text_references(app, warning):
+ app.build(filenames=[app.srcdir / 'refs.txt'])
+
+ warnings = warning.getvalue().replace(os.sep, '/')
+ warning_expr = 'refs.txt:\\d+: ERROR: Unknown target name:'
+ assert_count(warning_expr, warnings, 0)
+
+
+@pytest.mark.sphinx(
+ 'text',
+ testroot='intl_substitution_definitions',
+ confoverrides={
+ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'],
+ 'gettext_compact': False,
+ },
+)
+def test_text_prolog_epilog_substitution(app):
+ app.build()
+
+ result = (app.outdir / 'prolog_epilog_substitution.txt').read_text(encoding='utf8')
+
+ assert result == """\
+1. I18N WITH PROLOGUE AND EPILOGUE SUBSTITUTIONS
+************************************************
+
+THIS IS CONTENT THAT CONTAINS prologue substitute text.
+
+SUBSTITUTED IMAGE [image: SUBST_PROLOG_2 TRANSLATED][image] HERE.
+
+THIS IS CONTENT THAT CONTAINS epilogue substitute text.
+
+SUBSTITUTED IMAGE [image: SUBST_EPILOG_2 TRANSLATED][image] HERE.
+"""
+
+
+@pytest.mark.sphinx(
+ 'dummy', testroot='images',
+ srcdir='test_intl_images',
+ confoverrides={'language': _CATALOG_LOCALE},
+)
+def test_image_glob_intl(app):
+ app.build()
+
+ # index.rst
+ doctree = app.env.get_doctree('index')
+ assert_node(doctree[0][1], nodes.image, uri='rimg.xx.png',
+ candidates={'*': 'rimg.xx.png'})
+
+ assert isinstance(doctree[0][2], nodes.figure)
+ assert_node(doctree[0][2][0], nodes.image, uri='rimg.xx.png',
+ candidates={'*': 'rimg.xx.png'})
+
+ assert_node(doctree[0][3], nodes.image, uri='img.*',
+ candidates={'application/pdf': 'img.pdf',
+ 'image/gif': 'img.gif',
+ 'image/png': 'img.png'})
+
+ assert isinstance(doctree[0][4], nodes.figure)
+ assert_node(doctree[0][4][0], nodes.image, uri='img.*',
+ candidates={'application/pdf': 'img.pdf',
+ 'image/gif': 'img.gif',
+ 'image/png': 'img.png'})
+
+ # subdir/index.rst
+ doctree = app.env.get_doctree('subdir/index')
+ assert_node(doctree[0][1], nodes.image, uri='subdir/rimg.xx.png',
+ candidates={'*': 'subdir/rimg.xx.png'})
+
+ assert_node(doctree[0][2], nodes.image, uri='subdir/svgimg.*',
+ candidates={'application/pdf': 'subdir/svgimg.pdf',
+ 'image/svg+xml': 'subdir/svgimg.xx.svg'})
+
+ assert isinstance(doctree[0][3], nodes.figure)
+ assert_node(doctree[0][3][0], nodes.image, uri='subdir/svgimg.*',
+ candidates={'application/pdf': 'subdir/svgimg.pdf',
+ 'image/svg+xml': 'subdir/svgimg.xx.svg'})
+
+
+@pytest.mark.sphinx(
+ 'dummy', testroot='images',
+ srcdir='test_intl_images',
+ confoverrides={
+ 'language': _CATALOG_LOCALE,
+ 'figure_language_filename': '{root}{ext}.{language}',
+ },
+)
+def test_image_glob_intl_using_figure_language_filename(app):
+ app.build()
+
+ # index.rst
+ doctree = app.env.get_doctree('index')
+ assert_node(doctree[0][1], nodes.image, uri='rimg.png.xx',
+ candidates={'*': 'rimg.png.xx'})
+
+ assert isinstance(doctree[0][2], nodes.figure)
+ assert_node(doctree[0][2][0], nodes.image, uri='rimg.png.xx',
+ candidates={'*': 'rimg.png.xx'})
+
+ assert_node(doctree[0][3], nodes.image, uri='img.*',
+ candidates={'application/pdf': 'img.pdf',
+ 'image/gif': 'img.gif',
+ 'image/png': 'img.png'})
+
+ assert isinstance(doctree[0][4], nodes.figure)
+ assert_node(doctree[0][4][0], nodes.image, uri='img.*',
+ candidates={'application/pdf': 'img.pdf',
+ 'image/gif': 'img.gif',
+ 'image/png': 'img.png'})
+
+ # subdir/index.rst
+ doctree = app.env.get_doctree('subdir/index')
+ assert_node(doctree[0][1], nodes.image, uri='subdir/rimg.png',
+ candidates={'*': 'subdir/rimg.png'})
+
+ assert_node(doctree[0][2], nodes.image, uri='subdir/svgimg.*',
+ candidates={'application/pdf': 'subdir/svgimg.pdf',
+ 'image/svg+xml': 'subdir/svgimg.svg'})
+
+ assert isinstance(doctree[0][3], nodes.figure)
+ assert_node(doctree[0][3][0], nodes.image, uri='subdir/svgimg.*',
+ candidates={'application/pdf': 'subdir/svgimg.pdf',
+ 'image/svg+xml': 'subdir/svgimg.svg'})
+
+
+def getwarning(warnings):
+ return strip_colors(warnings.getvalue().replace(os.sep, '/'))
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ srcdir='gettext_allow_fuzzy_translations',
+ confoverrides={
+ 'language': 'de',
+ 'gettext_allow_fuzzy_translations': True,
+ })
+def test_gettext_allow_fuzzy_translations(app):
+ locale_dir = app.srcdir / 'locales' / 'de' / 'LC_MESSAGES'
+ locale_dir.mkdir(parents=True, exist_ok=True)
+ with (locale_dir / 'index.po').open('wb') as f:
+ catalog = Catalog()
+ catalog.add('features', 'FEATURES', flags=('fuzzy',))
+ pofile.write_po(f, catalog)
+
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert 'FEATURES' in content
+
+
+@pytest.mark.sphinx('html', testroot='basic',
+ srcdir='gettext_disallow_fuzzy_translations',
+ confoverrides={
+ 'language': 'de',
+ 'gettext_allow_fuzzy_translations': False,
+ })
+def test_gettext_disallow_fuzzy_translations(app):
+ locale_dir = app.srcdir / 'locales' / 'de' / 'LC_MESSAGES'
+ locale_dir.mkdir(parents=True, exist_ok=True)
+ with (locale_dir / 'index.po').open('wb') as f:
+ catalog = Catalog()
+ catalog.add('features', 'FEATURES', flags=('fuzzy',))
+ pofile.write_po(f, catalog)
+
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert 'FEATURES' not in content
+
+
+@pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'})
+def test_customize_system_message(make_app, app_params):
+ try:
+ # clear translators cache
+ locale.translators.clear()
+
+ # prepare message catalog (.po)
+ locale_dir = app_params.kwargs['srcdir'] / 'locales' / 'de' / 'LC_MESSAGES'
+ locale_dir.mkdir(parents=True, exist_ok=True)
+ with (locale_dir / 'sphinx.po').open('wb') as f:
+ catalog = Catalog()
+ catalog.add('Quick search', 'QUICK SEARCH')
+ pofile.write_po(f, catalog)
+
+ # construct application and convert po file to .mo
+ args, kwargs = app_params
+ app = make_app(*args, **kwargs)
+ assert (locale_dir / 'sphinx.mo').exists()
+ assert app.translator.gettext('Quick search') == 'QUICK SEARCH'
+
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert 'QUICK SEARCH' in content
+ finally:
+ locale.translators.clear()
+
+
+@pytest.mark.sphinx('html', testroot='intl', confoverrides={'today_fmt': '%Y-%m-%d'})
+def test_customize_today_date_format(app, monkeypatch):
+ with monkeypatch.context() as m:
+ m.setenv('SOURCE_DATE_EPOCH', '1439131307')
+ app.build()
+ content = (app.outdir / 'refs.html').read_text(encoding='utf8')
+
+ assert '2015-08-09' in content
diff --git a/tests/test_intl/test_locale.py b/tests/test_intl/test_locale.py
new file mode 100644
index 0000000..11dd95d
--- /dev/null
+++ b/tests/test_intl/test_locale.py
@@ -0,0 +1,76 @@
+"""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'