Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Patch by Claude for #16084.

  • Loading branch information...
commit 2babab0bb351ff7a13fd23795f5e926a9bf95d22 1 parent b9c8bbf
Ramiro Morales authored January 25, 2013
139  django/core/management/commands/makemessages.py
@@ -19,25 +19,28 @@
19 19
 
20 20
 @total_ordering
21 21
 class TranslatableFile(object):
22  
-    def __init__(self, dirpath, file_name):
  22
+    def __init__(self, dirpath, file_name, locale_dir):
23 23
         self.file = file_name
24 24
         self.dirpath = dirpath
  25
+        self.locale_dir = locale_dir
25 26
 
26 27
     def __repr__(self):
27 28
         return "<TranslatableFile: %s>" % os.sep.join([self.dirpath, self.file])
28 29
 
29 30
     def __eq__(self, other):
30  
-        return self.dirpath == other.dirpath and self.file == other.file
  31
+        return self.path == other.path
31 32
 
32 33
     def __lt__(self, other):
33  
-        if self.dirpath == other.dirpath:
34  
-            return self.file < other.file
35  
-        return self.dirpath < other.dirpath
  34
+        return self.path < other.path
36 35
 
37  
-    def process(self, command, potfile, domain, keep_pot=False):
  36
+    @property
  37
+    def path(self):
  38
+        return os.path.join(self.dirpath, self.file)
  39
+
  40
+    def process(self, command, domain):
38 41
         """
39  
-        Extract translatable literals from self.file for :param domain:
40  
-        creating or updating the :param potfile: POT file.
  42
+        Extract translatable literals from self.file for :param domain:,
  43
+        creating or updating the POT file.
41 44
 
42 45
         Uses the xgettext GNU gettext utility.
43 46
         """
@@ -91,8 +94,6 @@ def process(self, command, potfile, domain, keep_pot=False):
91 94
             if status != STATUS_OK:
92 95
                 if is_templatized:
93 96
                     os.unlink(work_file)
94  
-                if not keep_pot and os.path.exists(potfile):
95  
-                    os.unlink(potfile)
96 97
                 raise CommandError(
97 98
                     "errors happened while running xgettext on %s\n%s" %
98 99
                     (self.file, errors))
@@ -100,11 +101,14 @@ def process(self, command, potfile, domain, keep_pot=False):
100 101
                 # Print warnings
101 102
                 command.stdout.write(errors)
102 103
         if msgs:
  104
+            # Write/append messages to pot file
  105
+            potfile = os.path.join(self.locale_dir, '%s.pot' % str(domain))
103 106
             if is_templatized:
104 107
                 old = '#: ' + work_file[2:]
105 108
                 new = '#: ' + orig_file[2:]
106 109
                 msgs = msgs.replace(old, new)
107 110
             write_pot_file(potfile, msgs)
  111
+
108 112
         if is_templatized:
109 113
             os.unlink(work_file)
110 114
 
@@ -232,21 +236,21 @@ def handle_noargs(self, *args, **options):
232 236
             settings.configure(USE_I18N = True)
233 237
 
234 238
         self.invoked_for_django = False
  239
+        self.locale_paths = []
  240
+        self.default_locale_path = None
235 241
         if os.path.isdir(os.path.join('conf', 'locale')):
236  
-            localedir = os.path.abspath(os.path.join('conf', 'locale'))
  242
+            self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
  243
+            self.default_locale_path = self.locale_paths[0]
237 244
             self.invoked_for_django = True
238 245
             # Ignoring all contrib apps
239 246
             self.ignore_patterns += ['contrib/*']
240  
-        elif os.path.isdir('locale'):
241  
-            localedir = os.path.abspath('locale')
242 247
         else:
243  
-            raise CommandError("This script should be run from the Django Git "
244  
-                    "tree or your project or app tree. If you did indeed run it "
245  
-                    "from the Git checkout or your project or application, "
246  
-                    "maybe you are just missing the conf/locale (in the django "
247  
-                    "tree) or locale (for project and application) directory? It "
248  
-                    "is not created automatically, you have to create it by hand "
249  
-                    "if you want to enable i18n for your project or application.")
  248
