Fixed #19552 -- Enhanced makemessages handling of ``{# #}``-style template comments.
They are simply ignored now. This allows for a more correct behavior when they are placed before translatable constructs on the same line. Previously, the latter were wrongly ignored because the former were preserved when converting template code to the internal Python-syntax form later fed to xgettext but Python has no ``/* ... */``-style comments. Also, special comments directed to translators are now only taken in account when they are located at the end of a line. e.g.:: {# Translators: ignored #}{% trans "Literal A" %}{# Translators: valid, associated with "Literal B" below #} {% trans "Literal B" %} Behavior of ``{% comment %}...{% endcomment %}``tags remains unchanged. Thanks juneih at redpill-linpro dot com for the report and Claude for his work on the issue.
This commit is contained in:
parent
eb9430fc4b
commit
47ddd6a408
|
@ -21,6 +21,11 @@ __all__ = [
|
||||||
'npgettext', 'npgettext_lazy',
|
'npgettext', 'npgettext_lazy',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatorCommentWarning(SyntaxWarning):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Here be dragons, so a short explanation of the logic won't hurt:
|
# Here be dragons, so a short explanation of the logic won't hurt:
|
||||||
# We are trying to solve two problems: (1) access settings, in particular
|
# We are trying to solve two problems: (1) access settings, in particular
|
||||||
# settings.USE_I18N, as late as possible, so that modules can be imported
|
# settings.USE_I18N, as late as possible, so that modules can be imported
|
||||||
|
|
|
@ -7,6 +7,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import gettext as gettext_module
|
import gettext as gettext_module
|
||||||
from threading import local
|
from threading import local
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
from django.utils.encoding import force_str, force_text
|
from django.utils.encoding import force_str, force_text
|
||||||
|
@ -14,6 +15,7 @@ from django.utils._os import upath
|
||||||
from django.utils.safestring import mark_safe, SafeData
|
from django.utils.safestring import mark_safe, SafeData
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.six import StringIO
|
from django.utils.six import StringIO
|
||||||
|
from django.utils.translation import TranslatorCommentWarning
|
||||||
|
|
||||||
|
|
||||||
# Translations are cached in a dictionary for every language+app tuple.
|
# Translations are cached in a dictionary for every language+app tuple.
|
||||||
|
@ -41,6 +43,7 @@ accept_language_re = re.compile(r'''
|
||||||
|
|
||||||
language_code_prefix_re = re.compile(r'^/([\w-]+)(/|$)')
|
language_code_prefix_re = re.compile(r'^/([\w-]+)(/|$)')
|
||||||
|
|
||||||
|
|
||||||
def to_locale(language, to_lower=False):
|
def to_locale(language, to_lower=False):
|
||||||
"""
|
"""
|
||||||
Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
|
Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
|
||||||
|
@ -468,6 +471,9 @@ def templatize(src, origin=None):
|
||||||
plural = []
|
plural = []
|
||||||
incomment = False
|
incomment = False
|
||||||
comment = []
|
comment = []
|
||||||
|
lineno_comment_map = {}
|
||||||
|
comment_lineno_cache = None
|
||||||
|
|
||||||
for t in Lexer(src, origin).tokenize():
|
for t in Lexer(src, origin).tokenize():
|
||||||
if incomment:
|
if incomment:
|
||||||
if t.token_type == TOKEN_BLOCK and t.contents == 'endcomment':
|
if t.token_type == TOKEN_BLOCK and t.contents == 'endcomment':
|
||||||
|
@ -529,7 +535,27 @@ def templatize(src, origin=None):
|
||||||
plural.append(contents)
|
plural.append(contents)
|
||||||
else:
|
else:
|
||||||
singular.append(contents)
|
singular.append(contents)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Handle comment tokens (`{# ... #}`) plus other constructs on
|
||||||
|
# the same line:
|
||||||
|
if comment_lineno_cache is not None:
|
||||||
|
cur_lineno = t.lineno + t.contents.count('\n')
|
||||||
|
if comment_lineno_cache == cur_lineno:
|
||||||
|
if t.token_type != TOKEN_COMMENT:
|
||||||
|
for c in lineno_comment_map[comment_lineno_cache]:
|
||||||
|
filemsg = ''
|
||||||
|
if origin:
|
||||||
|
filemsg = 'file %s, ' % origin
|
||||||
|
warn_msg = ("The translator-targeted comment '%s' "
|
||||||
|
"(%sline %d) was ignored, because it wasn't the last item "
|
||||||
|
"on the line.") % (c, filemsg, comment_lineno_cache)
|
||||||
|
warnings.warn(warn_msg, TranslatorCommentWarning)
|
||||||
|
lineno_comment_map[comment_lineno_cache] = []
|
||||||
|
else:
|
||||||
|
out.write('# %s' % ' | '.join(lineno_comment_map[comment_lineno_cache]))
|
||||||
|
comment_lineno_cache = None
|
||||||
|
|
||||||
if t.token_type == TOKEN_BLOCK:
|
if t.token_type == TOKEN_BLOCK:
|
||||||
imatch = inline_re.match(t.contents)
|
imatch = inline_re.match(t.contents)
|
||||||
bmatch = block_re.match(t.contents)
|
bmatch = block_re.match(t.contents)
|
||||||
|
@ -586,7 +612,10 @@ def templatize(src, origin=None):
|
||||||
else:
|
else:
|
||||||
out.write(blankout(p, 'F'))
|
out.write(blankout(p, 'F'))
|
||||||
elif t.token_type == TOKEN_COMMENT:
|
elif t.token_type == TOKEN_COMMENT:
|
||||||
out.write(' # %s' % t.contents)
|
if t.contents.lstrip().startswith(TRANSLATOR_COMMENT_MARK):
|
||||||
|
lineno_comment_map.setdefault(t.lineno,
|
||||||
|
[]).append(t.contents)
|
||||||
|
comment_lineno_cache = t.lineno
|
||||||
else:
|
else:
|
||||||
out.write(blankout(t.contents, 'X'))
|
out.write(blankout(t.contents, 'X'))
|
||||||
return force_str(out.getvalue())
|
return force_str(out.getvalue())
|
||||||
|
|
|
@ -47,6 +47,26 @@ Backwards incompatible changes in 1.6
|
||||||
should review it as ``type='text'`` widgets might be now output as
|
should review it as ``type='text'`` widgets might be now output as
|
||||||
``type='email'`` or ``type='url'`` depending on their corresponding field type.
|
``type='email'`` or ``type='url'`` depending on their corresponding field type.
|
||||||
|
|
||||||
|
* Extraction of translatable literals from templates with the
|
||||||
|
:djadmin:`makemessages` command now correctly detects i18n constructs when
|
||||||
|
they are located after a ``{#`` / ``#}``-type comment on the same line. E.g.:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{# A comment #}{% trans "This literal was incorrectly ignored. Not anymore" %}
|
||||||
|
|
||||||
|
* (Related to the above item.) Validation of the placement of
|
||||||
|
:ref:`translator-comments-in-templates` specified using ``{#`` / ``#}`` is now
|
||||||
|
stricter. All translator comments not located at the end of their respective
|
||||||
|
lines in a template are ignored and a warning is generated by
|
||||||
|
:djadmin:`makemessages` when it finds them. E.g.:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{# Translators: This is ignored #}{% trans "Translate me" %}
|
||||||
|
{{ title }}{# Translators: Extracted and associated with 'Welcome' below #}
|
||||||
|
<h1>{% trans "Welcome" %}</h1>
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
In addition to the changes outlined in this section, be sure to review the
|
In addition to the changes outlined in this section, be sure to review the
|
||||||
|
|
|
@ -142,14 +142,22 @@ preceding the string, e.g.::
|
||||||
# Translators: This message appears on the home page only
|
# Translators: This message appears on the home page only
|
||||||
output = ugettext("Welcome to my site.")
|
output = ugettext("Welcome to my site.")
|
||||||
|
|
||||||
This also works in templates with the :ttag:`comment` tag:
|
The comment will then appear in the resulting ``.po`` file associated with the
|
||||||
|
translatable contruct located below it and should also be displayed by most
|
||||||
|
translation tools.
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. note:: Just for completeness, this is the corresponding fragment of the
|
||||||
|
resulting ``.po`` file:
|
||||||
|
|
||||||
{% comment %}Translators: This is a text of the base template {% endcomment %}
|
.. code-block:: po
|
||||||
|
|
||||||
The comment will then appear in the resulting ``.po`` file and should also be
|
#. Translators: This message appears on the home page only
|
||||||
displayed by most translation tools.
|
# path/to/python/file.py:123
|
||||||
|
msgid "Welcome to my site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
This also works in templates. See :ref:`translator-comments-in-templates` for
|
||||||
|
more details.
|
||||||
|
|
||||||
Marking strings as no-op
|
Marking strings as no-op
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -620,6 +628,63 @@ markers<contextual-markers>` using the ``context`` keyword:
|
||||||
|
|
||||||
{% blocktrans with name=user.username context "greeting" %}Hi {{ name }}{% endblocktrans %}
|
{% blocktrans with name=user.username context "greeting" %}Hi {{ name }}{% endblocktrans %}
|
||||||
|
|
||||||
|
.. _translator-comments-in-templates:
|
||||||
|
|
||||||
|
Comments for translators in templates
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
Just like with :ref:`Python code <translator-comments>`, these notes for
|
||||||
|
translators can be specified using comments, either with the :ttag:`comment`
|
||||||
|
tag:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% comment %}Translators: View verb{% endcomment %}
|
||||||
|
{% trans "View" %}
|
||||||
|
|
||||||
|
{% comment %}Translators: Short intro blurb{% endcomment %}
|
||||||
|
<p>{% blocktrans %}A multiline translatable
|
||||||
|
literal.{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs <template-comments>`:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{# Translators: Label of a button that triggers search{% endcomment #}
|
||||||
|
<button type="submit">{% trans "Go" %}</button>
|
||||||
|
|
||||||
|
{# Translators: This is a text of the base template #}
|
||||||
|
{% blocktrans %}Ambiguous translatable block of text{% endtransblock %}
|
||||||
|
|
||||||
|
.. note:: Just for completeness, these are the corresponding fragments of the
|
||||||
|
resulting ``.po`` file:
|
||||||
|
|
||||||
|
.. code-block:: po
|
||||||
|
|
||||||
|
#. Translators: View verb
|
||||||
|
# path/to/template/file.html:10
|
||||||
|
msgid "View"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Translators: Short intro blurb
|
||||||
|
# path/to/template/file.html:13
|
||||||
|
msgid ""
|
||||||
|
"A multiline translatable"
|
||||||
|
"literal."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
#. Translators: Label of a button that triggers search
|
||||||
|
# path/to/template/file.html:100
|
||||||
|
msgid "Go"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Translators:
|
||||||
|
# path/to/template/file.html:103
|
||||||
|
msgid "Ambiguous translatable block of text"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
.. _template-translation-vars:
|
.. _template-translation-vars:
|
||||||
|
|
||||||
Other tags
|
Other tags
|
||||||
|
|
|
@ -250,6 +250,8 @@ You can also create your own custom template tags; see
|
||||||
tags and filters available for a given site. See
|
tags and filters available for a given site. See
|
||||||
:doc:`/ref/contrib/admin/admindocs`.
|
:doc:`/ref/contrib/admin/admindocs`.
|
||||||
|
|
||||||
|
.. _template-comments:
|
||||||
|
|
||||||
Comments
|
Comments
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
@ -11,6 +12,7 @@ from django.utils.encoding import force_text
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.six import StringIO
|
from django.utils.six import StringIO
|
||||||
|
from django.utils.translation import TranslatorCommentWarning
|
||||||
|
|
||||||
|
|
||||||
LOCALE='de'
|
LOCALE='de'
|
||||||
|
@ -120,6 +122,7 @@ class BasicExtractorTests(ExtractorTests):
|
||||||
self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py'))
|
self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py'))
|
||||||
|
|
||||||
def test_extraction_warning(self):
|
def test_extraction_warning(self):
|
||||||
|
"""test xgettext warning about multiple bare interpolation placeholders"""
|
||||||
os.chdir(self.test_dir)
|
os.chdir(self.test_dir)
|
||||||
shutil.copyfile('./code.sample', './code_sample.py')
|
shutil.copyfile('./code.sample', './code_sample.py')
|
||||||
stdout = StringIO()
|
stdout = StringIO()
|
||||||
|
@ -172,6 +175,63 @@ class BasicExtractorTests(ExtractorTests):
|
||||||
self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents)
|
self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents)
|
||||||
self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents)
|
self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents)
|
||||||
|
|
||||||
|
def test_template_comments(self):
|
||||||
|
"""Template comment tags on the same line of other constructs (#19552)"""
|
||||||
|
os.chdir(self.test_dir)
|
||||||
|
# Test detection/end user reporting of old, incorrect templates
|
||||||
|
# translator comments syntax
|
||||||
|
with warnings.catch_warnings(record=True) as ws:
|
||||||
|
warnings.simplefilter('always')
|
||||||
|
management.call_command('makemessages', locale=LOCALE, extensions=['thtml'], verbosity=0)
|
||||||
|
self.assertEqual(len(ws), 3)
|
||||||
|
for w in ws:
|
||||||
|
self.assertTrue(issubclass(w.category, TranslatorCommentWarning))
|
||||||
|
six.assertRegex(self, str(ws[0].message),
|
||||||
|
r"The translator-targeted comment 'Translators: ignored i18n comment #1' \(file templates/comments.thtml, line 4\) was ignored, because it wasn't the last item on the line\."
|
||||||
|
)
|
||||||
|
six.assertRegex(self, str(ws[1].message),
|
||||||
|
r"The translator-targeted comment 'Translators: ignored i18n comment #3' \(file templates/comments.thtml, line 6\) was ignored, because it wasn't the last item on the line\."
|
||||||
|
)
|
||||||
|
six.assertRegex(self, str(ws[2].message),
|
||||||
|
r"The translator-targeted comment 'Translators: ignored i18n comment #4' \(file templates/comments.thtml, line 8\) was ignored, because it wasn't the last item on the line\."
|
||||||
|
)
|
||||||
|
# Now test .po file contents
|
||||||
|
self.assertTrue(os.path.exists(self.PO_FILE))
|
||||||
|
with open(self.PO_FILE, 'r') as fp:
|
||||||
|
po_contents = force_text(fp.read())
|
||||||
|
|
||||||
|
self.assertMsgId('Translatable literal #9a', po_contents)
|
||||||
|
self.assertFalse('ignored comment #1' in po_contents)
|
||||||
|
|
||||||
|
self.assertFalse('Translators: ignored i18n comment #1' in po_contents)
|
||||||
|
self.assertMsgId("Translatable literal #9b", po_contents)
|
||||||
|
|
||||||
|
self.assertFalse('ignored i18n comment #2' in po_contents)
|
||||||
|
self.assertFalse('ignored comment #2' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9c', po_contents)
|
||||||
|
|
||||||
|
self.assertFalse('ignored comment #3' in po_contents)
|
||||||
|
self.assertFalse('ignored i18n comment #3' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9d', po_contents)
|
||||||
|
|
||||||
|
self.assertFalse('ignored comment #4' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9e', po_contents)
|
||||||
|
self.assertFalse('ignored comment #5' in po_contents)
|
||||||
|
|
||||||
|
self.assertFalse('ignored i18n comment #4' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9f', po_contents)
|
||||||
|
self.assertTrue('#. Translators: valid i18n comment #5' in po_contents)
|
||||||
|
|
||||||
|
self.assertMsgId('Translatable literal #9g', po_contents)
|
||||||
|
self.assertTrue('#. Translators: valid i18n comment #6' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9h', po_contents)
|
||||||
|
self.assertTrue('#. Translators: valid i18n comment #7' in po_contents)
|
||||||
|
self.assertMsgId('Translatable literal #9i', po_contents)
|
||||||
|
|
||||||
|
six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #8')
|
||||||
|
six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #9')
|
||||||
|
self.assertMsgId("Translatable literal #9j", po_contents)
|
||||||
|
|
||||||
|
|
||||||
class JavascriptExtractorTests(ExtractorTests):
|
class JavascriptExtractorTests(ExtractorTests):
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{# ignored comment #1 #}{% trans "Translatable literal #9a" %}
|
||||||
|
{# Translators: ignored i18n comment #1 #}{% trans "Translatable literal #9b" %}
|
||||||
|
{# Translators: ignored i18n comment #2 #}{# ignored comment #2 #}{% trans "Translatable literal #9c" %}
|
||||||
|
{# ignored comment #3 #}{# Translators: ignored i18n comment #3 #}{% trans "Translatable literal #9d" %}
|
||||||
|
{# ignored comment #4 #}{% trans "Translatable literal #9e" %}{# ignored comment #5 #}
|
||||||
|
{# Translators: ignored i18n comment #4 #}{% trans "Translatable literal #9f" %}{# Translators: valid i18n comment #5 #}
|
||||||
|
{% trans "Translatable literal #9g" %}{# Translators: valid i18n comment #6 #}
|
||||||
|
{# ignored comment #6 #}{% trans "Translatable literal #9h" %}{# Translators: valid i18n comment #7 #}
|
||||||
|
{% trans "Translatable literal #9i" %}
|
||||||
|
{# Translators: valid i18n comment #8 #}{# Translators: valid i18n comment #9 #}
|
||||||
|
{% trans "Translatable literal #9j" %}
|
Loading…
Reference in New Issue