diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 3be280859c2..71a21e16855 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -282,14 +282,21 @@ def update_refnamed_references(self) -> None: is_refnamed_ref = NodeMatcher(nodes.reference, refname=Any) old_refs = list(is_refnamed_ref.findall(self.node)) new_refs = list(is_refnamed_ref.findall(self.patch)) - self.compare_references( - old_refs, - new_refs, - __( - 'inconsistent references in translated message.' - ' original: {0}, translated: {1}' - ), - ) + # Only compare the count of references, not their content. + # Translators are allowed to change display text (which affects rawsource comparison), + # and the fixup mechanism below will correct the refnames if needed. + if not self.noqa and len(old_refs) != len(new_refs): + old_ref_rawsources = [ref.rawsource for ref in old_refs] + new_ref_rawsources = [ref.rawsource for ref in new_refs] + logger.warning( + __( + 'inconsistent references in translated message.' + ' original: {0}, translated: {1}' + ).format(old_ref_rawsources, new_ref_rawsources), + location=self.node, + type='i18n', + subtype='inconsistent_references', + ) old_ref_names = [r['refname'] for r in old_refs] new_ref_names = [r['refname'] for r in new_refs] orphans = [*({*old_ref_names} - {*new_ref_names})] @@ -354,17 +361,22 @@ def update_pending_xrefs(self) -> None: # This code restricts to change ref-targets in the translation. old_xrefs = [*self.node.findall(addnodes.pending_xref)] new_xrefs = [*self.patch.findall(addnodes.pending_xref)] - self.compare_references( - old_xrefs, - new_xrefs, - __( - 'inconsistent term references in translated message.' - ' original: {0}, translated: {1}' - ), - # Compare by reftarget only, allowing translated display text. - key_func=lambda ref: ref.get('reftarget'), - ) - + # Only compare the count of cross-references, not their targets. + # For term references, translators may translate both display text and + # the term name itself (when the glossary is also translated). + # For other xrefs, the fixup mechanism below handles target corrections. + if not self.noqa and len(old_xrefs) != len(new_xrefs): + old_xref_rawsources = [ref.rawsource for ref in old_xrefs] + new_xref_rawsources = [ref.rawsource for ref in new_xrefs] + logger.warning( + __( + 'inconsistent term references in translated message.' + ' original: {0}, translated: {1}' + ).format(old_xref_rawsources, new_xref_rawsources), + location=self.node, + type='i18n', + subtype='inconsistent_references', + ) xref_reftarget_map: dict[tuple[str, str, str] | None, dict[str, Any]] = {} def get_ref_key(node: addnodes.pending_xref) -> tuple[str, str, str] | None: diff --git a/tests/roots/test-intl/refs_translated_display_text.txt b/tests/roots/test-intl/refs_translated_display_text.txt new file mode 100644 index 00000000000..9d2ca279ac1 --- /dev/null +++ b/tests/roots/test-intl/refs_translated_display_text.txt @@ -0,0 +1,52 @@ +:tocdepth: 2 + +i18n with translated display text for references +================================================= + +.. _vectorcall: https://peps.python.org/pep-0590/ +.. _Documentation bugs: https://github.com/sphinx-doc/sphinx/issues + +.. glossary:: + + locale encoding + The encoding used for the locale. + + abstract base class + A base class that is abstract. + + text encoding + The encoding used for text. + + decorator + A function decorator. + + abstrakcyjna klasa bazowa + Translated: A base class that is abstract. + + kodowanie tekstu + Translated: The encoding used for text. + + dekorator + Translated: A function decorator. + +.. rubric:: Test cases from issue #14162 + +1. Add translated display text for hyperlink (case 2): vectorcall_. + +2. Translated display text for hyperlink (case 3): `Improved suggestions `_. + +3. Use translated hyperlink tag (case 4): `Documentation bugs`_. + +4. Translated glossary term (case 5): :term:`locale encoding`. + +5. Multiple translated refs: vectorcall_ and `Documentation bugs`_. + +.. rubric:: Additional term reference cases + +6. Term with explicit target and newline in source: :term:`virtual `. + +7. Term without explicit target: :term:`text encoding`. + +8. Simple term reference: :term:`decorator`. + +9. Multiple term refs: :term:`text encoding` and :term:`decorator`. diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/refs_translated_display_text.po b/tests/roots/test-intl/xx/LC_MESSAGES/refs_translated_display_text.po new file mode 100644 index 00000000000..ff0f87d0fbc --- /dev/null +++ b/tests/roots/test-intl/xx/LC_MESSAGES/refs_translated_display_text.po @@ -0,0 +1,94 @@ +# Test for translated display text in references. +# These should NOT trigger inconsistency warnings (issue #14162). +# +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 translated display text for references" +msgstr "I18N WITH TRANSLATED DISPLAY TEXT FOR REFERENCES" + +msgid "The encoding used for the locale." +msgstr "THE ENCODING USED FOR THE LOCALE." + +msgid "A base class that is abstract." +msgstr "A BASE CLASS THAT IS ABSTRACT." + +msgid "The encoding used for text." +msgstr "THE ENCODING USED FOR TEXT." + +msgid "A function decorator." +msgstr "A FUNCTION DECORATOR." + +msgid "Translated: A base class that is abstract." +msgstr "TRANSLATED: A BASE CLASS THAT IS ABSTRACT." + +msgid "Translated: The encoding used for text." +msgstr "TRANSLATED: THE ENCODING USED FOR TEXT." + +msgid "Translated: A function decorator." +msgstr "TRANSLATED: A FUNCTION DECORATOR." + +msgid "Test cases from issue #14162" +msgstr "TEST CASES FROM ISSUE #14162" + +# Case 2: Add translated display text for hyperlink +# Original: vectorcall_ +# Translated: add display text but keep same target +msgid "Add translated display text for hyperlink (case 2): vectorcall_." +msgstr "ADD TRANSLATED DISPLAY TEXT FOR HYPERLINK (CASE 2): `VECTORCALL `_." + +# Case 3: Translated display text for hyperlink +# Original: `Improved suggestions `_ +# Translated: translate display text but keep same URL +msgid "Translated display text for hyperlink (case 3): `Improved suggestions `_." +msgstr "TRANSLATED DISPLAY TEXT FOR HYPERLINK (CASE 3): `SUGGERIMENTI MIGLIORATI `_." + +# Case 4: Use translated hyperlink tag +# Original: `Documentation bugs`_ +# Translated: translate tag name - the fixup mechanism will correct the refname +msgid "Use translated hyperlink tag (case 4): `Documentation bugs`_." +msgstr "USE TRANSLATED HYPERLINK TAG (CASE 4): `TRANSLATED DOCUMENTATION BUGS`_." + +# Case 5: Translated glossary term +# Original: :term:`locale encoding` +# Translated: translate display text using explicit target syntax +msgid "Translated glossary term (case 5): :term:`locale encoding`." +msgstr "TRANSLATED GLOSSARY TERM (CASE 5): :term:`CODIFICAÇÃO DA LOCALIDADE `." + +# Multiple refs: should allow translating display text and rely on fixup +msgid "Multiple translated refs: vectorcall_ and `Documentation bugs`_." +msgstr "MULTIPLE TRANSLATED REFS: `TRANSLATED DOCUMENTATION BUGS`_ AND `VECTORCALL `_." + +msgid "Additional term reference cases" +msgstr "ADDITIONAL TERM REFERENCE CASES" + +# Term with explicit target - simulating newline in original (from PO wrapping) +# and translated glossary term name as target +msgid "" +"Term with explicit target and newline in source: :term:`virtual `." +msgstr "" +"TERM WITH EXPLICIT TARGET AND NEWLINE IN SOURCE: :term:`wirtualną " +"`." + +# Term without explicit target - translate the term name itself +# This simulates when the glossary is also translated +msgid "Term without explicit target: :term:`text encoding`." +msgstr "TERM WITHOUT EXPLICIT TARGET: :term:`kodowanie tekstu`." + +# Simple term reference - translate the term name +msgid "Simple term reference: :term:`decorator`." +msgstr "SIMPLE TERM REFERENCE: :term:`dekorator`." + +# Multiple term refs with translated names +msgid "Multiple term refs: :term:`text encoding` and :term:`decorator`." +msgstr "MULTIPLE TERM REFS: :term:`kodowanie tekstu` AND :term:`dekorator`." diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py index c823a6a3b44..b8d2cc761ca 100644 --- a/tests/test_intl/test_intl.py +++ b/tests/test_intl/test_intl.py @@ -413,6 +413,45 @@ def test_text_refs_reordered_no_warning(app: SphinxTestApp) -> None: ) +@sphinx_intl +@pytest.mark.sphinx('text', testroot='intl') +@pytest.mark.test_params(shared_result='test_intl_basic') +def test_text_refs_translated_display_text_no_warning(app: SphinxTestApp) -> None: + """Test that translated display text in references doesn't trigger warnings. + + This test covers cases 2-5 from issue #14162 plus additional term reference cases: + - Case 2: Add translated display text for hyperlink + - Case 3: Translated display text for hyperlink + - Case 4: Use translated hyperlink tag + - Case 5: Translated glossary term (with explicit target syntax) + - Additional: Term references with translated glossary term names + - Additional: Term references with newlines from PO file wrapping. + """ + app.build() + result = (app.outdir / 'refs_translated_display_text.txt').read_text( + encoding='utf8' + ) + + # Verify the translations were applied + assert 'I18N WITH TRANSLATED DISPLAY TEXT FOR REFERENCES' in result + assert 'TEST CASES FROM ISSUE #14162' in result + assert 'ADD TRANSLATED DISPLAY TEXT FOR HYPERLINK (CASE 2)' in result + assert 'TRANSLATED DISPLAY TEXT FOR HYPERLINK (CASE 3)' in result + assert 'USE TRANSLATED HYPERLINK TAG (CASE 4)' in result + assert 'TRANSLATED GLOSSARY TERM (CASE 5)' in result + assert 'MULTIPLE TRANSLATED REFS' in result + assert 'ADDITIONAL TERM REFERENCE CASES' in result + + warnings = getwarning(app.warning) + # Should NOT have any inconsistent_references warnings for refs_translated_display_text.txt + unexpected_warning_expr = ( + '.*/refs_translated_display_text.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')