Skip to content

🐛 Keep buttons intact under gettext translation#270

Merged
chrisjsewell merged 3 commits into
mainfrom
brief/03-button-i18n
Jul 12, 2026
Merged

🐛 Keep buttons intact under gettext translation#270
chrisjsewell merged 3 commits into
mainfrom
brief/03-button-i18n

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Jul 12, 2026

Copy link
Copy Markdown
Member

Carries #264 by @sneakers-the-rat (commit preserved) with hardening and test coverage on top. Closes #96, closes #44, closes #263.

From #264 (the core fix): gettext now targets only the button text via a translatable inline (translatable=True) instead of the whole directive paragraph (translatable=False), so translation no longer replaces the styled button with plain text.

Added on top:

  • Crash fix: Sphinx's RemoveTranslatableInline transform unwraps the translatable placeholder, leaving a bare Text as the reference/xref child — an unresolved button-ref in a translated project then crashed the build (Losing "classes" attribute assertion during reference resolution). The placeholder is now nested inside a plain inline wrapper, so an element child always remains. This also restores the pre-existing doctree shape, keeping HTML output identical to released behaviour.
  • Tests: .pot extraction regression; a translated-build round-trip (compiled .mo, language="de") asserting both button types keep classes and links and resolve refs with translated text, plus a badge control; rst snippet aligned with the myst one (they share regression files).
  • .gitignore exception for the new pot regression dir; changelog entry.

Known gap, deliberately out of scope: button-ref text is absent from .pot extraction because the gettext builder extracts from the resolved doctree, and std-domain xref resolution flattens explicit-title content (rebuilds it as a plain std std-ref inline), discarding the translatable marker. Translation itself works for both button types since the Locale transform runs before resolution. Documented in the test docstring; follow-up tracked in #265.

Validation: tox -e py311 (113 passed), py311-no-myst, mypy, ruff — all green.

@chrisjsewell chrisjsewell force-pushed the brief/03-button-i18n branch 2 times, most recently from 0a1ccad to f8792ba Compare July 12, 2026 13:53
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.39%. Comparing base (ce1e0a9) to head (d3bbb47).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #270      +/-   ##
==========================================
+ Coverage   88.08%   88.39%   +0.31%     
==========================================
  Files          11       11              
  Lines        1041     1043       +2     
==========================================
+ Hits          917      922       +5     
+ Misses        124      121       -3     
Flag Coverage Δ
pytests 88.39% <100.00%> (+0.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

sneakers-the-rat and others added 3 commits July 12, 2026 14:00
Builds on the approach from #264: gettext targets only the button text
via a translatable inline, so translated button-link/button-ref keep
their classes and links (closes #96, #44, #263).

Additional hardening and coverage on top of #264:
- wrap the translatable placeholder inline in a plain inline, so the
  reference always retains an element child after sphinx unwraps the
  placeholder (an unresolved button-ref otherwise crashed the build
  during reference resolution in translated projects)
- this also restores the pre-existing doctree shape (inline wrapper),
  keeping HTML output identical to previous releases
- add gettext extraction regression (pot) and a translated-build
  round-trip test proving classes/links/refs survive with translated
  text, for both button types, plus a badge control
- align the rst button-link snippet with the myst one (both feed the
  same regression files)
- note: button-ref text is still missing from pot extraction because
  std-domain xref resolution flattens explicit-title content before the
  gettext builder extracts; translation itself works (documented in the
  test, follow-up tracked on the roadmap)
- add 'translatable'/'translated' to the boolean-attribute
  normalization list, so regressions are stable across docutils
  0.22 (1/0) and earlier (True/False) serializations
- define an 'issue' extlink for changelog references
@chrisjsewell chrisjsewell force-pushed the brief/03-button-i18n branch from f8792ba to d3bbb47 Compare July 12, 2026 14:00
@chrisjsewell chrisjsewell merged commit 834eebe into main Jul 12, 2026
25 checks passed
@chrisjsewell chrisjsewell deleted the brief/03-button-i18n branch July 12, 2026 14:05
chrisjsewell added a commit that referenced this pull request Jul 14, 2026
Two `button-ref` bugs, plus a parity improvement:

## Changes

- **#110 — targets no longer whitespace-stripped**: both button
directives ran their argument through `directives.uri`, which deletes
all whitespace — correct for `button-link` URLs, but it turned
multi-word ref targets (exactly what `autosectionlabel` generates, e.g.
`my article section`) into `myarticlesection` → `undefined label`. Each
directive now normalizes its own argument: `button-link` keeps
`directives.uri`; `button-ref` collapses whitespace runs like Sphinx's
`XRefRole` does. Additionally, `ref-type: ref` targets are lowercased to
match the `:ref:` role (`XRefRole(lowercase=True)` — std labels are
stored lowercased), so the visible Title-Case heading text can be pasted
verbatim; `doc`/`myst` targets stay case-sensitive.
- **#228 — rich content preserved through resolution**: the std-domain
resolvers rebuild explicit-title refs from `astext()`, flattening nested
inline markup (emphasis, icons rendered as escaped text). Fixed with a
stash/graft pair of post-transforms using only stable APIs: content is
stashed and the `pending_xref` tagged with a transient marker class
before any resolver runs (priority 8 — below myst-parser's resolver at 9
and the built-in at 10), then grafted back onto the resolved reference
(priority 11). Buttons whose content itself contains a cross-reference
role are deliberately left on the old flatten path — regrafting would
re-insert an unresolved `pending_xref` and crash the writer (and a link
inside a button link is invalid HTML anyway).
- Content-less `button-ref`s keep the resolver-generated section title
(guarded by `refexplicit`).
- Unresolved `button-ref`s degrade to a standard `undefined label`
warning with unlinked content — no traceback (locking in the hardening
from #270). Docs updated: the "bold text will be rendered without bold"
caveat is gone (it now renders), and targets may contain spaces.

## Verification

- New `tests/test_buttons.py` (11 tests, rst + myst): multi-word target
resolution (verified red on the old code: `undefined label:
'myarticlesection'`), Title-Case + `ref-type: ref`, nested-content
parity with `button-link`, nested-xref-in-content build safety (verified
red — reproduced the writer crash pre-fix), content-less title
rendering, unresolved warning path, and marker-leak assertions (HTML +
doctree) on every path.
- Independent adversarial review: marker-leak sweep across
HTML/latex/text/xml/gettext outputs *and* pickled doctrees/env (clean,
including incremental rebuilds), resolver-path matrix (std ref/doc, any,
myst resolving + failing, missing-ref), `sd-btn` class output
byte-identical to main outside the intended fixes, nitpicky-mode parity.
- Full suite green on pinned Sphinx 7.2.6 / 7.4.7 / 8.2.3 (145 passed
each, versions verified in-venv) and the standard matrix on 9.0.4. An
earlier draft used a private resolver API that doesn't exist before
Sphinx 8.2 — the final design deliberately uses only stable public
mechanics.

Closes #110
Closes #228
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants