diff --git a/CHANGES b/CHANGES
index 36e5cc77b4b..7a44cde27d9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,8 @@ Dependencies
Incompatible changes
--------------------
+* #7784: i18n: The msgid for alt text of image is changed
+
Deprecated
----------
@@ -26,6 +28,8 @@ Features added
* #7745: html: inventory is broken if the docname contains a space
* #7902: html theme: Add a new option :confval:`globaltoc_maxdepth` to control
the behavior of globaltoc in sidebar
+* #7784: i18n: The alt text for image is translated by default (without
+ :confval:`gettext_additional_targets` setting)
* #7052: add ``:noindexentry:`` to the Python, C, C++, and Javascript domains.
Update the documentation to better reflect the relationship between this option
and the ``:noindex:`` option.
diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst
index eba6904c40c..c962f1f3c90 100644
--- a/doc/usage/configuration.rst
+++ b/doc/usage/configuration.rst
@@ -802,13 +802,16 @@ documentation on :ref:`intl` for details.
:literal-block: literal blocks (``::`` annotation and ``code-block`` directive)
:doctest-block: doctest block
:raw: raw content
- :image: image/figure uri and alt
+ :image: image/figure uri
For example: ``gettext_additional_targets = ['literal-block', 'image']``.
The default is ``[]``.
.. versionadded:: 1.3
+ .. versionchanged:: 3.2
+
+ The alt text for image is translated by default.
.. confval:: figure_language_filename
diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py
index 34d5b13687d..eeeb3cb4f3a 100644
--- a/sphinx/transforms/i18n.py
+++ b/sphinx/transforms/i18n.py
@@ -238,6 +238,10 @@ def apply(self, **kwargs: Any) -> None:
node.details['nodes'][0]['content'] = msgstr
continue
+ if isinstance(node, nodes.image) and node.get('alt') == msg:
+ node['alt'] = msgstr
+ continue
+
# Avoid "Literal block expected; none found." warnings.
# If msgstr ends with '::' then it cause warning message at
# parser.parse() processing.
@@ -441,8 +445,9 @@ def get_ref_key(node: addnodes.pending_xref) -> Tuple[str, str, str]:
if isinstance(node, LITERAL_TYPE_NODES):
node.rawsource = node.astext()
- if isinstance(node, IMAGE_TYPE_NODES):
- node.update_all_atts(patch)
+ if isinstance(node, nodes.image) and node.get('alt') != msg:
+ node['uri'] = patch['uri']
+ continue # do not mark translated
node['translated'] = True # to avoid double translation
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py
index b4d796f6184..725f4037915 100644
--- a/sphinx/util/nodes.py
+++ b/sphinx/util/nodes.py
@@ -201,6 +201,10 @@ def is_translatable(node: Node) -> bool:
if isinstance(node, addnodes.translatable):
return True
+ # image node marked as translatable or having alt text
+ if isinstance(node, nodes.image) and (node.get('translatable') or node.get('alt')):
+ return True
+
if isinstance(node, nodes.Inline) and 'translatable' not in node: # type: ignore
# inline node must not be translated if 'translatable' is not set
return False
@@ -228,9 +232,6 @@ def is_translatable(node: Node) -> bool:
return False
return True
- if isinstance(node, nodes.image) and node.get('translatable'):
- return True
-
if isinstance(node, addnodes.meta):
return True
if is_pending_meta(node):
@@ -264,10 +265,13 @@ def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]:
msg = node.rawsource
if not msg:
msg = node.astext()
- elif isinstance(node, IMAGE_TYPE_NODES):
- msg = '.. image:: %s' % node['uri']
+ elif isinstance(node, nodes.image):
if node.get('alt'):
- msg += '\n :alt: %s' % node['alt']
+ yield node, node['alt']
+ if node.get('translatable'):
+ msg = '.. image:: %s' % node['uri']
+ else:
+ msg = None
elif isinstance(node, META_TYPE_NODES):
msg = node.rawcontent
elif isinstance(node, nodes.pending) and is_pending_meta(node):
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/figure.po b/tests/roots/test-intl/xx/LC_MESSAGES/figure.po
index 449b15e3f3e..64bbdf763db 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/figure.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/figure.po
@@ -37,19 +37,17 @@ msgstr "BLOCK"
msgid "image url and alt"
msgstr "IMAGE URL AND ALT"
-msgid ""
-".. image:: img.png\n"
-" :alt: img"
-msgstr ""
-".. image:: i18n.png\n"
-" :alt: IMG -> I18N"
+msgid "img"
+msgstr "IMG -> I18N"
-msgid ""
-".. image:: i18n.png\n"
-" :alt: i18n"
-msgstr ""
-".. image:: img.png\n"
-" :alt: I18N -> IMG"
+msgid ".. image:: img.png"
+msgstr ".. image:: i18n.png"
+
+msgid "i18n"
+msgstr "I18N -> IMG"
+
+msgid ".. image:: i18n.png"
+msgstr ".. image:: img.png"
msgid "image on substitution"
msgstr "IMAGE ON SUBSTITUTION"
diff --git a/tests/test_intl.py b/tests/test_intl.py
index d0c64b589df..d9701343ef9 100644
--- a/tests/test_intl.py
+++ b/tests/test_intl.py
@@ -345,9 +345,9 @@ def test_text_figure_captions(app):
"14.2. IMAGE URL AND ALT\n"
"=======================\n"
"\n"
- "[image: i18n][image]\n"
+ "[image: I18N -> IMG][image]\n"
"\n"
- " [image: img][image]\n"
+ " [image: IMG -> I18N][image]\n"
"\n"
"\n"
"14.3. IMAGE ON SUBSTITUTION\n"
@@ -1102,12 +1102,12 @@ def test_additional_targets_should_not_be_translated(app):
result = (app.outdir / 'figure.html').read_text()
- # alt and src for image block should not be translated
- expected_expr = """"""
+ # src for image block should not be translated (alt is translated)
+ expected_expr = """"""
assert_count(expected_expr, result, 1)
- # alt and src for figure block should not be translated
- expected_expr = """"""
+ # src for figure block should not be translated (alt is translated)
+ expected_expr = """"""
assert_count(expected_expr, result, 1)