From 45bcdf802738d3baa8286baddf5b1d7a384dda5e Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 24 Jul 2023 16:46:04 +0200 Subject: [PATCH 1/9] Documentation about `translated=True` attribute on nodes Minimal explanation with a full extension example about how the `translated=True` attribute injected by `sphinx.transforms.i18n.Locale` can be used to extend Sphinx's functionality. Related #1246 --- doc/usage/advanced/intl.rst | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst index 1a98ebb1f31..c8522eea76b 100644 --- a/doc/usage/advanced/intl.rst +++ b/doc/usage/advanced/intl.rst @@ -340,6 +340,120 @@ There is a `sphinx translation page`_ for Sphinx (master) documentation. Detail is here: https://docs.transifex.com/getting-started-1/translators + +Using Sphinx's internals to extend translations functionality +------------------------------------------------------------- + +.. versionadded:: 7.1.0 + +During the build process, Sphinx marks each translated node that with a ``translated=True`` attribute, +meaning it was able to find the translated version of the paragraph to the target language. +Developers and users with coding knowledge, can be benefit from this attribute to build extensions around translations. + +The following example shows a minimal Sphinx extension that covers 3 different usage for this attribute: + +* Mark untranslated paragraphs with a different background color using +* Calculate translated percentage per page and total +* Use a custom substitution to show the translated percentage of the page + + +.. code:: python + :caption: translated.py + + import docutils + from sphinx.transforms import SphinxTransform + + + class TranslationsManipulation(SphinxTransform): + default_priority = 50 + + def apply(self, **kwargs): + filename = self.document.get('source') # absolute source filename + + # Default values for current source filename + self.app._translations[filename] = { + 'total': 0, + 'translated': 0, + } + + # Traverse all the nodes of the document + for node in self.document.traverse(): + if not hasattr(node, 'get'): + # Discard nodes we cannot access to its attributes + continue + + if any([isinstance(child, docutils.nodes.Text) for child in node.children]): + # Only work over nodes with a text child + if node.get('translated', False): + # Increase the translated nodes + self.app._translations[filename]['translated'] += 1 + css_class = self.app.env.config.translated_class + else: + css_class = self.app.env.config.untranslated_class + + # Append our custom untranslated CSS class to the node + classes = node.get('classes', []) + classes.append(css_class) + node.replace_attr('classes', classes) + + # Increase the total of nodes + self.app._translations[filename]['total'] += 1 + + + # Calculate total percentage of the page translated + self.app._translations[filename]['percentage'] = ( + self.app._translations[filename]['translated'] / + self.app._translations[filename]['total'] + ) * 100 + + # Handle substitutions (used as ``|translated-page-percentage|`` in .rst source files) + substitution = 'translated-page-percentage' + for ref in self.document.findall(docutils.nodes.substitution_reference): + refname = ref['refname'] + if refname == substitution: + text = self.app._translations[filename]['percentage'] + newnode = docutils.nodes.Text(text) + if 'classes' in ref: + ref.replace_attr('classes', []) + ref.replace_self(newnode) + + + def setup(app): + """ + Setup ``translated`` Sphinx extension. + """ + # CSS class to add to translated nodes + app.add_config_value('translated_class', 'translated', 'env') + app.add_config_value('untranslated_class', 'untranslated', 'env') + + # Add the CSS file with our custom styles + app.add_css_file('translated.css') + + app.add_transform(TranslationsManipulation) + + # Define an internal variable to store translated percentages + app._translations = {} + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } + +The ``.css`` added looks like the following: + +.. code:: css + :caption: _static/translated.css + + .translated { + background-color: rgba(0, 255, 0, .20) + } + + .untranslated { + background-color: rgba(255, 0, 0, .20) + } + + .. rubric:: Footnotes .. [1] See the `GNU gettext utilities From 734f5880ba338457214efc041f1760357081e806 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:16:07 +0100 Subject: [PATCH 2/9] :caption: doesn't work on ``.. code::`` --- doc/usage/advanced/intl.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst index c8522eea76b..ee5dcc2866c 100644 --- a/doc/usage/advanced/intl.rst +++ b/doc/usage/advanced/intl.rst @@ -357,7 +357,7 @@ The following example shows a minimal Sphinx extension that covers 3 different u * Use a custom substitution to show the translated percentage of the page -.. code:: python +.. code-block:: python :caption: translated.py import docutils @@ -442,7 +442,7 @@ The following example shows a minimal Sphinx extension that covers 3 different u The ``.css`` added looks like the following: -.. code:: css +.. code-block:: css :caption: _static/translated.css .translated { From 153ba7b8cf1eccd4de5d9004d029573bc594ef20 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:37:07 +0100 Subject: [PATCH 3/9] Add translation progress features --- doc/usage/advanced/intl.rst | 117 ++---------------- doc/usage/configuration.rst | 16 +++ doc/usage/restructuredtext/roles.rst | 6 + sphinx/config.py | 4 +- sphinx/environment/__init__.py | 2 +- sphinx/themes/basic/static/basic.css_t | 8 ++ sphinx/transforms/__init__.py | 20 ++- sphinx/transforms/i18n.py | 72 ++++++++++- tests/roots/test-intl/index.txt | 1 + .../roots/test-intl/translation_progress.txt | 36 ++++++ .../xx/LC_MESSAGES/translation_progress.po | 48 +++++++ tests/test_intl.py | 63 ++++++++-- 12 files changed, 271 insertions(+), 122 deletions(-) create mode 100644 tests/roots/test-intl/translation_progress.txt create mode 100644 tests/roots/test-intl/xx/LC_MESSAGES/translation_progress.po diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst index ee5dcc2866c..64c74872e2d 100644 --- a/doc/usage/advanced/intl.rst +++ b/doc/usage/advanced/intl.rst @@ -341,118 +341,21 @@ There is a `sphinx translation page`_ for Sphinx (master) documentation. Detail is here: https://docs.transifex.com/getting-started-1/translators -Using Sphinx's internals to extend translations functionality -------------------------------------------------------------- +Translation progress and statistics +----------------------------------- .. versionadded:: 7.1.0 -During the build process, Sphinx marks each translated node that with a ``translated=True`` attribute, -meaning it was able to find the translated version of the paragraph to the target language. -Developers and users with coding knowledge, can be benefit from this attribute to build extensions around translations. - -The following example shows a minimal Sphinx extension that covers 3 different usage for this attribute: - -* Mark untranslated paragraphs with a different background color using -* Calculate translated percentage per page and total -* Use a custom substitution to show the translated percentage of the page - - -.. code-block:: python - :caption: translated.py - - import docutils - from sphinx.transforms import SphinxTransform - - - class TranslationsManipulation(SphinxTransform): - default_priority = 50 - - def apply(self, **kwargs): - filename = self.document.get('source') # absolute source filename - - # Default values for current source filename - self.app._translations[filename] = { - 'total': 0, - 'translated': 0, - } - - # Traverse all the nodes of the document - for node in self.document.traverse(): - if not hasattr(node, 'get'): - # Discard nodes we cannot access to its attributes - continue - - if any([isinstance(child, docutils.nodes.Text) for child in node.children]): - # Only work over nodes with a text child - if node.get('translated', False): - # Increase the translated nodes - self.app._translations[filename]['translated'] += 1 - css_class = self.app.env.config.translated_class - else: - css_class = self.app.env.config.untranslated_class - - # Append our custom untranslated CSS class to the node - classes = node.get('classes', []) - classes.append(css_class) - node.replace_attr('classes', classes) - - # Increase the total of nodes - self.app._translations[filename]['total'] += 1 - - - # Calculate total percentage of the page translated - self.app._translations[filename]['percentage'] = ( - self.app._translations[filename]['translated'] / - self.app._translations[filename]['total'] - ) * 100 - - # Handle substitutions (used as ``|translated-page-percentage|`` in .rst source files) - substitution = 'translated-page-percentage' - for ref in self.document.findall(docutils.nodes.substitution_reference): - refname = ref['refname'] - if refname == substitution: - text = self.app._translations[filename]['percentage'] - newnode = docutils.nodes.Text(text) - if 'classes' in ref: - ref.replace_attr('classes', []) - ref.replace_self(newnode) - - - def setup(app): - """ - Setup ``translated`` Sphinx extension. - """ - # CSS class to add to translated nodes - app.add_config_value('translated_class', 'translated', 'env') - app.add_config_value('untranslated_class', 'untranslated', 'env') - - # Add the CSS file with our custom styles - app.add_css_file('translated.css') - - app.add_transform(TranslationsManipulation) - - # Define an internal variable to store translated percentages - app._translations = {} - - return { - 'version': '0.1', - 'parallel_read_safe': True, - 'parallel_write_safe': True, - } - -The ``.css`` added looks like the following: - -.. code-block:: css - :caption: _static/translated.css - - .translated { - background-color: rgba(0, 255, 0, .20) - } +During the rendering process, +Sphinx marks each translatable node with a ``translated`` attribute, +indicating if a translation was found for the text in that node. - .untranslated { - background-color: rgba(255, 0, 0, .20) - } +The :confval:`translation_progress_classes` configuration value +can be used to add a class to each element, +depending on the value of the ``translated`` attribute. +The :ref:`|translation progress|` substitution can be used to display the +percentage of nodes that have been translated on a per-document basis. .. rubric:: Footnotes diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 30c552a6c0f..ec88b065be1 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -1002,6 +1002,22 @@ documentation on :ref:`intl` for details. .. versionchanged:: 3.2 Added ``{docpath}`` token. +.. confval:: translation_progress_classes + + Control which, if any, classes are added to indicate translation progress. + This setting would likely only be used by translators of documentation, + in order to quickly indicate translated and untranslated content. + + * ``True``: add ``translated`` and ``untranslated`` classes + to all nodes with translatable content. + * ``translated``: only add the ``translated`` class. + * ``untranslated``: only add the ``untranslated`` class. + * ``False``: do not add any classes to indicate translation progress. + + Defaults to ``False``. + + .. versionadded:: 7.1 + .. _math-options: diff --git a/doc/usage/restructuredtext/roles.rst b/doc/usage/restructuredtext/roles.rst index 62fcaa4c4dd..e468de9c244 100644 --- a/doc/usage/restructuredtext/roles.rst +++ b/doc/usage/restructuredtext/roles.rst @@ -528,3 +528,9 @@ default. They are set in the build configuration file. Replaced by either today's date (the date on which the document is read), or the date set in the build configuration file. Normally has the format ``April 14, 2007``. Set by :confval:`today_fmt` and :confval:`today`. + +.. describe:: |translation progress| + + Replaced by the translation progress of the document. + This substitution is intented for use by document translators + as a marker for the translation progress of the document. diff --git a/sphinx/config.py b/sphinx/config.py index b8cf1eda2ca..8b8a136e185 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -58,7 +58,7 @@ class ENUM: Example: app.add_config_value('latex_show_urls', 'no', None, ENUM('no', 'footnote', 'inline')) """ - def __init__(self, *candidates: str) -> None: + def __init__(self, *candidates: str | bool) -> None: self.candidates = candidates def match(self, value: str | list | tuple) -> bool: @@ -101,6 +101,8 @@ class Config: 'locale_dirs': (['locales'], 'env', []), 'figure_language_filename': ('{root}.{language}{ext}', 'env', [str]), 'gettext_allow_fuzzy_translations': (False, 'gettext', []), + 'translation_progress_classes': (False, 'env', + ENUM(True, False, 'translated', 'untranslated')), 'master_doc': ('index', 'env', []), 'root_doc': (lambda config: config.master_doc, 'env', []), diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index d2beffdd9db..1d34e867a1e 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -629,7 +629,7 @@ def get_and_resolve_doctree( prune=prune_toctrees, includehidden=includehidden) if result is None: - toctreenode.replace_self([]) + toctreenode.parent.replace(toctreenode, []) else: toctreenode.replace_self(result) diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index 9ae18026720..d816abc3f1d 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -748,6 +748,14 @@ abbr, acronym { cursor: help; } +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + /* -- code displays --------------------------------------------------------- */ pre { diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 4cfc2b5bcaa..be38c6f244f 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -34,6 +34,7 @@ 'version', 'release', 'today', + 'translation progress', } @@ -103,7 +104,11 @@ def apply(self, **kwargs: Any) -> None: for ref in self.document.findall(nodes.substitution_reference): refname = ref['refname'] if refname in to_handle: - text = self.config[refname] + if refname == 'translation progress': + # special handling: calculate translation progress + text = _calculate_translation_progress(self.document) + else: + text = self.config[refname] if refname == 'today' and not text: # special handling: can also specify a strftime format text = format_date(self.config.today_fmt or _('%b %d, %Y'), @@ -111,6 +116,19 @@ def apply(self, **kwargs: Any) -> None: ref.replace_self(nodes.Text(text)) +def _calculate_translation_progress(document: nodes.document) -> str: + try: + translation_progress = document['translation_progress'] + except KeyError: + return _('could not calculate translation progress!') + + total = translation_progress['total'] + translated = translation_progress['translated'] + if total <= 0: + return _('no translated elements!') + return f'{translated / total:.2%}' + + class MoveModuleTargets(SphinxTransform): """ Move module targets that are the first thing in a section to the section diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index c412a0d12fb..d95bf69417a 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -14,6 +14,7 @@ from sphinx import addnodes from sphinx.config import Config from sphinx.domains.std import make_glossary_term, split_term_classifiers +from sphinx.errors import ConfigError from sphinx.locale import __ from sphinx.locale import init as init_locale from sphinx.transforms import SphinxTransform @@ -360,9 +361,9 @@ def apply(self, **kwargs: Any) -> None: if not isinstance(node, LITERAL_TYPE_NODES): msgstr, _ = parse_noqa(msgstr) - # XXX add marker to untranslated parts if not msgstr or msgstr == msg or not msgstr.strip(): # as-of-yet untranslated + node['translated'] = False continue # Avoid "Literal block expected; none found." warnings. @@ -404,10 +405,12 @@ def apply(self, **kwargs: Any) -> None: if processed: updater.update_leaves() node['translated'] = True # to avoid double translation + else: + node['translated'] = False # phase2: translation for node, msg in extract_messages(self.document): - if node.get('translated', False): # to avoid double translation + if node.setdefault('translated', False): # to avoid double translation continue # skip if the node is already translated by phase1 msgstr = catalog.gettext(msg) @@ -417,22 +420,25 @@ def apply(self, **kwargs: Any) -> None: if not isinstance(node, LITERAL_TYPE_NODES): msgstr, noqa = parse_noqa(msgstr) - # XXX add marker to untranslated parts if not msgstr or msgstr == msg: # as-of-yet untranslated + node['translated'] = False continue # update translatable nodes if isinstance(node, addnodes.translatable): node.apply_translated_message(msg, msgstr) # type: ignore[attr-defined] + node['translated'] = True # node is always an Element continue # update meta nodes if isinstance(node, nodes.meta): # type: ignore[attr-defined] node['content'] = msgstr + node['translated'] = True continue if isinstance(node, nodes.image) and node.get('alt') == msg: node['alt'] = msgstr + node['translated'] = True continue # Avoid "Literal block expected; none found." warnings. @@ -490,6 +496,7 @@ def apply(self, **kwargs: Any) -> None: if isinstance(node, nodes.image) and node.get('alt') != msg: node['uri'] = patch['uri'] + node['translated'] = False continue # do not mark translated node['translated'] = True # to avoid double translation @@ -514,6 +521,63 @@ def apply(self, **kwargs: Any) -> None: node['entries'] = new_entries +class TranslationProgressTotaliser(SphinxTransform): + """ + Calculate the number of translated and untranslated nodes. + """ + default_priority = 25 # MUST happen after Locale + + def apply(self, **kwargs: Any) -> None: + from sphinx.builders.gettext import MessageCatalogBuilder + if isinstance(self.app.builder, MessageCatalogBuilder): + return + + total = translated = 0 + for node in self.document.findall(NodeMatcher(translated=Any)): + total += 1 + if node['translated']: + translated += 1 + + self.document['translation_progress'] = { + 'total': total, + 'translated': translated, + } + + +class AddTranslationClasses(SphinxTransform): + """ + Add ``translated`` or ``untranslated`` classes to indicate translation status. + """ + default_priority = 950 + + def apply(self, **kwargs: Any) -> None: + from sphinx.builders.gettext import MessageCatalogBuilder + if isinstance(self.app.builder, MessageCatalogBuilder): + return + + if not self.config.translation_progress_classes: + return + + if self.config.translation_progress_classes is True: + add_translated = add_untranslated = True + elif self.config.translation_progress_classes == 'translated': + add_translated = True + add_untranslated = False + elif self.config.translation_progress_classes == 'untranslated': + add_translated = False + add_untranslated = True + else: + raise ConfigError('translation_progress_classes must be True, False, "translated" or "untranslated"') + + for node in self.document.findall(NodeMatcher(translated=Any)): + if node['translated']: + if add_translated: + node.setdefault('classes', []).append('translated') + else: + if add_untranslated: + node.setdefault('classes', []).append('untranslated') + + class RemoveTranslatableInline(SphinxTransform): """ Remove inline nodes used for translation as placeholders. @@ -534,6 +598,8 @@ def apply(self, **kwargs: Any) -> None: def setup(app: Sphinx) -> dict[str, Any]: app.add_transform(PreserveTranslatableMessages) app.add_transform(Locale) + app.add_transform(TranslationProgressTotaliser) + app.add_transform(AddTranslationClasses) app.add_transform(RemoveTranslatableInline) return { diff --git a/tests/roots/test-intl/index.txt b/tests/roots/test-intl/index.txt index 1e09294f984..9de15d5463b 100644 --- a/tests/roots/test-intl/index.txt +++ b/tests/roots/test-intl/index.txt @@ -29,6 +29,7 @@ CONTENTS raw refs section + translation_progress topic .. toctree:: diff --git a/tests/roots/test-intl/translation_progress.txt b/tests/roots/test-intl/translation_progress.txt new file mode 100644 index 00000000000..61f893e2245 --- /dev/null +++ b/tests/roots/test-intl/translation_progress.txt @@ -0,0 +1,36 @@ +Translation Progress +==================== + +When, in disgrace with fortune and men’s eyes, + +I all alone beweep my outcast state, + +And trouble deaf heaven with my bootless cries, + +And look upon myself, and curse my fate, + +Wishing me like to one more rich in hope, + +Featur’d like him, like him with friends possess’d, + +Desiring this man’s art and that man’s scope, + +With what I most enjoy contented least; + +Yet in these thoughts myself almost despising, + +Haply I think on thee, and then my state, + +Like to the lark at break of day arising + +.. untranslated (3 out of 14 lines): + +From sullen earth, sings hymns at heaven’s gate; + +For thy sweet love remember’d such wealth brings + +That then I scorn to change my state with kings. + +.. translation progress substitution + +|translation progress| diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/translation_progress.po b/tests/roots/test-intl/xx/LC_MESSAGES/translation_progress.po new file mode 100644 index 00000000000..a83112f48f6 --- /dev/null +++ b/tests/roots/test-intl/xx/LC_MESSAGES/translation_progress.po @@ -0,0 +1,48 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2000-01-01 00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: \n" +"Language: xx\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Translation Progress" +msgstr "TRANSLATION PROGRESS" + +msgid "When, in disgrace with fortune and men’s eyes," +msgstr "WHEN, IN DISGRACE WITH FORTUNE AND MEN’S EYES," + +msgid "I all alone beweep my outcast state," +msgstr "I ALL ALONE BEWEEP MY OUTCAST STATE," + +msgid "And trouble deaf heaven with my bootless cries," +msgstr "AND TROUBLE DEAF HEAVEN WITH MY BOOTLESS CRIES," + +msgid "And look upon myself, and curse my fate," +msgstr "AND LOOK UPON MYSELF, AND CURSE MY FATE," + +msgid "Wishing me like to one more rich in hope," +msgstr "WISHING ME LIKE TO ONE MORE RICH IN HOPE," + +msgid "Featur’d like him, like him with friends possess’d," +msgstr "FEATUR’D LIKE HIM, LIKE HIM WITH FRIENDS POSSESS’D," + +msgid "Desiring this man’s art and that man’s scope," +msgstr "DESIRING THIS MAN’S ART AND THAT MAN’S SCOPE," + +msgid "With what I most enjoy contented least;" +msgstr "WITH WHAT I MOST ENJOY CONTENTED LEAST;" + +msgid "Yet in these thoughts myself almost despising," +msgstr "YET IN THESE THOUGHTS MYSELF ALMOST DESPISING," + +msgid "Haply I think on thee, and then my state," +msgstr "HAPLY I THINK ON THEE, AND THEN MY STATE," + +msgid "Like to the lark at break of day arising" +msgstr "LIKE TO THE LARK AT BREAK OF DAY ARISING" diff --git a/tests/test_intl.py b/tests/test_intl.py index 45cf8f5436d..b2a5797907c 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -23,6 +23,7 @@ path, strip_escseq, ) +from sphinx.util.nodes import NodeMatcher sphinx_intl = pytest.mark.sphinx( testroot='intl', @@ -619,14 +620,58 @@ def test_gettext_buildr_ignores_only_directive(app): def test_node_translated_attribute(app): app.build() - expected = 23 - translated_nodes = 0 + doctree = app.env.get_doctree('translation_progress') - doctree = app.env.get_doctree('admonitions') - for node in doctree.findall(): - if hasattr(node, 'get') and node.get('translated', False): - translated_nodes += 1 - assert translated_nodes == expected + translated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=True))) + assert translated_nodes == 11+1 # 11 lines + title + + untranslated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=False))) + assert untranslated_nodes == 3+1 # 3 lines + substitution reference + + +@sphinx_intl +def test_translation_progress_substitution(app): + app.build() + + doctree = app.env.get_doctree('translation_progress') + + assert doctree[0][17][0] == '75.00%' # 12 out of 16 lines are translated + + +@pytest.mark.sphinx(testroot='intl', freshenv=True, confoverrides={ + 'language': 'xx', 'locale_dirs': ['.'], + 'gettext_compact': False, + 'translation_progress_classes': True, +}) +def test_translation_progress_classes_true(app): + app.build() + + doctree = app.env.get_doctree('translation_progress') + + assert 'translated' in doctree[0][0]['classes'] + 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 'translated' in doctree[0][9]['classes'] + assert 'translated' in doctree[0][10]['classes'] + assert 'translated' in doctree[0][11]['classes'] + + assert doctree[0][12]['classes'] == [] # comment node + + assert 'untranslated' in doctree[0][13]['classes'] + assert 'untranslated' in doctree[0][14]['classes'] + assert 'untranslated' in doctree[0][15]['classes'] + + assert doctree[0][16]['classes'] == [] # comment node + + assert 'untranslated' in doctree[0][17]['classes'] + + assert len(doctree[0]) == 18 @sphinx_intl @@ -682,9 +727,9 @@ def test_html_meta(app): app.build() # --- test for meta result = (app.outdir / 'index.html').read_text(encoding='utf8') - expected_expr = '' + expected_expr = '' assert expected_expr in result - expected_expr = '' + expected_expr = '' assert expected_expr in result expected_expr = '

