Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ cover/
# Translations
*.mo
*.pot
!tests/test_snippets/*.pot
!tests/test_misc/*.pot

# Django stuff:
*.log
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change Log

## Unreleased

- 🐛 FIX: buttons are no longer destroyed by gettext translation:
translated `button-link`/`button-ref` keep their styling and links
(gettext now targets only the button text), thanks to {user}`sneakers-the-rat`
in {pr}`264` ({issue}`96`, {issue}`44`, {issue}`263`)

## 0.7.0 - 2025-01-19

### Dependencies
Expand Down
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@

extlinks = {
"pr": ("https://github.com/executablebooks/sphinx-design/pull/%s", "PR #%s"),
"issue": (
"https://github.com/executablebooks/sphinx-design/issues/%s",
"#%s",
),
"user": ("https://github.com/%s", "@%s"),
}

Expand Down
6 changes: 6 additions & 0 deletions docs/snippets/myst/button-link.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
Button text
```

```{button-link} https://example.com
:color: primary

Button text with options
```

```{button-link} https://example.com
:color: primary
:shadow:
Expand Down
5 changes: 5 additions & 0 deletions docs/snippets/rst/button-link.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

Button text

.. button-link:: https://example.com
:color: primary

Button text with options

.. button-link:: https://example.com
:color: primary
:shadow:
Expand Down
16 changes: 13 additions & 3 deletions sphinx_design/badges_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,16 @@ def run_with_defaults(self) -> list[nodes.Node]:
textnodes, _ = self.state.inline_text(
"\n".join(self.content), self.lineno + self.content_offset
)
content = nodes.inline("", "")
content.extend(textnodes)
# make link text translatable -
# target gettext to the content lines, not the outer directive
translatable = nodes.inline("", "", *textnodes, translatable=True)
self.set_source_info(translatable)
translatable.line += self.content_offset
# the translatable inline is a placeholder that sphinx unwraps
# after translation (RemoveTranslatableInline); keep a plain outer
# inline so the reference always retains an element child
# (an unresolved xref otherwise crashes on replacement)
content = nodes.inline("", "", translatable)
else:
content = nodes.inline(target, target)
node.append(content)
Expand All @@ -191,7 +199,9 @@ def run_with_defaults(self) -> list[nodes.Node]:
node = grid_container

# `visit_reference` requires that a reference be inside a `TextElement` parent
container = nodes.paragraph(classes=self.options.get("align", []))
container = nodes.paragraph(
classes=self.options.get("align", []), translatable=False
)
self.set_source_info(container)
container += node

Expand Down
10 changes: 7 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,12 @@ def get_doctree(
return doctree


@pytest.fixture()
def sphinx_builder(tmp_path: Path, make_app, monkeypatch):
@pytest.fixture(params=[pytest.param("html", id="html")])
def sphinx_builder(
tmp_path: Path, make_app, monkeypatch, request: pytest.FixtureRequest
):
def _create_project(
buildername: str = "html", conf_kwargs: dict[str, Any] | None = None
buildername: str = request.param, conf_kwargs: dict[str, Any] | None = None
):
src_path = tmp_path / "srcdir"
src_path.mkdir()
Expand Down Expand Up @@ -120,6 +122,8 @@ def _normalize(text: str) -> str:
"refexplicit",
"refwarn",
"selected",
"translatable",
"translated",
]
text = re.sub(rf' ({"|".join(attrs)})="1"', r' \1="True"', text)
text = re.sub(rf' ({"|".join(attrs)})="0"', r' \1="False"', text)
Expand Down
100 changes: 100 additions & 0 deletions tests/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from babel.messages.catalog import Catalog
from babel.messages.mofile import write_mo
from docutils import nodes
import pytest

Expand Down Expand Up @@ -73,6 +75,7 @@ def test_tab_set_with_invalid_children(
doctree.attributes.pop("translation_progress", None)
file_regression.check(
normalize_doctree_xml(doctree.pformat()),
basename="test_tab_set_with_invalid_children",
extension=".xml",
encoding="utf8",
)
Expand Down Expand Up @@ -342,3 +345,100 @@ def test_tab_set_with_paragraph_warns(sphinx_builder):
builder.build(assert_pass=False)
assert "All children of a 'tab-set' should be 'tab-item'" in builder.warnings
assert "[design.tab]" in builder.warnings


I18N_INDEX_RST = """\
Heading
=======

.. _target-section:

Target Section
==============

.. button-link:: https://example.com

Click me now

.. button-ref:: target-section
:ref-type: ref

Go to target

See the :bdg-primary:`stable` badge.
"""


def test_button_i18n_gettext(sphinx_builder, file_regression):
"""Gettext extraction should target only the button text, not the directive.

See https://github.com/executablebooks/sphinx-design/issues/96

Known gap (visible in the regression file): ``button-ref`` text is absent
from the ``.pot`` because the gettext builder extracts from the resolved
doctree, and std-domain xref resolution flattens explicit-title content,
discarding the translatable marker. Translation itself still works for
both button types (see ``test_button_i18n_translated``), since the Locale
transform runs before resolution.
"""
builder = sphinx_builder("gettext", conf_kwargs={"extensions": ["sphinx_design"]})
builder.src_path.joinpath("index.rst").write_text(I18N_INDEX_RST, encoding="utf8")
builder.build()
out = (builder.out_path / "index.pot").read_text(encoding="utf8")
# strip the metadata header (contains a varying timestamp)
out = out[out.find("#: ") :]
file_regression.check(
out, basename="test_button_i18n_gettext", extension=".pot", encoding="utf8"
)


def test_button_i18n_translated(sphinx_builder):
"""Translated buttons must keep their classes, link and resolve refs.

See https://github.com/executablebooks/sphinx-design/issues/44
and https://github.com/executablebooks/sphinx-design/issues/263
"""
builder = sphinx_builder(
conf_kwargs={
"extensions": ["sphinx_design"],
"language": "de",
"locale_dirs": ["locales"],
}
)
builder.src_path.joinpath("index.rst").write_text(I18N_INDEX_RST, encoding="utf8")
catalog = Catalog(locale="de", domain="index")
catalog.add("Click me now", "Klick mich jetzt")
catalog.add("Go to target", "Zum Ziel")
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()
doctree = builder.get_doctree("index", post_transforms=True)
references = list(doctree.findall(nodes.reference))

external = [
ref
for ref in references
if "sd-btn" in ref["classes"]
and ref.get("refuri", "").startswith("https://example.com")
]
assert len(external) == 1, [r.pformat() for r in references]
assert external[0].astext() == "Klick mich jetzt"

internal = [
ref
for ref in references
if "sd-btn" in ref["classes"] and not ref.get("refuri", "").startswith("http")
]
assert len(internal) == 1, [r.pformat() for r in references]
assert internal[0].astext() == "Zum Ziel"

badges = [
node
for node in doctree.findall(nodes.inline)
if "sd-badge" in node.get("classes", [])
]
assert len(badges) == 1
assert badges[0].astext() == "stable"
15 changes: 15 additions & 0 deletions tests/test_misc/test_button_i18n_gettext.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#: ../../index.rst:2
msgid "Heading"
msgstr ""

#: ../../index.rst:7
msgid "Target Section"
msgstr ""

#: ../../index.rst:19
msgid "Click me now"
msgstr ""

#: ../../index.rst:18
msgid "See the :bdg-primary:`stable` badge."
msgstr ""
38 changes: 38 additions & 0 deletions tests/test_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
SNIPPETS_PATH = Path(__file__).parent.parent / "docs" / "snippets"
SNIPPETS_GLOB_RST = list((SNIPPETS_PATH / "rst").glob("[!_]*"))
SNIPPETS_GLOB_MYST = list((SNIPPETS_PATH / "myst").glob("[!_]*"))
EXPECTED_PATH = Path(__file__).parent / "test_snippets"
I18N_GLOB_MYST = [
p
for p in SNIPPETS_GLOB_MYST
if (EXPECTED_PATH / f"snippet_i18n_{p.stem}.pot").exists()
]


def write_assets(src_path: Path):
Expand Down Expand Up @@ -205,3 +211,35 @@ def test_sd_custom_directives(
extension=".xml",
encoding="utf8",
)


@pytest.mark.parametrize("sphinx_builder", ["gettext"], indirect=True)
@pytest.mark.parametrize(
"path",
I18N_GLOB_MYST,
ids=[path.stem for path in I18N_GLOB_MYST],
)
@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed")
def test_i18n_myst(
sphinx_builder: Callable[..., SphinxBuilder],
path: Path,
normalize_doctree_xml,
file_regression,
):
builder = sphinx_builder()
content = path.read_text(encoding="utf8")
builder.src_path.joinpath("index.md").write_text(content, encoding="utf8")
write_assets(builder.src_path)
builder.build()

# strip metadata (that includes a constantly-varying timestamp)
out_path = builder.out_path / "index.pot"
out = out_path.read_text()
out = out[out.find("#: ../../index") :]

file_regression.check(
out,
basename=f"snippet_i18n_{path.stem}",
extension=".pot",
encoding="utf8",
)
7 changes: 7 additions & 0 deletions tests/test_snippets/snippet_i18n_button-link.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#: ../../index.md:4
msgid "Button text"
msgstr ""

#: ../../index.md:10
msgid "Button text with options"
msgstr ""
14 changes: 9 additions & 5 deletions tests/test_snippets/snippet_post_button-link.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@
<section ids="heading" names="heading">
<title>
Heading
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap" refuri="https://example.com">
<inline>
Button text
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-primary" refuri="https://example.com">
<inline>
Button text with options
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-primary sd-shadow-sm" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-outline-primary" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<inline classes="sd-d-grid">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-secondary" refuri="https://example.com">
<inline>
Expand Down
14 changes: 9 additions & 5 deletions tests/test_snippets/snippet_pre_button-link.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@
<section ids="heading" names="heading">
<title>
Heading
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap" refuri="https://example.com">
<inline>
Button text
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-primary" refuri="https://example.com">
<inline>
Button text with options
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-primary sd-shadow-sm" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-outline-primary" refuri="https://example.com">
<inline>
https://example.com
<paragraph>
<paragraph translatable="False">
<inline classes="sd-d-grid">
<reference classes="sd-sphinx-override sd-btn sd-text-wrap sd-btn-secondary" refuri="https://example.com">
<inline>
Expand Down
Loading