+            self.locale_paths.extend(list(settings.LOCALE_PATHS))
  249
+            # Allow to run makemessages inside an app dir
  250
+            if os.path.isdir('locale'):
  251
+                self.locale_paths.append(os.path.abspath('locale'))
  252
+            if self.locale_paths:
  253
+                self.default_locale_path = self.locale_paths[0]
250 254
 
251 255
         # We require gettext version 0.15 or newer.
252 256
         output, errors, status = _popen('xgettext --version')
@@ -261,24 +265,25 @@ def handle_noargs(self, *args, **options):
261 265
                         "gettext 0.15 or newer. You are using version %s, please "
262 266
                         "upgrade your gettext toolset." % match.group())
263 267
 
264  
-        potfile = self.build_pot_file(localedir)
  268
+        try:
  269
+            potfiles = self.build_potfiles()
265 270
 
266  
-        # Build po files for each selected locale
267  
-        locales = []
268  
-        if locale is not None:
269  
-            locales += locale.split(',') if not isinstance(locale, list) else locale
270  
-        elif process_all:
271  
-            locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir))
272  
-            locales = [os.path.basename(l) for l in locale_dirs]
  271
+            # Build po files for each selected locale
  272
+            locales = []
  273
+            if locale is not None:
  274
+                locales = locale.split(',') if not isinstance(locale, list) else locale
  275
+            elif process_all:
  276
+                locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))
  277
+                locales = [os.path.basename(l) for l in locale_dirs]
273 278
 
274  
-        try:
275 279
             for locale in locales:
276 280
                 if self.verbosity > 0:
277 281
                     self.stdout.write("processing locale %s\n" % locale)
278  
-                self.write_po_file(potfile, locale)
  282
+                for potfile in potfiles:
  283
+                    self.write_po_file(potfile, locale)
279 284
         finally:
280  
-            if not self.keep_pot and os.path.exists(potfile):
281  
-                os.unlink(potfile)
  285
+            if not self.keep_pot:
  286
+                self.remove_potfiles()
282 287
 
283 288
     def build_pot_file(self, localedir):
284 289
         file_list = self.find_files(".")
@@ -292,9 +297,41 @@ def build_pot_file(self, localedir):
292 297
             f.process(self, potfile, self.domain, self.keep_pot)
293 298
         return potfile
294 299
 
  300
+    def build_potfiles(self):
  301
+        """Build pot files and apply msguniq to them"""
  302
+        file_list = self.find_files(".")
  303
+        self.remove_potfiles()
  304
+        for f in file_list:
  305
+            f.process(self, self.domain)
  306
+
  307
+        potfiles = []
  308
+        for path in self.locale_paths:
  309
+            potfile = os.path.join(path, '%s.pot' % str(self.domain))
  310
+            if not os.path.exists(potfile):
  311
+                continue
  312
+            msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' %
  313
+                                    (self.wrap, self.location, potfile))
  314
+            if errors:
  315
+                if status != STATUS_OK:
  316
+                    raise CommandError(
  317
+                        "errors happened while running msguniq\n%s" % errors)
  318
+                elif self.verbosity > 0:
  319
+                    self.stdout.write(errors)
  320
+            with open(potfile, 'w') as fp:
  321
+                fp.write(msgs)
  322
+            potfiles.append(potfile)
  323
+        return potfiles
  324
+
  325
+    def remove_potfiles(self):
  326
+        for path in self.locale_paths:
  327
+            pot_path = os.path.join(path, '%s.pot' % str(self.domain))
  328
+            if os.path.exists(pot_path):
  329
+                os.unlink(pot_path)
  330
+
295 331
     def find_files(self, root):
296 332
         """
297  
-        Helper method to get all files in the given root.
  333
+        Helper function to get all files in the given root. Also check that there
  334
+        is a matching locale dir for each file.
298 335
         """
