From 36b5e2214082b2c458b06399fc7af8a97be61374 Mon Sep 17 00:00:00 2001 From: Manuel YGUEL Date: Wed, 7 Jan 2026 19:14:48 +0100 Subject: [PATCH] Add mechanics to enable automatic subtitle detection for translation --- sphinx_exercise/__init__.py | 18 ++++++++++++++++-- sphinx_exercise/directive.py | 11 ++++++++++- sphinx_exercise/nodes.py | 21 +++++++++++++++++++++ sphinx_exercise/transforms.py | 16 ++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/sphinx_exercise/__init__.py b/sphinx_exercise/__init__.py index 1e74f78..72345ad 100644 --- a/sphinx_exercise/__init__.py +++ b/sphinx_exercise/__init__.py @@ -50,11 +50,16 @@ exercise_latex_number_reference, visit_exercise_latex_number_reference, depart_exercise_latex_number_reference, + visit_exercise_subtitle, + depart_exercise_subtitle, + visit_solution_subtitle, + depart_solution_subtitle, ) from .transforms import ( CheckGatedDirectives, MergeGatedSolutions, MergeGatedExercises, + setup_i18n_for_subtitles, ) from .post_transforms import ( ResolveTitlesInExercises, @@ -279,6 +284,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.connect("config-inited", init_numfig) # event order - 1 app.connect("env-purge-doc", purge_exercises) # event order - 5 per file app.connect("doctree-read", doctree_read) # event order - 8 + app.connect("doctree-read", setup_i18n_for_subtitles) # mark subtitles translatable app.connect("env-merge-info", merge_exercises) # event order - 9 app.connect("env-updated", validate_exercise_solution_order) # event order - 10 app.connect("build-finished", copy_asset_files) # event order - 16 @@ -312,9 +318,17 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_node(solution_start_node) app.add_node(solution_end_node) app.add_node(exercise_title) - app.add_node(exercise_subtitle) + app.add_node(exercise_subtitle, + singlehtml=(visit_exercise_subtitle, depart_exercise_subtitle), + html=(visit_exercise_subtitle, depart_exercise_subtitle), + latex=(visit_exercise_subtitle, depart_exercise_subtitle), + ) app.add_node(solution_title) - app.add_node(solution_subtitle) + app.add_node(solution_subtitle, + singlehtml=(visit_solution_subtitle, depart_solution_subtitle), + html=(visit_solution_subtitle, depart_solution_subtitle), + latex=(visit_solution_subtitle, depart_solution_subtitle), + ) app.add_node( exercise_latex_number_reference, diff --git a/sphinx_exercise/directive.py b/sphinx_exercise/directive.py index 6dd8fcf..a40bd84 100644 --- a/sphinx_exercise/directive.py +++ b/sphinx_exercise/directive.py @@ -121,7 +121,14 @@ def run(self) -> List[Node]: subtitle_nodes, _ = self.state.inline_text(subtitle_text, self.lineno) for subtitle_node in subtitle_nodes: subtitle += subtitle_node + # Set attributes needed for i18n extraction + subtitle.rawsource = subtitle_text + subtitle.source = self.state.document.current_source + subtitle.line = self.lineno + # Also add as child of title for post-transforms to find title += subtitle + else: + subtitle = None # State Parsing section = nodes.section(ids=["exercise-content"]) @@ -149,7 +156,9 @@ def run(self) -> List[Node]: self.options["name"] = label # Construct Node - node += title + node += title # Add title as child of node to be found for translation + if subtitle is not None: + node += subtitle node += section node["classes"].extend(classes) node["ids"].append(label) diff --git a/sphinx_exercise/nodes.py b/sphinx_exercise/nodes.py index 711500a..b67e69b 100644 --- a/sphinx_exercise/nodes.py +++ b/sphinx_exercise/nodes.py @@ -191,3 +191,24 @@ def visit_exercise_latex_number_reference(self, node): def depart_exercise_latex_number_reference(self, node): pass + +# Visitor functions for subtitle nodes +# These skip rendering - subtitle is only used in title for i18n +def visit_exercise_subtitle(self, node): + """Visit exercise_subtitle - skip rendering (only in title)""" + raise docutil_nodes.SkipNode + + +def depart_exercise_subtitle(self, node): + """Depart exercise_subtitle""" + pass + + +def visit_solution_subtitle(self, node): + """Visit solution_subtitle - skip rendering""" + raise docutil_nodes.SkipNode + + +def depart_solution_subtitle(self, node): + """Depart solution_subtitle""" + pass diff --git a/sphinx_exercise/transforms.py b/sphinx_exercise/transforms.py index fdf2f48..f7622d1 100644 --- a/sphinx_exercise/transforms.py +++ b/sphinx_exercise/transforms.py @@ -182,3 +182,19 @@ def apply(self): if node.gated: self.merge_nodes(node) node.gated = False + + +def setup_i18n_for_subtitles(app, document): + """ + Event handler for 'doctree-read' event. + Marks exercise and solution subtitle nodes as translatable. + """ + from .nodes import exercise_subtitle, solution_subtitle + + # Mark exercise subtitles as translatable + for node in findall(document, exercise_subtitle): + node['translatable'] = True + + # Mark solution subtitles as translatable + for node in findall(document, solution_subtitle): + node['translatable'] = True