1
# -*- encoding: utf-8 -*-
2
from __future__ import unicode_literals
10
from django.core import management
11
from django.test import SimpleTestCase
12
from django.utils.encoding import force_text
13
from django.utils._os import upath
14
from django.utils import six
15
from django.utils.six import StringIO
16
from django.utils.translation import TranslatorCommentWarning
17
from django.utils.unittest import SkipTest
22
class ExtractorTests(SimpleTestCase):
24
PO_FILE='locale/%s/LC_MESSAGES/django.po' % LOCALE
27
self._cwd = os.getcwd()
28
self.test_dir = os.path.abspath(os.path.dirname(upath(__file__)))
30
def _rmrf(self, dname):
31
if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
35
def rmfile(self, filepath):
36
if os.path.exists(filepath):
40
os.chdir(self.test_dir)
42
self._rmrf('locale/%s' % LOCALE)
47
def assertMsgId(self, msgid, s, use_quotes=True):
50
msgid = '"%s"' % msgid
52
needle = 'msgid %s' % msgid
53
msgid = re.escape(msgid)
54
return self.assertTrue(re.search('^msgid %s' % msgid, s, re.MULTILINE), 'Could not find %(q)s%(n)s%(q)s in generated PO file' % {'n':needle, 'q':q})
56
def assertNotMsgId(self, msgid, s, use_quotes=True):
58
msgid = '"%s"' % msgid
59
msgid = re.escape(msgid)
60
return self.assertTrue(not re.search('^msgid %s' % msgid, s, re.MULTILINE))
63
class BasicExtractorTests(ExtractorTests):
65
def test_comments_extractor(self):
66
os.chdir(self.test_dir)
67
management.call_command('makemessages', locale=LOCALE, verbosity=0)
68
self.assertTrue(os.path.exists(self.PO_FILE))
69
with io.open(self.PO_FILE, 'r', encoding='utf-8') as fp:
70
po_contents = fp.read()
71
self.assertTrue('#. Translators: This comment should be extracted' in po_contents)
72
self.assertTrue('This comment should not be extracted' not in po_contents)
73
# Comments in templates
74
self.assertTrue('#. Translators: Django template comment for translators' in po_contents)
75
self.assertTrue("#. Translators: Django comment block for translators\n#. string's meaning unveiled" in po_contents)
77
self.assertTrue('#. Translators: One-line translator comment #1' in po_contents)
78
self.assertTrue('#. Translators: Two-line translator comment #1\n#. continued here.' in po_contents)
80
self.assertTrue('#. Translators: One-line translator comment #2' in po_contents)
81
self.assertTrue('#. Translators: Two-line translator comment #2\n#. continued here.' in po_contents)
83
self.assertTrue('#. Translators: One-line translator comment #3' in po_contents)
84
self.assertTrue('#. Translators: Two-line translator comment #3\n#. continued here.' in po_contents)
86
self.assertTrue('#. Translators: One-line translator comment #4' in po_contents)
87
self.assertTrue('#. Translators: Two-line translator comment #4\n#. continued here.' in po_contents)
89
self.assertTrue('#. Translators: One-line translator comment #5 -- with non ASCII characters: áéíóúö' in po_contents)
90
self.assertTrue('#. Translators: Two-line translator comment #5 -- with non ASCII characters: áéíóúö\n#. continued here.' in po_contents)
92
def test_templatize_trans_tag(self):
94
os.chdir(self.test_dir)
95
management.call_command('makemessages', locale=LOCALE, verbosity=0)
96
self.assertTrue(os.path.exists(self.PO_FILE))
97
with open(self.PO_FILE, 'r') as fp:
98
po_contents = force_text(fp.read())
99
self.assertMsgId('Literal with a percent symbol at the end %%', po_contents)
100
self.assertMsgId('Literal with a percent %% symbol in the middle', po_contents)
101
self.assertMsgId('Completed 50%% of all the tasks', po_contents)
102
self.assertMsgId('Completed 99%% of all the tasks', po_contents)
103
self.assertMsgId("Shouldn't double escape this sequence: %% (two percent signs)", po_contents)
104
self.assertMsgId("Shouldn't double escape this sequence %% either", po_contents)
105
self.assertMsgId("Looks like a str fmt spec %%s but shouldn't be interpreted as such", po_contents)
106
self.assertMsgId("Looks like a str fmt spec %% o but shouldn't be interpreted as such", po_contents)
108
def test_templatize_blocktrans_tag(self):
110
os.chdir(self.test_dir)
111
management.call_command('makemessages', locale=LOCALE, verbosity=0)
112
self.assertTrue(os.path.exists(self.PO_FILE))
113
with open(self.PO_FILE, 'r') as fp:
114
po_contents = force_text(fp.read())
115
self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents)
116
self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents)
117
self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents)
119
def test_force_en_us_locale(self):
120
"""Value of locale-munging option used by the command is the right one"""
121
from django.core.management.commands.makemessages import Command
122
self.assertTrue(Command.leave_locale_alone)
124
def test_extraction_error(self):
125
os.chdir(self.test_dir)
126
self.assertRaises(SyntaxError, management.call_command, 'makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0)
127
with self.assertRaises(SyntaxError) as context_manager:
128
management.call_command('makemessages', locale=LOCALE, extensions=['tpl'], verbosity=0)
129
six.assertRegex(self, str(context_manager.exception),
130
r'Translation blocks must not include other block tags: blocktrans \(file templates[/\\]template_with_error\.tpl, line 3\)'
132
# Check that the temporary file was cleaned up
133
self.assertFalse(os.path.exists('./templates/template_with_error.tpl.py'))
135
def test_unicode_decode_error(self):
136
os.chdir(self.test_dir)
137
shutil.copyfile('./not_utf8.sample', './not_utf8.txt')
138
self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'not_utf8.txt'))
140
management.call_command('makemessages', locale=LOCALE, stdout=stdout)
141
self.assertIn("UnicodeDecodeError: skipped file not_utf8.txt in .",
142
force_text(stdout.getvalue()))
144
def test_extraction_warning(self):
145
"""test xgettext warning about multiple bare interpolation placeholders"""
146
os.chdir(self.test_dir)
147
shutil.copyfile('./code.sample', './code_sample.py')
148
self.addCleanup(self.rmfile, os.path.join(self.test_dir, 'code_sample.py'))
150
management.call_command('makemessages', locale=LOCALE, stdout=stdout)
151
self.assertIn("code_sample.py:4", force_text(stdout.getvalue()))
153
def test_template_message_context_extractor(self):
155
Ensure that message contexts are correctly extracted for the
156
{% trans %} and {% blocktrans %} template tags.
159
os.chdir(self.test_dir)
160
management.call_command('makemessages', locale=LOCALE, verbosity=0)
161
self.assertTrue(os.path.exists(self.PO_FILE))
162
with open(self.PO_FILE, 'r') as fp:
163
po_contents = force_text(fp.read())
165
self.assertTrue('msgctxt "Special trans context #1"' in po_contents)
166
self.assertMsgId("Translatable literal #7a", po_contents)
167
self.assertTrue('msgctxt "Special trans context #2"' in po_contents)
168
self.assertMsgId("Translatable literal #7b", po_contents)
169
self.assertTrue('msgctxt "Special trans context #3"' in po_contents)
170
self.assertMsgId("Translatable literal #7c", po_contents)
173
self.assertTrue('msgctxt "Special blocktrans context #1"' in po_contents)
174
self.assertMsgId("Translatable literal #8a", po_contents)
175
self.assertTrue('msgctxt "Special blocktrans context #2"' in po_contents)
176
self.assertMsgId("Translatable literal #8b-singular", po_contents)
177
self.assertTrue("Translatable literal #8b-plural" in po_contents)
178
self.assertTrue('msgctxt "Special blocktrans context #3"' in po_contents)
179
self.assertMsgId("Translatable literal #8c-singular", po_contents)
180
self.assertTrue("Translatable literal #8c-plural" in po_contents)
181
self.assertTrue('msgctxt "Special blocktrans context #4"' in po_contents)
182
self.assertMsgId("Translatable literal #8d %(a)s", po_contents)
184
def test_context_in_single_quotes(self):
185
os.chdir(self.test_dir)
186
management.call_command('makemessages', locale=LOCALE, verbosity=0)
187
self.assertTrue(os.path.exists(self.PO_FILE))
188
with open(self.PO_FILE, 'r') as fp:
189
po_contents = force_text(fp.read())
191
self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents)
192
self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents)
195
self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents)
196
self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents)
198
def test_template_comments(self):
199
"""Template comment tags on the same line of other constructs (#19552)"""
200
os.chdir(self.test_dir)
201
# Test detection/end user reporting of old, incorrect templates
202
# translator comments syntax
203
with warnings.catch_warnings(record=True) as ws:
204
warnings.simplefilter('always')
205
management.call_command('makemessages', locale=LOCALE, extensions=['thtml'], verbosity=0)
206
self.assertEqual(len(ws), 3)
208
self.assertTrue(issubclass(w.category, TranslatorCommentWarning))
209
six.assertRegex(self, str(ws[0].message),
210
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\."
212
six.assertRegex(self, str(ws[1].message),
213
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\."
215
six.assertRegex(self, str(ws[2].message),
216
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\."
218
# Now test .po file contents
219
self.assertTrue(os.path.exists(self.PO_FILE))
220
with open(self.PO_FILE, 'r') as fp:
221
po_contents = force_text(fp.read())
223
self.assertMsgId('Translatable literal #9a', po_contents)
224
self.assertFalse('ignored comment #1' in po_contents)
226
self.assertFalse('Translators: ignored i18n comment #1' in po_contents)
227
self.assertMsgId("Translatable literal #9b", po_contents)
229
self.assertFalse('ignored i18n comment #2' in po_contents)
230
self.assertFalse('ignored comment #2' in po_contents)
231
self.assertMsgId('Translatable literal #9c', po_contents)
233
self.assertFalse('ignored comment #3' in po_contents)
234
self.assertFalse('ignored i18n comment #3' in po_contents)
235
self.assertMsgId('Translatable literal #9d', po_contents)
237
self.assertFalse('ignored comment #4' in po_contents)
238
self.assertMsgId('Translatable literal #9e', po_contents)
239
self.assertFalse('ignored comment #5' in po_contents)
241
self.assertFalse('ignored i18n comment #4' in po_contents)
242
self.assertMsgId('Translatable literal #9f', po_contents)
243
self.assertTrue('#. Translators: valid i18n comment #5' in po_contents)
245
self.assertMsgId('Translatable literal #9g', po_contents)
246
self.assertTrue('#. Translators: valid i18n comment #6' in po_contents)
247
self.assertMsgId('Translatable literal #9h', po_contents)
248
self.assertTrue('#. Translators: valid i18n comment #7' in po_contents)
249
self.assertMsgId('Translatable literal #9i', po_contents)
251
six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #8')
252
six.assertRegex(self, po_contents, r'#\..+Translators: valid i18n comment #9')
253
self.assertMsgId("Translatable literal #9j", po_contents)
256
class JavascriptExtractorTests(ExtractorTests):
258
PO_FILE='locale/%s/LC_MESSAGES/djangojs.po' % LOCALE
260
def test_javascript_literals(self):
261
os.chdir(self.test_dir)
262
management.call_command('makemessages', domain='djangojs', locale=LOCALE, verbosity=0)
263
self.assertTrue(os.path.exists(self.PO_FILE))
264
with open(self.PO_FILE, 'r') as fp:
265
po_contents = fp.read()
266
self.assertMsgId('This literal should be included.', po_contents)
267
self.assertMsgId('This one as well.', po_contents)
268
self.assertMsgId(r'He said, \"hello\".', po_contents)
269
self.assertMsgId("okkkk", po_contents)
270
self.assertMsgId("TEXT", po_contents)
271
self.assertMsgId("It's at http://example.com", po_contents)
272
self.assertMsgId("String", po_contents)
273
self.assertMsgId("/* but this one will be too */ 'cause there is no way of telling...", po_contents)
274
self.assertMsgId("foo", po_contents)
275
self.assertMsgId("bar", po_contents)
276
self.assertMsgId("baz", po_contents)
277
self.assertMsgId("quz", po_contents)
278
self.assertMsgId("foobar", po_contents)
280
class IgnoredExtractorTests(ExtractorTests):
282
def test_ignore_option(self):
283
os.chdir(self.test_dir)
285
os.path.join('ignore_dir', '*'),
289
management.call_command('makemessages', locale=LOCALE, verbosity=2,
290
ignore_patterns=ignore_patterns, stdout=stdout)
291
data = stdout.getvalue()
292
self.assertTrue("ignoring directory ignore_dir" in data)
293
self.assertTrue("ignoring file xxx_ignored.html" in data)
294
self.assertTrue(os.path.exists(self.PO_FILE))
295
with open(self.PO_FILE, 'r') as fp:
296
po_contents = fp.read()
297
self.assertMsgId('This literal should be included.', po_contents)
298
self.assertNotMsgId('This should be ignored.', po_contents)
299
self.assertNotMsgId('This should be ignored too.', po_contents)
302
class SymlinkExtractorTests(ExtractorTests):
305
self._cwd = os.getcwd()
306
self.test_dir = os.path.abspath(os.path.dirname(upath(__file__)))
307
self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked')
310
super(SymlinkExtractorTests, self).tearDown()
311
os.chdir(self.test_dir)
313
os.remove(self.symlinked_dir)
318
def test_symlink(self):
319
# On Python < 3.2 os.symlink() exists only on Unix
320
if hasattr(os, 'symlink'):
321
if os.path.exists(self.symlinked_dir):
322
self.assertTrue(os.path.islink(self.symlinked_dir))
324
# On Python >= 3.2) os.symlink() exists always but then can
325
# fail at runtime when user hasn't the needed permissions on
326
# WIndows versions that support symbolink links (>= 6/Vista).
327
# See Python issue 9333 (http://bugs.python.org/issue9333).
328
# Skip the test in that case
330
os.symlink(os.path.join(self.test_dir, 'templates'), self.symlinked_dir)
331
except (OSError, NotImplementedError):
332
raise SkipTest("os.symlink() is available on this OS but can't be used by this user.")
333
os.chdir(self.test_dir)
334
management.call_command('makemessages', locale=LOCALE, verbosity=0, symlinks=True)
335
self.assertTrue(os.path.exists(self.PO_FILE))
336
with open(self.PO_FILE, 'r') as fp:
337
po_contents = force_text(fp.read())
338
self.assertMsgId('This literal should be included.', po_contents)
339
self.assertTrue('templates_symlinked/test.html' in po_contents)
342
class CopyPluralFormsExtractorTests(ExtractorTests):
343
PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'
346
super(CopyPluralFormsExtractorTests, self).tearDown()
347
os.chdir(self.test_dir)
349
self._rmrf('locale/es')
354
def test_copy_plural_forms(self):
355
os.chdir(self.test_dir)
356
management.call_command('makemessages', locale=LOCALE, verbosity=0)
357
self.assertTrue(os.path.exists(self.PO_FILE))
358
with open(self.PO_FILE, 'r') as fp:
359
po_contents = force_text(fp.read())
360
self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents)
362
def test_override_plural_forms(self):
364
os.chdir(self.test_dir)
365
management.call_command('makemessages', locale='es', extensions=['djtpl'], verbosity=0)
366
self.assertTrue(os.path.exists(self.PO_FILE_ES))
367
with io.open(self.PO_FILE_ES, 'r', encoding='utf-8') as fp:
368
po_contents = fp.read()
369
found = re.findall(r'^(?P<value>"Plural-Forms.+?\\n")\s*$', po_contents, re.MULTILINE | re.DOTALL)
370
self.assertEqual(1, len(found))
373
class NoWrapExtractorTests(ExtractorTests):
375
def test_no_wrap_enabled(self):
376
os.chdir(self.test_dir)
377
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=True)
378
self.assertTrue(os.path.exists(self.PO_FILE))
379
with open(self.PO_FILE, 'r') as fp:
380
po_contents = force_text(fp.read())
381
self.assertMsgId('This literal should also be included wrapped or not wrapped depending on the use of the --no-wrap option.', po_contents)
383
def test_no_wrap_disabled(self):
384
os.chdir(self.test_dir)
385
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=False)
386
self.assertTrue(os.path.exists(self.PO_FILE))
387
with open(self.PO_FILE, 'r') as fp:
388
po_contents = force_text(fp.read())
389
self.assertMsgId('""\n"This literal should also be included wrapped or not wrapped depending on the "\n"use of the --no-wrap option."', po_contents, use_quotes=False)
392
class LocationCommentsTests(ExtractorTests):
394
def test_no_location_enabled(self):
395
"""Behavior is correct if --no-location switch is specified. See #16903."""
396
os.chdir(self.test_dir)
397
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=True)
398
self.assertTrue(os.path.exists(self.PO_FILE))
399
with open(self.PO_FILE, 'r') as fp:
400
po_contents = force_text(fp.read())
401
needle = os.sep.join(['#: templates', 'test.html:55'])
402
self.assertFalse(needle in po_contents, '"%s" shouldn\'t be in final .po file.' % needle)
404
def test_no_location_disabled(self):
405
"""Behavior is correct if --no-location switch isn't specified."""
406
os.chdir(self.test_dir)
407
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=False)
408
self.assertTrue(os.path.exists(self.PO_FILE))
409
with open(self.PO_FILE, 'r') as fp:
410
# Standard comment with source file relative path should be present -- #16903
411
po_contents = force_text(fp.read())
413
# #: .\path\to\file.html:123
414
cwd_prefix = '%s%s' % (os.curdir, os.sep)
416
# #: path/to/file.html:123
418
needle = os.sep.join(['#: %stemplates' % cwd_prefix, 'test.html:55'])
419
self.assertTrue(needle in po_contents, '"%s" not found in final .po file.' % needle)
421
# #21208 -- Leaky paths in comments on Windows e.g. #: path\to\file.html.py:123
423
bad_string = 'templates%stest.html%s' % (os.sep, bad_suffix) #
424
self.assertFalse(bad_string in po_contents, '"%s" shouldn\'t be in final .po file.' % bad_string)
427
class KeepPotFileExtractorTests(ExtractorTests):
429
POT_FILE='locale/django.pot'
432
super(KeepPotFileExtractorTests, self).setUp()
435
super(KeepPotFileExtractorTests, self).tearDown()
436
os.chdir(self.test_dir)
438
os.unlink(self.POT_FILE)
443
def test_keep_pot_disabled_by_default(self):
444
os.chdir(self.test_dir)
445
management.call_command('makemessages', locale=LOCALE, verbosity=0)
446
self.assertFalse(os.path.exists(self.POT_FILE))
448
def test_keep_pot_explicitly_disabled(self):
449
os.chdir(self.test_dir)
450
management.call_command('makemessages', locale=LOCALE, verbosity=0,
452
self.assertFalse(os.path.exists(self.POT_FILE))
454
def test_keep_pot_enabled(self):
455
os.chdir(self.test_dir)
456
management.call_command('makemessages', locale=LOCALE, verbosity=0,
458
self.assertTrue(os.path.exists(self.POT_FILE))
461
class MultipleLocaleExtractionTests(ExtractorTests):
462
PO_FILE_PT = 'locale/pt/LC_MESSAGES/django.po'
463
PO_FILE_DE = 'locale/de/LC_MESSAGES/django.po'
464
LOCALES = ['pt', 'de', 'ch']
467
os.chdir(self.test_dir)
468
for locale in self.LOCALES:
470
self._rmrf('locale/%s' % locale)
475
def test_multiple_locales(self):
476
os.chdir(self.test_dir)
477
management.call_command('makemessages', locale=['pt','de'], verbosity=0)
478
self.assertTrue(os.path.exists(self.PO_FILE_PT))
479
self.assertTrue(os.path.exists(self.PO_FILE_DE))
481
def test_comma_separated_locales(self):
482
os.chdir(self.test_dir)
483
management.call_command('makemessages', locale='pt,de,ch', verbosity=0)
484
self.assertTrue(os.path.exists(self.PO_FILE_PT))
485
self.assertTrue(os.path.exists(self.PO_FILE_DE))