299 336
 
300 337
         def is_ignored(path, ignore_patterns):
@@ -315,12 +352,26 @@ def is_ignored(path, ignore_patterns):
315 352
                     dirnames.remove(dirname)
316 353
                     if self.verbosity > 1:
317 354
                         self.stdout.write('ignoring directory %s\n' % dirname)
  355
+                elif dirname == 'locale':
  356
+                    dirnames.remove(dirname)
  357
+                    self.locale_paths.insert(0, os.path.join(os.path.abspath(dirpath), dirname))
318 358
             for filename in filenames:
319  
-                if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), self.ignore_patterns):
  359
+                file_path = os.path.normpath(os.path.join(dirpath, filename))
  360
+                if is_ignored(file_path, self.ignore_patterns):
320 361
                     if self.verbosity > 1:
321 362
                         self.stdout.write('ignoring file %s in %s\n' % (filename, dirpath))
322 363
                 else:
323  
-                    all_files.append(TranslatableFile(dirpath, filename))
  364
+                    locale_dir = None
  365
+                    for path in self.locale_paths:
  366
+                        if os.path.abspath(dirpath).startswith(os.path.dirname(path)):
  367
+                            locale_dir = path
  368
+                            break
  369
+                    if not locale_dir:
  370
+                        locale_dir = self.default_locale_path
  371
+                    if not locale_dir:
  372
+                        raise CommandError(
  373
+                            "Unable to find a locale path to store translations for file %s" % file_path)
  374
+                    all_files.append(TranslatableFile(dirpath, filename, locale_dir))
324 375
         return sorted(all_files)
325 376
 
326 377
     def write_po_file(self, potfile, locale):
@@ -328,16 +379,8 @@ def write_po_file(self, potfile, locale):
328 379
         Creates or updates the PO file for self.domain and :param locale:.
329 380
         Uses contents of the existing :param potfile:.
330 381
 
331  
-        Uses mguniq, msgmerge, and msgattrib GNU gettext utilities.
  382
