Skip to content

Commit 066e0fa

Browse files
AA-Turnerhumitos
andauthored
Add translation progress information (#11509)
Co-authored-by: Manuel Kaufmann <[email protected]>
1 parent 0882914 commit 066e0fa

File tree

13 files changed

+282
-16
lines changed

13 files changed

+282
-16
lines changed

CHANGES

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ Features added
5353
via :confval:`linkcheck_anchors_ignore_for_url` while
5454
still checking the validity of the page itself.
5555
Patch by Bénédikt Tran
56+
* #1246: Add translation progress statistics and inspection support,
57+
via a new substitution (``|translation progress|``) and a new
58+
configuration variable (:confval:`translation_progress_classes`).
59+
These enable determining the percentage of translated elements within
60+
a document, and the remaining translated and untranslated elements.
5661

5762
Bugs fixed
5863
----------

doc/usage/advanced/intl.rst

+17
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,23 @@ There is a `sphinx translation page`_ for Sphinx (master) documentation.
340340

341341
Detail is here: https://docs.transifex.com/getting-started-1/translators
342342

343+
344+
Translation progress and statistics
345+
-----------------------------------
346+
347+
.. versionadded:: 7.1.0
348+
349+
During the rendering process,
350+
Sphinx marks each translatable node with a ``translated`` attribute,
351+
indicating if a translation was found for the text in that node.
352+
353+
The :confval:`translation_progress_classes` configuration value
354+
can be used to add a class to each element,
355+
depending on the value of the ``translated`` attribute.
356+
357+
The ``|translation progress|`` substitution can be used to display the
358+
percentage of nodes that have been translated on a per-document basis.
359+
343360
.. rubric:: Footnotes
344361

345362
.. [1] See the `GNU gettext utilities

doc/usage/configuration.rst

+15
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,21 @@ documentation on :ref:`intl` for details.
10021002
.. versionchanged:: 3.2
10031003
Added ``{docpath}`` token.
10041004

1005+
.. confval:: translation_progress_classes
1006+
1007+
Control which, if any, classes are added to indicate translation progress.
1008+
This setting would likely only be used by translators of documentation,
1009+
in order to quickly indicate translated and untranslated content.
1010+
1011+
* ``True``: add ``translated`` and ``untranslated`` classes
1012+
to all nodes with translatable content.
1013+
* ``translated``: only add the ``translated`` class.
1014+
* ``untranslated``: only add the ``untranslated`` class.
1015+
* ``False``: do not add any classes to indicate translation progress.
1016+
1017+
Defaults to ``False``.
1018+
1019+
.. versionadded:: 7.1
10051020

10061021
.. _math-options:
10071022

doc/usage/restructuredtext/roles.rst

+6
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,9 @@ default. They are set in the build configuration file.
528528
Replaced by either today's date (the date on which the document is read), or
529529
the date set in the build configuration file. Normally has the format
530530
``April 14, 2007``. Set by :confval:`today_fmt` and :confval:`today`.
531+
532+
.. describe:: |translation progress|
533+
534+
Replaced by the translation progress of the document.
535+
This substitution is intented for use by document translators
536+
as a marker for the translation progress of the document.

sphinx/config.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class ENUM:
5858
Example:
5959
app.add_config_value('latex_show_urls', 'no', None, ENUM('no', 'footnote', 'inline'))
6060
"""
61-
def __init__(self, *candidates: str) -> None:
61+
def __init__(self, *candidates: str | bool) -> None:
6262
self.candidates = candidates
6363

6464
def match(self, value: str | list | tuple) -> bool:
@@ -101,6 +101,8 @@ class Config:
101101
'locale_dirs': (['locales'], 'env', []),
102102
'figure_language_filename': ('{root}.{language}{ext}', 'env', [str]),
103103
'gettext_allow_fuzzy_translations': (False, 'gettext', []),
104+
'translation_progress_classes': (False, 'env',
105+
ENUM(True, False, 'translated', 'untranslated')),
104106

105107
'master_doc': ('index', 'env', []),
106108
'root_doc': (lambda config: config.master_doc, 'env', []),

sphinx/environment/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ def get_and_resolve_doctree(
629629
prune=prune_toctrees,
630630
includehidden=includehidden)
631631
if result is None:
632-
toctreenode.replace_self([])
632+
toctreenode.parent.replace(toctreenode, [])
633633
else:
634634
toctreenode.replace_self(result)
635635

sphinx/themes/basic/static/basic.css_t

+8
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,14 @@ abbr, acronym {
748748
cursor: help;
749749
}
750750

751+
.translated {
752+
background-color: rgba(207, 255, 207, 0.2)
753+
}
754+
755+
.untranslated {
756+
background-color: rgba(255, 207, 207, 0.2)
757+
}
758+
751759
/* -- code displays --------------------------------------------------------- */
752760

753761
pre {

sphinx/transforms/__init__.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'version',
3535
'release',
3636
'today',
37+
'translation progress',
3738
}
3839

3940

@@ -103,14 +104,31 @@ def apply(self, **kwargs: Any) -> None:
103104
for ref in self.document.findall(nodes.substitution_reference):
104105
refname = ref['refname']
105106
if refname in to_handle:
106-
text = self.config[refname]
107+
if refname == 'translation progress':
108+
# special handling: calculate translation progress
109+
text = _calculate_translation_progress(self.document)
110+
else:
111+
text = self.config[refname]
107112
if refname == 'today' and not text:
108113
# special handling: can also specify a strftime format
109114
text = format_date(self.config.today_fmt or _('%b %d, %Y'),
110115
language=self.config.language)
111116
ref.replace_self(nodes.Text(text))
112117

113118

119+
def _calculate_translation_progress(document: nodes.document) -> str:
120+
try:
121+
translation_progress = document['translation_progress']
122+
except KeyError:
123+
return _('could not calculate translation progress!')
124+
125+
total = translation_progress['total']
126+
translated = translation_progress['translated']
127+
if total <= 0:
128+
return _('no translated elements!')
129+
return f'{translated / total:.2%}'
130+
131+
114132
class MoveModuleTargets(SphinxTransform):
115133
"""
116134
Move module targets that are the first thing in a section to the section

sphinx/transforms/i18n.py

+68-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from sphinx import addnodes
1515
from sphinx.config import Config
1616
from sphinx.domains.std import make_glossary_term, split_term_classifiers
17+
from sphinx.errors import ConfigError
1718
from sphinx.locale import __
1819
from sphinx.locale import init as init_locale
1920
from sphinx.transforms import SphinxTransform
@@ -360,9 +361,9 @@ def apply(self, **kwargs: Any) -> None:
360361
if not isinstance(node, LITERAL_TYPE_NODES):
361362
msgstr, _ = parse_noqa(msgstr)
362363

363-
# XXX add marker to untranslated parts
364364
if not msgstr or msgstr == msg or not msgstr.strip():
365365
# as-of-yet untranslated
366+
node['translated'] = False
366367
continue
367368

368369
# Avoid "Literal block expected; none found." warnings.
@@ -404,10 +405,12 @@ def apply(self, **kwargs: Any) -> None:
404405
if processed:
405406
updater.update_leaves()
406407
node['translated'] = True # to avoid double translation
408+
else:
409+
node['translated'] = False
407410

408411
# phase2: translation
409412
for node, msg in extract_messages(self.document):
410-
if node.get('translated', False): # to avoid double translation
413+
if node.setdefault('translated', False): # to avoid double translation
411414
continue # skip if the node is already translated by phase1
412415

413416
msgstr = catalog.gettext(msg)
@@ -417,8 +420,8 @@ def apply(self, **kwargs: Any) -> None:
417420
if not isinstance(node, LITERAL_TYPE_NODES):
418421
msgstr, noqa = parse_noqa(msgstr)
419422

420-
# XXX add marker to untranslated parts
421423
if not msgstr or msgstr == msg: # as-of-yet untranslated
424+
node['translated'] = False
422425
continue
423426

424427
# update translatable nodes
@@ -429,6 +432,7 @@ def apply(self, **kwargs: Any) -> None:
429432
# update meta nodes
430433
if isinstance(node, nodes.meta): # type: ignore[attr-defined]
431434
node['content'] = msgstr
435+
node['translated'] = True
432436
continue
433437

434438
if isinstance(node, nodes.image) and node.get('alt') == msg:
@@ -490,6 +494,7 @@ def apply(self, **kwargs: Any) -> None:
490494

491495
if isinstance(node, nodes.image) and node.get('alt') != msg:
492496
node['uri'] = patch['uri']
497+
node['translated'] = False
493498
continue # do not mark translated
494499

495500
node['translated'] = True # to avoid double translation
@@ -514,6 +519,64 @@ def apply(self, **kwargs: Any) -> None:
514519
node['entries'] = new_entries
515520

516521

522+
class TranslationProgressTotaliser(SphinxTransform):
523+
"""
524+
Calculate the number of translated and untranslated nodes.
525+
"""
526+
default_priority = 25 # MUST happen after Locale
527+
528+
def apply(self, **kwargs: Any) -> None:
529+
from sphinx.builders.gettext import MessageCatalogBuilder
530+
if isinstance(self.app.builder, MessageCatalogBuilder):
531+
return
532+
533+
total = translated = 0
534+
for node in self.document.findall(NodeMatcher(translated=Any)): # type: nodes.Element
535+
total += 1
536+
if node['translated']:
537+
translated += 1
538+
539+
self.document['translation_progress'] = {
540+
'total': total,
541+
'translated': translated,
542+
}
543+
544+
545+
class AddTranslationClasses(SphinxTransform):
546+
"""
547+
Add ``translated`` or ``untranslated`` classes to indicate translation status.
548+
"""
549+
default_priority = 950
550+
551+
def apply(self, **kwargs: Any) -> None:
552+
from sphinx.builders.gettext import MessageCatalogBuilder
553+
if isinstance(self.app.builder, MessageCatalogBuilder):
554+
return
555+
556+
if not self.config.translation_progress_classes:
557+
return
558+
559+
if self.config.translation_progress_classes is True:
560+
add_translated = add_untranslated = True
561+
elif self.config.translation_progress_classes == 'translated':
562+
add_translated = True
563+
add_untranslated = False
564+
elif self.config.translation_progress_classes == 'untranslated':
565+
add_translated = False
566+
add_untranslated = True
567+
else:
568+
raise ConfigError('translation_progress_classes must be'
569+
' True, False, "translated" or "untranslated"')
570+
571+
for node in self.document.findall(NodeMatcher(translated=Any)): # type: nodes.Element
572+
if node['translated']:
573+
if add_translated:
574+
node.setdefault('classes', []).append('translated')
575+
else:
576+
if add_untranslated:
577+
node.setdefault('classes', []).append('untranslated')
578+
579+
517580
class RemoveTranslatableInline(SphinxTransform):
518581
"""
519582
Remove inline nodes used for translation as placeholders.
@@ -534,6 +597,8 @@ def apply(self, **kwargs: Any) -> None:
534597
def setup(app: Sphinx) -> dict[str, Any]:
535598
app.add_transform(PreserveTranslatableMessages)
536599
app.add_transform(Locale)
600+
app.add_transform(TranslationProgressTotaliser)
601+
app.add_transform(AddTranslationClasses)
537602
app.add_transform(RemoveTranslatableInline)
538603

539604
return {

tests/roots/test-intl/index.txt

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ CONTENTS
2929
raw
3030
refs
3131
section
32+
translation_progress
3233
topic
3334

3435
.. toctree::
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
Translation Progress
2+
====================
3+
4+
When, in disgrace with fortune and men’s eyes,
5+
6+
I all alone beweep my outcast state,
7+
8+
And trouble deaf heaven with my bootless cries,
9+
10+
And look upon myself, and curse my fate,
11+
12+
Wishing me like to one more rich in hope,
13+
14+
Featur’d like him, like him with friends possess’d,
15+
16+
Desiring this man’s art and that man’s scope,
17+
18+
With what I most enjoy contented least;
19+
20+
Yet in these thoughts myself almost despising,
21+
22+
Haply I think on thee, and then my state,
23+
24+
Like to the lark at break of day arising
25+
26+
.. untranslated (3 out of 14 lines):
27+
28+
From sullen earth, sings hymns at heaven’s gate;
29+
30+
For thy sweet love remember’d such wealth brings
31+
32+
That then I scorn to change my state with kings.
33+
34+
.. translation progress substitution
35+
36+
|translation progress|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
msgid ""
2+
msgstr ""
3+
"Project-Id-Version: \n"
4+
"Report-Msgid-Bugs-To: \n"
5+
"POT-Creation-Date: 2000-01-01 00:00\n"
6+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
7+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
8+
"Language-Team: \n"
9+
"Language: xx\n"
10+
"MIME-Version: 1.0\n"
11+
"Content-Type: text/plain; charset=UTF-8\n"
12+
"Content-Transfer-Encoding: 8bit\n"
13+
14+
msgid "Translation Progress"
15+
msgstr "TRANSLATION PROGRESS"
16+
17+
msgid "When, in disgrace with fortune and men’s eyes,"
18+
msgstr "WHEN, IN DISGRACE WITH FORTUNE AND MEN’S EYES,"
19+
20+
msgid "I all alone beweep my outcast state,"
21+
msgstr "I ALL ALONE BEWEEP MY OUTCAST STATE,"
22+
23+
msgid "And trouble deaf heaven with my bootless cries,"
24+
msgstr "AND TROUBLE DEAF HEAVEN WITH MY BOOTLESS CRIES,"
25+
26+
msgid "And look upon myself, and curse my fate,"
27+
msgstr "AND LOOK UPON MYSELF, AND CURSE MY FATE,"
28+
29+
msgid "Wishing me like to one more rich in hope,"
30+
msgstr "WISHING ME LIKE TO ONE MORE RICH IN HOPE,"
31+
32+
msgid "Featur’d like him, like him with friends possess’d,"
33+
msgstr "FEATUR’D LIKE HIM, LIKE HIM WITH FRIENDS POSSESS’D,"
34+
35+
msgid "Desiring this man’s art and that man’s scope,"
36+
msgstr "DESIRING THIS MAN’S ART AND THAT MAN’S SCOPE,"
37+
38+
msgid "With what I most enjoy contented least;"
39+
msgstr "WITH WHAT I MOST ENJOY CONTENTED LEAST;"
40+
41+
msgid "Yet in these thoughts myself almost despising,"
42+
msgstr "YET IN THESE THOUGHTS MYSELF ALMOST DESPISING,"
43+
44+
msgid "Haply I think on thee, and then my state,"
45+
msgstr "HAPLY I THINK ON THEE, AND THEN MY STATE,"
46+
47+
msgid "Like to the lark at break of day arising"
48+
msgstr "LIKE TO THE LARK AT BREAK OF DAY ARISING"

0 commit comments

Comments
 (0)