1414from sphinx import addnodes
1515from sphinx .config import Config
1616from sphinx .domains .std import make_glossary_term , split_term_classifiers
17+ from sphinx .errors import ConfigError
1718from sphinx .locale import __
1819from sphinx .locale import init as init_locale
1920from 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+
517580class RemoveTranslatableInline (SphinxTransform ):
518581 """
519582 Remove inline nodes used for translation as placeholders.
@@ -534,6 +597,8 @@ def apply(self, **kwargs: Any) -> None:
534597def 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 {
0 commit comments