14
14
from sphinx import addnodes
15
15
from sphinx .config import Config
16
16
from sphinx .domains .std import make_glossary_term , split_term_classifiers
17
+ from sphinx .errors import ConfigError
17
18
from sphinx .locale import __
18
19
from sphinx .locale import init as init_locale
19
20
from sphinx .transforms import SphinxTransform
@@ -360,9 +361,9 @@ def apply(self, **kwargs: Any) -> None:
360
361
if not isinstance (node , LITERAL_TYPE_NODES ):
361
362
msgstr , _ = parse_noqa (msgstr )
362
363
363
- # XXX add marker to untranslated parts
364
364
if not msgstr or msgstr == msg or not msgstr .strip ():
365
365
# as-of-yet untranslated
366
+ node ['translated' ] = False
366
367
continue
367
368
368
369
# Avoid "Literal block expected; none found." warnings.
@@ -404,10 +405,12 @@ def apply(self, **kwargs: Any) -> None:
404
405
if processed :
405
406
updater .update_leaves ()
406
407
node ['translated' ] = True # to avoid double translation
408
+ else :
409
+ node ['translated' ] = False
407
410
408
411
# phase2: translation
409
412
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
411
414
continue # skip if the node is already translated by phase1
412
415
413
416
msgstr = catalog .gettext (msg )
@@ -417,8 +420,8 @@ def apply(self, **kwargs: Any) -> None:
417
420
if not isinstance (node , LITERAL_TYPE_NODES ):
418
421
msgstr , noqa = parse_noqa (msgstr )
419
422
420
- # XXX add marker to untranslated parts
421
423
if not msgstr or msgstr == msg : # as-of-yet untranslated
424
+ node ['translated' ] = False
422
425
continue
423
426
424
427
# update translatable nodes
@@ -429,6 +432,7 @@ def apply(self, **kwargs: Any) -> None:
429
432
# update meta nodes
430
433
if isinstance (node , nodes .meta ): # type: ignore[attr-defined]
431
434
node ['content' ] = msgstr
435
+ node ['translated' ] = True
432
436
continue
433
437
434
438
if isinstance (node , nodes .image ) and node .get ('alt' ) == msg :
@@ -490,6 +494,7 @@ def apply(self, **kwargs: Any) -> None:
490
494
491
495
if isinstance (node , nodes .image ) and node .get ('alt' ) != msg :
492
496
node ['uri' ] = patch ['uri' ]
497
+ node ['translated' ] = False
493
498
continue # do not mark translated
494
499
495
500
node ['translated' ] = True # to avoid double translation
@@ -514,6 +519,64 @@ def apply(self, **kwargs: Any) -> None:
514
519
node ['entries' ] = new_entries
515
520
516
521
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
+
517
580
class RemoveTranslatableInline (SphinxTransform ):
518
581
"""
519
582
Remove inline nodes used for translation as placeholders.
@@ -534,6 +597,8 @@ def apply(self, **kwargs: Any) -> None:
534
597
def setup (app : Sphinx ) -> dict [str , Any ]:
535
598
app .add_transform (PreserveTranslatableMessages )
536
599
app .add_transform (Locale )
600
+ app .add_transform (TranslationProgressTotaliser )
601
+ app .add_transform (AddTranslationClasses )
537
602
app .add_transform (RemoveTranslatableInline )
538
603
539
604
return {
0 commit comments