HIDDEN TOC

' assert expected_expr in result From 97850297d41bf319b162aa3e173bc8e0eaf3010a Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:41:57 +0100 Subject: [PATCH 4/9] Add CHANGES note --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index da6ba170357..3e948b06c14 100644 --- a/CHANGES +++ b/CHANGES @@ -53,6 +53,11 @@ Features added via :confval:`linkcheck_anchors_ignore_for_url` while still checking the validity of the page itself. Patch by Bénédikt Tran +* #1246: Add translation progress statistics and inspection support, + via a new substitution (:ref:`|translation progress|`) and a new + configuration variable (:confval:`translation_progress_classes`). + These enable determining the percentage of translated elements within + a document, and the remaining translated and untranslated elements. Bugs fixed ---------- From a3c573fdd483e489744bf8d80cac5681a7d82694 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:45:46 +0100 Subject: [PATCH 5/9] Whitespace --- doc/usage/configuration.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index ec88b065be1..aec911a292f 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -1018,7 +1018,6 @@ documentation on :ref:`intl` for details. .. versionadded:: 7.1 - .. _math-options: Options for Math From e3dcfc21f2d36a1615f54153c8485e4b601bc826 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:47:29 +0100 Subject: [PATCH 6/9] Add types --- sphinx/transforms/i18n.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index d95bf69417a..8b984b0710c 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -427,7 +427,7 @@ def apply(self, **kwargs: Any) -> None: # update translatable nodes if isinstance(node, addnodes.translatable): node.apply_translated_message(msg, msgstr) # type: ignore[attr-defined] - node['translated'] = True # node is always an Element + node['translated'] = True # type: ignore[index] # node is always an Element continue # update meta nodes @@ -533,7 +533,7 @@ def apply(self, **kwargs: Any) -> None: return total = translated = 0 - for node in self.document.findall(NodeMatcher(translated=Any)): + for node in self.document.findall(NodeMatcher(translated=Any)): # type: nodes.Element total += 1 if node['translated']: translated += 1 @@ -567,9 +567,10 @@ def apply(self, **kwargs: Any) -> None: add_translated = False add_untranslated = True else: - raise ConfigError('translation_progress_classes must be True, False, "translated" or "untranslated"') + raise ConfigError('translation_progress_classes must be' + ' True, False, "translated" or "untranslated"') - for node in self.document.findall(NodeMatcher(translated=Any)): + for node in self.document.findall(NodeMatcher(translated=Any)): # type: nodes.Element if node['translated']: if add_translated: node.setdefault('classes', []).append('translated') From 1c7751c36b3b503d2a12f70004397d48865b97d1 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:47:48 +0100 Subject: [PATCH 7/9] whitespace --- tests/test_intl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_intl.py b/tests/test_intl.py index b2a5797907c..a471ca3cacd 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -623,10 +623,10 @@ def test_node_translated_attribute(app): doctree = app.env.get_doctree('translation_progress') translated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=True))) - assert translated_nodes == 11+1 # 11 lines + title + assert translated_nodes == 11 + 1 # 11 lines + title untranslated_nodes = sum(1 for _ in doctree.findall(NodeMatcher(translated=False))) - assert untranslated_nodes == 3+1 # 3 lines + substitution reference + assert untranslated_nodes == 3 + 1 # 3 lines + substitution reference @sphinx_intl From d916d89c13c3f2813d5d9cf1494c9e2f131da428 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:53:59 +0100 Subject: [PATCH 8/9] cross-referencing --- CHANGES | 2 +- doc/usage/advanced/intl.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 3e948b06c14..8e0b7616e88 100644 --- a/CHANGES +++ b/CHANGES @@ -54,7 +54,7 @@ Features added still checking the validity of the page itself. Patch by Bénédikt Tran * #1246: Add translation progress statistics and inspection support, - via a new substitution (:ref:`|translation progress|`) and a new + via a new substitution (``|translation progress|``) and a new configuration variable (:confval:`translation_progress_classes`). These enable determining the percentage of translated elements within a document, and the remaining translated and untranslated elements. diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst index 64c74872e2d..ae6e7dc9d6b 100644 --- a/doc/usage/advanced/intl.rst +++ b/doc/usage/advanced/intl.rst @@ -354,7 +354,7 @@ The :confval:`translation_progress_classes` configuration value can be used to add a class to each element, depending on the value of the ``translated`` attribute. -The :ref:`|translation progress|` substitution can be used to display the +The ``|translation progress|`` substitution can be used to display the percentage of nodes that have been translated on a per-document basis. .. rubric:: Footnotes From f166b4f479f2dbc36875d724afc86388d61227b7 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 24 Jul 2023 20:07:33 +0100 Subject: [PATCH 9/9] Tests --- sphinx/transforms/i18n.py | 2 -- tests/test_intl.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 8b984b0710c..d16b8f75488 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -427,7 +427,6 @@ def apply(self, **kwargs: Any) -> None: # update translatable nodes if isinstance(node, addnodes.translatable): node.apply_translated_message(msg, msgstr) # type: ignore[attr-defined] - node['translated'] = True # type: ignore[index] # node is always an Element continue # update meta nodes @@ -438,7 +437,6 @@ def apply(self, **kwargs: Any) -> None: if isinstance(node, nodes.image) and node.get('alt') == msg: node['alt'] = msgstr - node['translated'] = True continue # Avoid "Literal block expected; none found." warnings. diff --git a/tests/test_intl.py b/tests/test_intl.py index a471ca3cacd..e451a664e27 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -522,7 +522,7 @@ def test_text_toctree(app): # --- toctree (toctree.rst) result = (app.outdir / 'toctree.txt').read_text(encoding='utf8') expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') - for expect_msg in [m for m in expect if m.id]: + for expect_msg in (m for m in expect if m.id): assert expect_msg.string in result @@ -727,9 +727,9 @@ def test_html_meta(app): app.build() # --- test for meta result = (app.outdir / 'index.html').read_text(encoding='utf8') - expected_expr = '' + expected_expr = '' assert expected_expr in result - expected_expr = '' + expected_expr = '' assert expected_expr in result expected_expr = '

HIDDEN TOC

' assert expected_expr in result