+        Uses msgmerge, and msgattrib GNU gettext utilities.
332 383
         """
333  
-        msgs, errors, status = _popen('msguniq %s %s --to-code=utf-8 "%s"' %
334  
-                                        (self.wrap, self.location, potfile))
335  
-        if errors:
336  
-            if status != STATUS_OK:
337  
-                raise CommandError(
338  
-                    "errors happened while running msguniq\n%s" % errors)
339  
-            elif self.verbosity > 0:
340  
-                self.stdout.write(errors)
341 384
 
342 385
         basedir = os.path.join(os.path.dirname(potfile), locale, 'LC_MESSAGES')
343 386
         if not os.path.isdir(basedir):
@@ -345,8 +388,6 @@ def write_po_file(self, potfile, locale):
345 388
         pofile = os.path.join(basedir, '%s.po' % str(self.domain))
346 389
 
347 390
         if os.path.exists(pofile):
348  
-            with open(potfile, 'w') as fp:
349  
-                fp.write(msgs)
350 391
             msgs, errors, status = _popen('msgmerge %s %s -q "%s" "%s"' %
351 392
                                             (self.wrap, self.location, pofile, potfile))
352 393
             if errors:
@@ -355,8 +396,10 @@ def write_po_file(self, potfile, locale):
355 396
                         "errors happened while running msgmerge\n%s" % errors)
356 397
                 elif self.verbosity > 0:
357 398
                     self.stdout.write(errors)
358  
-        elif not self.invoked_for_django:
359  
-            msgs = self.copy_plural_forms(msgs, locale)
  399
+        else:
  400
+            msgs = open(potfile, 'r').read()
  401
+            if not self.invoked_for_django:
  402
+                msgs = self.copy_plural_forms(msgs, locale)
360 403
         msgs = msgs.replace(
361 404
             "#. #-#-#-#-#  %s.pot (PACKAGE VERSION)  #-#-#-#-#\n" % self.domain, "")
362 405
         with open(pofile, 'w') as fp:
3  docs/man/django-admin.1
@@ -193,7 +193,8 @@ Ignore files or directories matching this glob-style pattern. Use multiple
193 193
 times to ignore more (makemessages command).
194 194
 .TP
195 195
 .I \-\-no\-default\-ignore
196  
-Don't ignore the common private glob-style patterns 'CVS', '.*' and '*~' (makemessages command).
  196
+Don't ignore the common private glob-style patterns 'CVS', '.*', '*~' and '*.pyc'
  197
+(makemessages command).
197 198
 .TP
198 199
 .I \-\-no\-wrap
199 200
 Don't break long message lines into several lines (makemessages command).
4  docs/ref/django-admin.txt
@@ -472,7 +472,7 @@ Example usage::
472 472
 Use the ``--ignore`` or ``-i`` option to ignore files or directories matching
473 473
 the given :mod:`glob`-style pattern. Use multiple times to ignore more.
474 474
 
475  
-These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``
  475
+These patterns are used by default: ``'CVS'``, ``'.*'``, ``'*~'``, ``'*.pyc'``
476 476
 
477 477
 Example usage::
478 478
 
@@ -499,7 +499,7 @@ for technically skilled translators to understand each message's context.
499 499
 .. versionadded:: 1.6
500 500
 
501 501
 Use the ``--keep-pot`` option to prevent django from deleting the temporary
502  
-.pot file it generates before creating the .po file. This is useful for
  502
+.pot files it generates before creating the .po file. This is useful for
503 503
 debugging errors which may prevent the final language files from being created.
504 504
 
505 505
 runfcgi [options]
17  docs/topics/i18n/translation.txt
@@ -1543,24 +1543,9 @@ All message file repositories are structured the same way. They are:
1543 1543
 * ``$PYTHONPATH/django/conf/locale/<language>/LC_MESSAGES/django.(po|mo)``
1544 1544
 
1545 1545
 To create message files, you use the :djadmin:`django-admin.py makemessages <makemessages>`
1546  
-tool. You only need to be in the same directory where the ``locale/`` directory
1547  
-is located. And you use :djadmin:`django-admin.py compilemessages <compilemessages>`
  1546
+tool. And you use :djadmin:`django-admin.py compilemessages <compilemessages>`
1548 1547
 to produce the binary ``.mo`` files that are used by ``gettext``.
1549 1548
 
1550 1549
 You can also run :djadmin:`django-admin.py compilemessages
1551 1550
 --settings=path.to.settings <compilemessages>` to make the compiler process all
1552 1551
 the directories in your :setting:`LOCALE_PATHS` setting.
1553  
-
1554  
-Finally, you should give some thought to the structure of your translation
1555  
-files. If your applications need to be delivered to other users and will be used
1556  
-in other projects, you might want to use app-specific translations. But using
1557  
-app-specific translations and project-specific translations could produce weird
1558  
-problems with :djadmin:`makemessages`: it will traverse all directories below
1559  
-the current path and so might put message IDs into a unified, common message
1560  
-file for the current project that are already in application message files.
1561  
-
1562  
-The easiest way out is to store applications that are not part of the project
1563  
-(and so carry their own translations) outside the project tree. That way,
1564  
-:djadmin:`django-admin.py makemessages <makemessages>`, when ran on a project
1565  
-level will only extract strings that are connected to your explicit project and
1566  
-not strings that are distributed independently.
44  tests/regressiontests/i18n/commands/extraction.py
@@ -5,10 +5,13 @@
5 5
 import re
6 6
 import shutil
7 7
 
  8
+from django.conf import settings
8 9
 from django.core import management
9 10
 from django.test import SimpleTestCase
  11
+from django.test.utils import override_settings
10 12
 from django.utils.encoding import force_text
11 13
 from django.utils._os import upath
  14
+from django.utils import six
12 15
 from django.utils.six import StringIO
13 16
 
14 17
 
@@ -352,3 +355,44 @@ def test_comma_separated_locales(self):
352 355
         management.call_command('makemessages', locale='pt,de,ch', verbosity=0)
353 356
         self.assertTrue(os.path.exists(self.PO_FILE_PT))
354 357
         self.assertTrue(os.path.exists(self.PO_FILE_DE))
  358
+
  359
+
  360
+class CustomLayoutExtractionTests(ExtractorTests):
  361
+    def setUp(self):
  362
+        self._cwd = os.getcwd()
  363
+        self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir')
  364
+
  365
+    def test_no_locale_raises(self):
  366
+        os.chdir(self.test_dir)
  367
+        with six.assertRaisesRegex(self, management.CommandError,
  368
+                "Unable to find a locale path to store translations for file"):
  369
+            management.call_command('makemessages', locale=LOCALE, verbosity=0)
  370
+
  371
+    @override_settings(
  372
+        LOCALE_PATHS=(os.path.join(os.path.dirname(upath(__file__)), 'project_dir/project_locale'),)
  373
+    )
  374
+    def test_project_locale_paths(self):
  375
+        """
  376
