Skip to content

Commit 88a4a97

Browse files
Allow reordered refs and translated display text in pending_xref (#14144)
Co-authored-by: Adam Turner <[email protected]>
1 parent f666208 commit 88a4a97

File tree

6 files changed

+99
-5
lines changed

6 files changed

+99
-5
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Release 9.0.4 (in development)
44
Bugs fixed
55
----------
66

7+
* #14143: Fix spurious build warnings when translators reorder references
8+
in strings, or use translated display text in references.
9+
Patch by Matt Wang.
710

811
Release 9.0.3 (released Dec 04, 2025)
912
=====================================

sphinx/transforms/i18n.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from operator import attrgetter
56
from re import DOTALL, match
67
from textwrap import indent
78
from typing import TYPE_CHECKING, Any, TypeVar
@@ -28,7 +29,7 @@
2829
)
2930

3031
if TYPE_CHECKING:
31-
from collections.abc import Sequence
32+
from collections.abc import Callable, Sequence
3233

3334
from docutils.frontend import Values
3435

@@ -133,11 +134,30 @@ def compare_references(
133134
old_refs: Sequence[nodes.Element],
134135
new_refs: Sequence[nodes.Element],
135136
warning_msg: str,
137+
*,
138+
key_func: Callable[[nodes.Element], Any] = attrgetter('rawsource'),
139+
ignore_order: bool = False,
136140
) -> None:
137-
"""Warn about mismatches between references in original and translated content."""
138-
old_ref_rawsources = [ref.rawsource for ref in old_refs]
139-
new_ref_rawsources = [ref.rawsource for ref in new_refs]
140-
if not self.noqa and old_ref_rawsources != new_ref_rawsources:
141+
"""Warn about mismatches between references in original and translated content.
142+
143+
:param key_func: A function to extract the comparison key from each reference.
144+
Defaults to extracting the ``rawsource`` attribute.
145+
:param ignore_order: If True, ignore the order of references when comparing.
146+
This allows translators to reorder references while still catching
147+
missing or extra references.
148+
"""
149+
old_ref_keys = list(map(key_func, old_refs))
150+
new_ref_keys = list(map(key_func, new_refs))
151+
152+
if ignore_order:
153+
# The ref_keys lists may contain ``None``, so compare hashes.
154+
# Recall objects which compare equal have the same hash value.
155+
old_ref_keys.sort(key=hash)
156+
new_ref_keys.sort(key=hash)
157+
158+
if not self.noqa and old_ref_keys != new_ref_keys:
159+
old_ref_rawsources = [ref.rawsource for ref in old_refs]
160+
new_ref_rawsources = [ref.rawsource for ref in new_refs]
141161
logger.warning(
142162
warning_msg.format(old_ref_rawsources, new_ref_rawsources),
143163
location=self.node,
@@ -347,6 +367,10 @@ def update_pending_xrefs(self) -> None:
347367
'inconsistent term references in translated message.'
348368
' original: {0}, translated: {1}'
349369
),
370+
# Compare by reftarget only, allowing translated display text.
371+
# Ignore order since translators may legitimately reorder references.
372+
key_func=lambda ref: ref.get('reftarget'),
373+
ignore_order=True,
350374
)
351375

352376
xref_reftarget_map: dict[tuple[str, str, str] | None, dict[str, Any]] = {}

tests/roots/test-intl/index.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CONTENTS
3333
topic
3434
markup
3535
backslashes
36+
refs_reordered
3637

3738
.. toctree::
3839
:maxdepth: 2
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
:tocdepth: 2
2+
3+
i18n with reordered and translated references
4+
==============================================
5+
6+
.. glossary::
7+
8+
term one
9+
First glossary term
10+
11+
term two
12+
Second glossary term
13+
14+
1. Multiple refs reordered: :term:`term one` and :term:`term two`.
15+
16+
2. Single ref with translated display text: :term:`term one`.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Test for reordered and translated references.
2+
# These should NOT trigger inconsistency warnings.
3+
#
4+
msgid ""
5+
msgstr ""
6+
"Project-Id-Version: sphinx 1.0\n"
7+
"Report-Msgid-Bugs-To: \n"
8+
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
9+
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
10+
"Last-Translator: Test\n"
11+
"Language-Team: xx\n"
12+
"MIME-Version: 1.0\n"
13+
"Content-Type: text/plain; charset=UTF-8\n"
14+
"Content-Transfer-Encoding: 8bit\n"
15+
16+
msgid "i18n with reordered and translated references"
17+
msgstr "I18N WITH REORDERED AND TRANSLATED REFERENCES"
18+
19+
msgid "First glossary term"
20+
msgstr "FIRST GLOSSARY TERM"
21+
22+
msgid "Second glossary term"
23+
msgstr "SECOND GLOSSARY TERM"
24+
25+
# Reordered references - should NOT warn because targets are the same
26+
msgid "Multiple refs reordered: :term:`term one` and :term:`term two`."
27+
msgstr "MULTIPLE REFS REORDERED: :term:`term two` AND :term:`term one`."
28+
29+
# Translated display text - should NOT warn because reftarget is the same
30+
msgid "Single ref with translated display text: :term:`term one`."
31+
msgstr "SINGLE REF WITH TRANSLATED DISPLAY TEXT: :term:`TRANSLATED TERM ONE <term one>`."

tests/test_intl/test_intl.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,25 @@ def test_text_glossary_term_inconsistencies(app):
378378
)
379379

380380

381+
@sphinx_intl
382+
@pytest.mark.sphinx('text', testroot='intl')
383+
@pytest.mark.test_params(shared_result='test_intl_basic')
384+
def test_text_refs_reordered_no_warning(app):
385+
app.build()
386+
# --- refs_reordered: verify no inconsistency warnings
387+
result = (app.outdir / 'refs_reordered.txt').read_text(encoding='utf8')
388+
# Verify the translation was applied
389+
assert 'MULTIPLE REFS REORDERED' in result
390+
assert 'SINGLE REF WITH TRANSLATED DISPLAY TEXT' in result
391+
392+
warnings = getwarning(app.warning)
393+
# Should NOT have any inconsistent_references warnings for refs_reordered.txt
394+
unexpected_warning_expr = '.*/refs_reordered.txt.*inconsistent.*references'
395+
assert not re.search(unexpected_warning_expr, warnings), (
396+
f'Unexpected warning found: {warnings!r}'
397+
)
398+
399+
381400
@sphinx_intl
382401
@pytest.mark.sphinx('gettext', testroot='intl')
383402
@pytest.mark.test_params(shared_result='test_intl_gettext')

0 commit comments

Comments
 (0)