From 4570640e27ed5af294ee11aa67ea391907a6c1f0 Mon Sep 17 00:00:00 2001 From: Yuzhong Zhang Date: Tue, 1 Sep 2026 19:46:33 +0000 Subject: [PATCH] Fix gettext skipping MyST table cells inside tab-item MyST sets source on the table node but not on cell paragraphs. During nested_parse of tab-item content, document.current_source is None, so Sphinx is_translatable skips those cells and they never appear in .pot. Propagate the table's source/line onto descendants after nested_parse. Fixes #234 --- CHANGELOG.md | 3 ++ sphinx_design/tabs.py | 22 +++++++++++++ tests/test_misc.py | 72 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f31abd0..b72ae261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- 🐛 FIX: Table cells inside a MyST ``tab-item`` are now extracted for gettext + and translated, rather than being omitted from the ``.pot`` because they + lacked ``source`` after nested parse ({issue}`234`) - ♻️ IMPROVE: Replace the Sass/Node build with a dependency-free Python CSS generator (`tools/generate_css.py` driven by `style/design.toml` and hand-authored `style/*.css`); `package.json` is gone. The compiled diff --git a/sphinx_design/tabs.py b/sphinx_design/tabs.py index 7f543b1d..bb28e3ae 100644 --- a/sphinx_design/tabs.py +++ b/sphinx_design/tabs.py @@ -132,6 +132,7 @@ def run_with_defaults(self) -> list[nodes.Node]: classes=["sd-tab-content", *self.options.get("class-content", [])], ) self.state.nested_parse(self.content, self.content_offset, tab_content) + _propagate_table_source(tab_content) tab_item += tab_content return [tab_item] @@ -197,6 +198,27 @@ def run_with_defaults(self) -> list[nodes.Node]: return [tab_set] +def _propagate_table_source(root: nodes.Node) -> None: + """Copy source/line from a table onto descendants that lack them. + + MyST's table renderer sets source on the ``table`` node but not on cell + paragraphs. At document level those cells inherit + ``document.current_source``; during ``nested_parse`` that is ``None``, + so Sphinx's gettext extractor skips them (``is_translatable`` requires + ``node.source``). See https://github.com/executablebooks/sphinx-design/issues/234 + """ + for table in root.findall(nodes.table): + source = table.source + if not source: + continue + line = table.line + for child in table.findall(nodes.Element): + if child.source is None: + child.source = source + if child.line is None: + child.line = line + + class sd_tab_input(nodes.Element, nodes.General): # noqa: N801 pass diff --git a/tests/test_misc.py b/tests/test_misc.py index dc651e50..a0238a2b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -504,6 +504,78 @@ def test_button_i18n_translated(sphinx_builder): assert badges[0].astext() == "stable" +TAB_TABLE_MYST = """\ +# Heading + +::::{tab-set} + +:::{tab-item} Label1 +Content1 + +| UniqueHeaderA | UniqueHeaderB | +| --- | --- | +| UniqueCellC | UniqueCellD | +::: + +:::: +""" + + +@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed") +def test_tab_table_i18n_gettext(sphinx_builder): + """Table cells inside a MyST tab-item must be extracted for gettext. + + See https://github.com/executablebooks/sphinx-design/issues/234 + """ + builder = sphinx_builder("gettext") + builder.src_path.joinpath("index.md").write_text(TAB_TABLE_MYST, encoding="utf8") + builder.build() + pot = (builder.out_path / "index.pot").read_text(encoding="utf8") + for msgid in ( + "Heading", + "Label1", + "Content1", + "UniqueHeaderA", + "UniqueHeaderB", + "UniqueCellC", + "UniqueCellD", + ): + assert f'msgid "{msgid}"' in pot, pot + + +@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed") +def test_tab_table_i18n_translated(sphinx_builder): + """Translated table cells inside a tab-item must appear in the HTML output. + + See https://github.com/executablebooks/sphinx-design/issues/234 + """ + builder = sphinx_builder( + conf_kwargs={ + "extensions": ["myst_parser", "sphinx_design"], + "myst_enable_extensions": ["colon_fence"], + "language": "de", + "locale_dirs": ["locales"], + } + ) + builder.src_path.joinpath("index.md").write_text(TAB_TABLE_MYST, encoding="utf8") + catalog = Catalog(locale="de", domain="index") + catalog.add("UniqueHeaderA", "KopfA") + catalog.add("UniqueHeaderB", "KopfB") + catalog.add("UniqueCellC", "ZelleC") + catalog.add("UniqueCellD", "ZelleD") + mo_dir = builder.src_path / "locales" / "de" / "LC_MESSAGES" + mo_dir.mkdir(parents=True) + with (mo_dir / "index.mo").open("wb") as handle: + write_mo(handle, catalog) + + builder.build() + html = (builder.out_path / "index.html").read_text(encoding="utf8") + for translated in ("KopfA", "KopfB", "ZelleC", "ZelleD"): + assert translated in html, html + for original in ("UniqueHeaderA", "UniqueHeaderB", "UniqueCellC", "UniqueCellD"): + assert original not in html, html + + INVALID_CONFIG_VALUES = { "custom_directives": (["not", "a", "dict"], "must be a dictionary"), "fontawesome_source": ("invalid", "must be one of"),