""" test_intl ~~~~~~~~~ Test message patching for internationalization purposes. Runs the text builder in the test root. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import os import re import pytest from babel.messages import pofile, mofile from babel.messages.catalog import Catalog from docutils import nodes from sphinx import locale from sphinx.testing.util import ( path, etree_parse, strip_escseq, assert_re_search, assert_not_re_search, assert_startswith, assert_node ) sphinx_intl = pytest.mark.sphinx( testroot='intl', confoverrides={ 'language': 'xx', 'locale_dirs': ['.'], 'gettext_compact': False, }, ) def read_po(pathname): with pathname.open() as f: return pofile.read_po(f) def write_mo(pathname, po): with pathname.open('wb') as f: return mofile.write_mo(f, po) @pytest.fixture(autouse=True) def setup_intl(app_params): srcdir = path(app_params.kwargs['srcdir']) for dirpath, dirs, files in os.walk(srcdir): dirpath = path(dirpath) for f in [f for f in files if f.endswith('.po')]: po = dirpath / f mo = srcdir / 'xx' / 'LC_MESSAGES' / ( os.path.relpath(po[:-3], srcdir) + '.mo') if not mo.parent.exists(): mo.parent.makedirs() if not mo.exists() or mo.stat().st_mtime < po.stat().st_mtime: # compile .mo file only if needed write_mo(mo, read_po(po)) @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_toctree(app): app.build() result = (app.outdir / 'index.txt').read_text() assert_startswith(result, "CONTENTS\n********\n\nTABLE OF CONTENTS\n") @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::1: ' 'WARNING: Inline literal start-string without end-string.\n') assert_re_search(warning_expr, warnings) @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() expect = ("3. I18N WITH REST WARNINGS" "\n**************************\n" "\nLINE OF >>``<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') @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() def wrap(tag, keyword): start_tag = "<%s[^>]*>" % tag end_tag = "" % tag return r"%s\s*%s\s*%s" % (start_tag, keyword, end_tag) def wrap_nest(parenttag, childtag, keyword): start_tag1 = "<%s[^>]*>" % parenttag start_tag2 = "<%s[^>]*>" % childtag return r"%s\s*%s\s*%s" % (start_tag1, keyword, 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'), wrap('a', 'MODULE'), wrap('a', 'KEYWORD'), wrap('a', 'OPERATOR'), wrap('a', 'OBJECT'), wrap('a', 'EXCEPTION'), wrap('a', 'STATEMENT'), wrap('a', 'BUILTIN'), ] for expr in expected_exprs: assert_re_search(expr, result, re.M) @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() 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 expect1 == matched_content expect2 = ( """

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

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

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

\n""") matched_content = get_content(result, "versionchanged") assert expect3 == 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() @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() 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.builder.build_update() app.env.find_files(app.config, app.builder) _, updated, _ = app.env.get_outdated_files(config_changed=False) assert len(updated) == 0 mtime = (app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').stat().st_mtime (app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').utime((mtime + 5, mtime + 5)) app.env.find_files(app.config, app.builder) _, updated, _ = app.env.get_outdated_files(config_changed=False) assert len(updated) == 1 @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) @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', '.'], ['http://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', '.'], ['http://sphinx-doc.org']) # unnamed link check assert_elem( para0[3], ['UNNAMED', 'LINK', '.'], ['http://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', '.'], ['http://python.org', 'http://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', 'http://python.org/index.html', 'http://sphinx-doc.org', 'http://google.com', 'http://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() # 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 = ( """>>> """ """import sys """ """# sys importing""") assert_count(expected_expr, result, 1) # [raw.txt] result = (app.outdir / 'raw.html').read_text() # raw block should not be translated expected_expr = """""" assert_count(expected_expr, result, 1) # [figure.txt] result = (app.outdir / 'figure.html').read_text() # alt and src for image block should not be translated expected_expr = """i18n""" assert_count(expected_expr, result, 1) # alt and src for figure block should not be translated expected_expr = """img""" assert_count(expected_expr, result, 1) @sphinx_intl @pytest.mark.sphinx( 'html', srcdir='test_additional_targets_should_be_translated', confoverrides={ 'language': 'xx', '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() # 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 = ( """>>> """ """import sys """ """# SYS IMPORTING""") assert_count(expected_expr, result, 1) # [raw.txt] result = (app.outdir / 'raw.html').read_text() # raw block should be translated expected_expr = """""" assert_count(expected_expr, result, 1) # [figure.txt] result = (app.outdir / 'figure.html').read_text() # 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) @sphinx_intl @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_references(app, warning): app.builder.build_specific([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( 'dummy', testroot='images', srcdir='test_intl_images', confoverrides={'language': 'xx'} ) @pytest.mark.xfail(os.name != 'posix', reason="Not working on windows") 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': 'xx', 'figure_language_filename': '{root}{ext}.{language}', } ) @pytest.mark.xfail(os.name != 'posix', reason="Not working on windows") 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_escseq(warnings.getvalue().replace(os.sep, '/')) @pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'}) def test_customize_system_message(make_app, app_params, sphinx_test_tempdir): try: # clear translators cache locale.translators.clear() # prepare message catalog (.po) locale_dir = sphinx_test_tempdir / 'basic' / 'locales' / 'de' / 'LC_MESSAGES' locale_dir.makedirs() 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() assert 'QUICK SEARCH' in content finally: locale.translators.clear()