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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Release 9.0.4 (in development)
Bugs fixed
----------

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

Release 9.0.3 (released Dec 04, 2025)
=====================================
Expand Down
34 changes: 29 additions & 5 deletions sphinx/transforms/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from operator import attrgetter
from re import DOTALL, match
from textwrap import indent
from typing import TYPE_CHECKING, Any, TypeVar
Expand All @@ -28,7 +29,7 @@
)

if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Callable, Sequence

from docutils.frontend import Values

Expand Down Expand Up @@ -133,11 +134,30 @@ def compare_references(
old_refs: Sequence[nodes.Element],
new_refs: Sequence[nodes.Element],
warning_msg: str,
*,
key_func: Callable[[nodes.Element], Any] = attrgetter('rawsource'),
ignore_order: bool = False,
) -> None:
"""Warn about mismatches between references in original and translated content."""
old_ref_rawsources = [ref.rawsource for ref in old_refs]
new_ref_rawsources = [ref.rawsource for ref in new_refs]
if not self.noqa and old_ref_rawsources != new_ref_rawsources:
"""Warn about mismatches between references in original and translated content.

:param key_func: A function to extract the comparison key from each reference.
Defaults to extracting the ``rawsource`` attribute.
:param ignore_order: If True, ignore the order of references when comparing.
This allows translators to reorder references while still catching
missing or extra references.
"""
old_ref_keys = list(map(key_func, old_refs))
new_ref_keys = list(map(key_func, new_refs))

if ignore_order:
# The ref_keys lists may contain ``None``, so compare hashes.
# Recall objects which compare equal have the same hash value.
old_ref_keys.sort(key=hash)
new_ref_keys.sort(key=hash)

if not self.noqa and old_ref_keys != new_ref_keys:
old_ref_rawsources = [ref.rawsource for ref in old_refs]
new_ref_rawsources = [ref.rawsource for ref in new_refs]
logger.warning(
warning_msg.format(old_ref_rawsources, new_ref_rawsources),
location=self.node,
Expand Down Expand Up @@ -347,6 +367,10 @@ def update_pending_xrefs(self) -> None:
'inconsistent term references in translated message.'
' original: {0}, translated: {1}'
),
# Compare by reftarget only, allowing translated display text.
# Ignore order since translators may legitimately reorder references.
key_func=lambda ref: ref.get('reftarget'),
ignore_order=True,
Comment on lines +370 to +373
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any of the other compare_references() calls have ignore_order=True?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I'm not pretty sure.
I only applied it to update_pending_xrefs() since this would slash all those building warnings regarding sequence issue from the pydoc-zh-tw project, but I believe making it the default behavior (and remove ignore_order) should be safe. Either making it default behavior or stay as it is and apply ignore_order=True on demand both fine for me. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwang44 are translators able to re-order any of the other ones?

Copy link
Contributor Author

@mattwang44 mattwang44 Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe so, but chances are rare.

)

xref_reftarget_map: dict[tuple[str, str, str] | None, dict[str, Any]] = {}
Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-intl/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ CONTENTS
topic
markup
backslashes
refs_reordered

.. toctree::
:maxdepth: 2
Expand Down
16 changes: 16 additions & 0 deletions tests/roots/test-intl/refs_reordered.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
:tocdepth: 2

i18n with reordered and translated references
==============================================

.. glossary::

term one
First glossary term

term two
Second glossary term

1. Multiple refs reordered: :term:`term one` and :term:`term two`.

2. Single ref with translated display text: :term:`term one`.
31 changes: 31 additions & 0 deletions tests/roots/test-intl/xx/LC_MESSAGES/refs_reordered.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Test for reordered and translated references.
# These should NOT trigger inconsistency warnings.
#
msgid ""
msgstr ""
"Project-Id-Version: sphinx 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-01 00:00+0000\n"
"PO-Revision-Date: 2024-01-01 00:00+0000\n"
"Last-Translator: Test\n"
"Language-Team: xx\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "i18n with reordered and translated references"
msgstr "I18N WITH REORDERED AND TRANSLATED REFERENCES"

msgid "First glossary term"
msgstr "FIRST GLOSSARY TERM"

msgid "Second glossary term"
msgstr "SECOND GLOSSARY TERM"

# Reordered references - should NOT warn because targets are the same
msgid "Multiple refs reordered: :term:`term one` and :term:`term two`."
msgstr "MULTIPLE REFS REORDERED: :term:`term two` AND :term:`term one`."

# Translated display text - should NOT warn because reftarget is the same
msgid "Single ref with translated display text: :term:`term one`."
msgstr "SINGLE REF WITH TRANSLATED DISPLAY TEXT: :term:`TRANSLATED TERM ONE <term one>`."
19 changes: 19 additions & 0 deletions tests/test_intl/test_intl.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,25 @@ def test_text_glossary_term_inconsistencies(app):
)


@sphinx_intl
@pytest.mark.sphinx('text', testroot='intl')
@pytest.mark.test_params(shared_result='test_intl_basic')
def test_text_refs_reordered_no_warning(app):
app.build()
# --- refs_reordered: verify no inconsistency warnings
result = (app.outdir / 'refs_reordered.txt').read_text(encoding='utf8')
# Verify the translation was applied
assert 'MULTIPLE REFS REORDERED' in result
assert 'SINGLE REF WITH TRANSLATED DISPLAY TEXT' in result

warnings = getwarning(app.warning)
# Should NOT have any inconsistent_references warnings for refs_reordered.txt
unexpected_warning_expr = '.*/refs_reordered.txt.*inconsistent.*references'
assert not re.search(unexpected_warning_expr, warnings), (
f'Unexpected warning found: {warnings!r}'
)


@sphinx_intl
@pytest.mark.sphinx('gettext', testroot='intl')
@pytest.mark.test_params(shared_result='test_intl_gettext')
Expand Down
Loading