+        Test that:
  377
+          * translations for app containing locale folder are stored in that folder
  378
+          * translations outside of that app are in LOCALE_PATHS[0]
  379
+        """
  380
+        os.chdir(self.test_dir)
  381
+        self.addCleanup(shutil.rmtree, os.path.join(settings.LOCALE_PATHS[0], LOCALE))
  382
+        self.addCleanup(shutil.rmtree, os.path.join(self.test_dir, 'app_with_locale/locale', LOCALE))
  383
+
  384
+        management.call_command('makemessages', locale=LOCALE, verbosity=0)
  385
+        project_de_locale = os.path.join(
  386
+            self.test_dir, 'project_locale/de/LC_MESSAGES/django.po',)
  387
+        app_de_locale = os.path.join(
  388
+            self.test_dir, 'app_with_locale/locale/de/LC_MESSAGES/django.po',)
  389
+        self.assertTrue(os.path.exists(project_de_locale))
  390
+        self.assertTrue(os.path.exists(app_de_locale))
  391
+
  392
+        with open(project_de_locale, 'r') as fp:
  393
+            po_contents = force_text(fp.read())
  394
+            self.assertMsgId('This app has no locale directory', po_contents)
  395
+            self.assertMsgId('This is a project-level string', po_contents)
  396
+        with open(app_de_locale, 'r') as fp:
  397
+            po_contents = force_text(fp.read())
  398
+            self.assertMsgId('This app has a locale directory', po_contents)
3  tests/regressiontests/i18n/commands/project_dir/__init__.py
... ...
@@ -0,0 +1,3 @@
  1
+from django.utils.translation import ugettext as _
  2
+
  3
+string = _("This is a project-level string")
4  tests/regressiontests/i18n/commands/project_dir/app_no_locale/models.py
... ...
@@ -0,0 +1,4 @@
  1
+from django.utils.translation import ugettext as _
  2
+
  3
+string = _("This app has no locale directory")
  4
+
4  tests/regressiontests/i18n/commands/project_dir/app_with_locale/models.py
... ...
@@ -0,0 +1,4 @@
  1
+from django.utils.translation import ugettext as _
  2
+
  3
+string = _("This app has a locale directory")
  4
+
2  tests/regressiontests/i18n/tests.py
@@ -33,7 +33,7 @@
33 33
         JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests,
34 34
         CopyPluralFormsExtractorTests, NoWrapExtractorTests,
35 35
         NoLocationExtractorTests, KeepPotFileExtractorTests,
36  
-        MultipleLocaleExtractionTests)
  36
+        MultipleLocaleExtractionTests, CustomLayoutExtractionTests)
37 37
 if can_run_compilation_tests:
38 38
     from .commands.compilation import (PoFileTests, PoFileContentsTests,
39 39
         PercentRenderingTests, MultipleLocaleCompilationTests)

0 notes on commit 2babab0

Please sign in to comment.
Something went wrong with that request. Please try again.