"""Test message patching for internationalization purposes. Runs the text builder in the test root. """ from __future__ import annotations import os import re import shutil import time from typing import TYPE_CHECKING import pygments import pytest from babel.messages import mofile, pofile from babel.messages.catalog import Catalog from docutils import nodes from sphinx import locale from sphinx._cli.util.errors import strip_escape_sequences from sphinx.testing.util import assert_node, etree_parse from sphinx.util.nodes import NodeMatcher if TYPE_CHECKING: from collections.abc import Iterator from io import StringIO from pathlib import Path _CATALOG_LOCALE = 'xx' sphinx_intl = pytest.mark.sphinx( confoverrides={ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, 'html_sidebars': {'**': ['globaltoc.html']}, # for test_html_meta }, ) 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: Path, value: int) -> int: os.utime(target, ns=(value, value)) return target.stat().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 list(filter(None, map(str.strip, elem.itertext()))) 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', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_emit_warnings(app): app.build() # test warnings in translation warnings = getwarning(app.warning) warning_expr = ( '.*/warnings.txt:4::1: ' 'WARNING: Inline literal start-string without end-string. \\[docutils\\]\n' ) assert re.search(warning_expr, warnings), ( f'{warning_expr!r} did not match {warnings!r}' ) @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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 >>``<:1: ' "WARNING: term not in glossary: 'TERM NOT IN GLOSSARY'" ) assert re.search(expected_warning_expr, warnings), ( f'{expected_warning_expr!r} did not match {warnings!r}' ) @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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 (msg for msg in expect if msg.id): assert expect_msg.string in result @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids # --- toctree (toctree.rst) expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po') actual = read_po(app.outdir / 'toctree.pot') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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 (msg for msg in expect if msg.id): assert expect_msg.string in result @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @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 (msg for msg in expect if msg.id): assert expect_msg.string in result @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_gettext') def test_gettext_glossary_terms(app): 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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids warnings = app.warning.getvalue().replace(os.sep, '/') assert 'term not in glossary' not in warnings @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): if len(expect_msg.id.splitlines()) == 1: # compare translations only labels assert expect_msg.id in actual_msg_ids else: pass # skip code-blocks and literalblocks @sphinx_intl @pytest.mark.sphinx('gettext', testroot='intl') @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') actual_msg_ids = {msg.id for msg in actual if msg.id} # pyright: ignore[reportUnhashable] for expect_msg in (msg for msg in expect if msg.id): assert expect_msg.id in actual_msg_ids @sphinx_intl @pytest.mark.sphinx('html', testroot='intl', copy_test_root=True) 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 @pytest.mark.sphinx('html', testroot='intl', copy_test_root=True) 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( 'html', testroot='intl', freshenv=True, confoverrides={ 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, 'translation_progress_classes': True, }, copy_test_root=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() -> Iterator[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: clock: _MockClock 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 # use the same testroot as 'gettext' since the latter contains less PO files @sphinx_intl @pytest.mark.sphinx( 'dummy', testroot='builder-gettext-dont-rebuild-mo', freshenv=True, copy_test_root=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, copy_test_root=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', testroot='intl') @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 = ( '' ) assert expected_expr in result expected_expr = ( '' ) assert expected_expr in result expected_expr = '

HIDDEN TOC

' assert expected_expr in result @sphinx_intl @pytest.mark.sphinx('html', testroot='intl') @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', testroot='intl') @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 = ( 'reference' ) assert len(re.findall(expected_expr, result)) == 2 expected_expr = 'reference' assert len(re.findall(expected_expr, result)) == 0 expected_expr = ( 'I18N WITH ' 'REFS INCONSISTENCY' ) assert len(re.findall(expected_expr, result)) == 1 @sphinx_intl @pytest.mark.sphinx('html', testroot='intl') @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 = '' % tag return rf'{start_tag}\s*{keyword}\s*{end_tag}' def wrap_nest(parenttag, childtag, keyword): start_tag1 = '<%s[^>]*>' % parenttag start_tag2 = '<%s[^>]*>' % childtag return rf'{start_tag1}\s*{keyword}\s*{start_tag2}' expected_exprs = [ wrap('h2', 'Symbols'), wrap('h2', 'C'), wrap('h2', 'E'), wrap('h2', 'F'), wrap('h2', 'M'), wrap('h2', 'N'), wrap('h2', 'R'), wrap('h2', 'S'), wrap('h2', 'T'), wrap('h2', 'V'), 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', testroot='intl') @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'
\n*(.*?)
' % name, result, re.DOTALL) if matched: return matched.group(1) else: return '' expect1 = ( """

Deprecated since version 1.0: """ """THIS IS THE FIRST PARAGRAPH OF DEPRECATED.

\n""" """

THIS IS THE SECOND PARAGRAPH OF DEPRECATED.

\n""" ) matched_content = get_content(result, 'deprecated') assert matched_content == expect1 expect2 = ( """

Added in version 1.0: """ """THIS IS THE FIRST PARAGRAPH OF VERSIONADDED.

\n""" ) matched_content = get_content(result, 'versionadded') assert matched_content == expect2 expect3 = ( """

Changed in version 1.0: """ """THIS IS THE FIRST PARAGRAPH OF VERSIONCHANGED.

\n""" ) matched_content = get_content(result, 'versionchanged') assert matched_content == expect3 expect4 = ( """

Removed in version 1.0: """ """THIS IS THE FIRST PARAGRAPH OF VERSIONREMOVED.

\n""" ) matched_content = get_content(result, 'versionremoved') assert matched_content == expect4 @sphinx_intl @pytest.mark.sphinx('html', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_xml_footnotes(app): 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(app.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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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', testroot='intl') @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'], ) (sec1_1,) = sec1.findall('section') (title,) = sec1_1.findall('title') assert_elem( title, [ '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'], ) # TODO: how do I link token role to productionlist? assert_elem(para2[5], ['LINK TO', 'token2', 'AND', 'token1', '.'], []) 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', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_xml_warnings(app): app.build() # warnings warnings = getwarning(app.warning) assert warnings.count('term not in glossary') == 1 assert 'undefined label' not in warnings assert 'unknown document' not in warnings @sphinx_intl @pytest.mark.sphinx('xml', testroot='intl') @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('xml', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_xml_strange_markup(app): app.build() et = etree_parse(app.outdir / 'markup.xml') secs = et.findall('section') (subsec1,) = secs[0].findall('section') (title1,) = subsec1.findall('title') assert_elem(title1, ['1. TITLE STARTING WITH 1.']) @sphinx_intl @pytest.mark.sphinx('html', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_additional_targets_should_not_be_translated(app): if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19): sp = ' ' else: sp = ' ' 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 = """'result'""" assert_count(expected_expr, result, 1) # C code block without lang should not be translated and *ruby* highlighted expected_expr = """#include <stdlib.h>""" assert_count(expected_expr, result, 1) # C code block with lang should not be translated but be *C* highlighted expected_expr = ( """#include""" """ """ """<stdio.h>""" ) assert_count(expected_expr, result, 1) # literal block in list item should not be translated expected_expr = ( """literal""" """-""" """block\n""" """in""" """ """ """list""" ) assert_count(expected_expr, result, 1) # doctest block should not be translated but be highlighted expected_expr = ( """>>> """ f"""import{sp}sys """ """# sys importing""" ) 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 = """""" 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 = """I18N -> IMG""" assert_count(expected_expr, result, 1) # src for figure block should not be translated (alt is translated) expected_expr = """IMG -> I18N""" assert_count(expected_expr, result, 1) @sphinx_intl @pytest.mark.sphinx( 'html', testroot='intl', 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): if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19): sp = ' ' else: sp = ' ' app.build() # [literalblock.txt] result = (app.outdir / 'literalblock.html').read_text(encoding='utf8') # basic literal block should be translated expected_expr = ( 'THIS IS\n' 'LITERAL BLOCK' ) assert_count(expected_expr, result, 1) # literalinclude should be translated expected_expr = '"HTTPS://SPHINX-DOC.ORG"' assert_count(expected_expr, result, 1) # 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 = """'RESULT'""" assert_count(expected_expr, result, 1) # C code block without lang should be translated and *ruby* highlighted expected_expr = """#include <STDLIB.H>""" assert_count(expected_expr, result, 1) # C code block with lang should be translated and be *C* highlighted expected_expr = ( """#include""" """ """ """<STDIO.H>""" ) assert_count(expected_expr, result, 1) # literal block in list item should be translated expected_expr = ( """LITERAL""" """-""" """BLOCK\n""" """IN""" """ """ """LIST""" ) assert_count(expected_expr, result, 1) # doctest block should not be translated but be highlighted expected_expr = ( """>>> """ f"""import{sp}sys """ """# SYS IMPORTING""" ) assert_count(expected_expr, result, 1) # 'noqa' comments 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 = """""" 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 = """I18N -> IMG""" assert_count(expected_expr, result, 1) # alt and src for figure block should be translated expected_expr = """IMG -> I18N""" 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', ], }, copy_test_root=True, ) 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 = """SUBST_PROLOG_2 TRANSLATED""" assert_count(expected_expr, result, 1) # alt and src for image block should be translated expected_expr = """SUBST_EPILOG_2 TRANSLATED""" assert_count(expected_expr, result, 1) @sphinx_intl @pytest.mark.sphinx('text', testroot='intl') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_references(app): app.build(filenames=[app.srcdir / 'refs.txt']) warnings = app.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, }, copy_test_root=True, ) 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.usefixtures('_http_teapot') @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.usefixtures('_http_teapot') @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: StringIO) -> str: return strip_escape_sequences(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', 'html_sidebars': {'**': ['searchbox.html']}}, copy_test_root=True, ) 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