Browse Source

Fixed #22328 -- Added --exclude option to compilemessages and makemessages.

Ana Krivokapic 11 years ago
parent
commit
0707b824fe

+ 1 - 0
AUTHORS

@@ -367,6 +367,7 @@ answer newbie questions, and generally made Django that much better:
     Martin Kosír <martin@martinkosir.net>
     Martin Kosír <martin@martinkosir.net>
     Arthur Koziel <http://arthurkoziel.com>
     Arthur Koziel <http://arthurkoziel.com>
     Meir Kriheli <http://mksoft.co.il/>
     Meir Kriheli <http://mksoft.co.il/>
+    Ana Krivokapic <https://github.com/infraredgirl>
     Bruce Kroeze <http://coderseye.com/>
     Bruce Kroeze <http://coderseye.com/>
     krzysiek.pawlik@silvermedia.pl
     krzysiek.pawlik@silvermedia.pl
     konrad@gwu.edu
     konrad@gwu.edu

+ 21 - 6
django/core/management/commands/compilemessages.py

@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import codecs
 import codecs
+import glob
 import os
 import os
 from optparse import make_option
 from optparse import make_option
 
 
@@ -30,8 +31,11 @@ def is_writable(path):
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
     option_list = BaseCommand.option_list + (
-        make_option('--locale', '-l', dest='locale', action='append',
-                    help='locale(s) to process (e.g. de_AT). Default is to process all. Can be used multiple times.'),
+        make_option('--locale', '-l', dest='locale', action='append', default=[],
+                    help='Locale(s) to process (e.g. de_AT). Default is to process all. Can be '
+                         'used multiple times.'),
+        make_option('--exclude', '-e', dest='exclude', action='append', default=[],
+                    help='Locales to exclude. Default is none. Can be used multiple times.'),
     )
     )
     help = 'Compiles .po files to .mo files for use with builtin gettext support.'
     help = 'Compiles .po files to .mo files for use with builtin gettext support.'
 
 
@@ -43,6 +47,7 @@ class Command(BaseCommand):
 
 
     def handle(self, **options):
     def handle(self, **options):
         locale = options.get('locale')
         locale = options.get('locale')
+        exclude = options.get('exclude')
         self.verbosity = int(options.get('verbosity'))
         self.verbosity = int(options.get('verbosity'))
 
 
         if find_command(self.program) is None:
         if find_command(self.program) is None:
@@ -62,9 +67,19 @@ class Command(BaseCommand):
                                "checkout or your project or app tree, or with "
                                "checkout or your project or app tree, or with "
                                "the settings module specified.")
                                "the settings module specified.")
 
 
+        # Build locale list
+        all_locales = []
         for basedir in basedirs:
         for basedir in basedirs:
-            if locale:
-                dirs = [os.path.join(basedir, l, 'LC_MESSAGES') for l in locale]
+            locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % basedir))
+            all_locales.extend(map(os.path.basename, locale_dirs))
+
+        # Account for excluded locales
+        locales = locale or all_locales
+        locales = set(locales) - set(exclude)
+
+        for basedir in basedirs:
+            if locales:
+                dirs = [os.path.join(basedir, l, 'LC_MESSAGES') for l in locales]
             else:
             else:
                 dirs = [basedir]
                 dirs = [basedir]
             locations = []
             locations = []
@@ -90,8 +105,8 @@ class Command(BaseCommand):
 
 
             # Check writability on first location
             # Check writability on first location
             if i == 0 and not is_writable(npath(base_path + '.mo')):
             if i == 0 and not is_writable(npath(base_path + '.mo')):
-                self.stderr.write("The po files under %s are in a seemingly not "
-                                  "writable location. mo files will not be updated/created." % dirpath)
+                self.stderr.write("The po files under %s are in a seemingly not writable location. "
+                                  "mo files will not be updated/created." % dirpath)
                 return
                 return
 
 
             args = [self.program] + self.program_options + ['-o',
             args = [self.program] + self.program_options + ['-o',

+ 16 - 9
django/core/management/commands/makemessages.py

@@ -160,9 +160,11 @@ def write_pot_file(potfile, msgs):
 
 
 class Command(NoArgsCommand):
 class Command(NoArgsCommand):
     option_list = NoArgsCommand.option_list + (
     option_list = NoArgsCommand.option_list + (
-        make_option('--locale', '-l', default=None, dest='locale', action='append',
+        make_option('--locale', '-l', default=[], dest='locale', action='append',
             help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
             help='Creates or updates the message files for the given locale(s) (e.g. pt_BR). '
                  'Can be used multiple times.'),
                  'Can be used multiple times.'),
+        make_option('--exclude', '-e', default=[], dest='exclude', action='append',
+                    help='Locales to exclude. Default is none. Can be used multiple times.'),
         make_option('--domain', '-d', default='django', dest='domain',
         make_option('--domain', '-d', default='django', dest='domain',
             help='The domain of the message files (default: "django").'),
             help='The domain of the message files (default: "django").'),
         make_option('--all', '-a', action='store_true', dest='all',
         make_option('--all', '-a', action='store_true', dest='all',
@@ -189,7 +191,7 @@ class Command(NoArgsCommand):
 "pulls out all strings marked for translation. It creates (or updates) a message "
 "pulls out all strings marked for translation. It creates (or updates) a message "
 "file in the conf/locale (in the django tree) or locale (for projects and "
 "file in the conf/locale (in the django tree) or locale (for projects and "
 "applications) directory.\n\nYou must run this command with one of either the "
 "applications) directory.\n\nYou must run this command with one of either the "
-"--locale or --all options.")
+"--locale, --exclude or --all options.")
 
 
     requires_system_checks = False
     requires_system_checks = False
     leave_locale_alone = True
     leave_locale_alone = True
@@ -201,6 +203,7 @@ class Command(NoArgsCommand):
 
 
     def handle_noargs(self, *args, **options):
     def handle_noargs(self, *args, **options):
         locale = options.get('locale')
         locale = options.get('locale')
+        exclude = options.get('exclude')
         self.domain = options.get('domain')
         self.domain = options.get('domain')
         self.verbosity = int(options.get('verbosity'))
         self.verbosity = int(options.get('verbosity'))
         process_all = options.get('all')
         process_all = options.get('all')
@@ -235,7 +238,7 @@ class Command(NoArgsCommand):
             exts = extensions if extensions else ['html', 'txt']
             exts = extensions if extensions else ['html', 'txt']
         self.extensions = handle_extensions(exts)
         self.extensions = handle_extensions(exts)
 
 
-        if (locale is None and not process_all) or self.domain is None:
+        if (locale is None and not exclude and not process_all) or self.domain is None:
             raise CommandError("Type '%s help %s' for usage information." % (
             raise CommandError("Type '%s help %s' for usage information." % (
                 os.path.basename(sys.argv[0]), sys.argv[1]))
                 os.path.basename(sys.argv[0]), sys.argv[1]))
 
 
@@ -270,12 +273,16 @@ class Command(NoArgsCommand):
                     os.makedirs(self.default_locale_path)
                     os.makedirs(self.default_locale_path)
 
 
         # Build locale list
         # Build locale list
-        locales = []
-        if locale is not None:
-            locales = locale
-        elif process_all:
-            locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))
-            locales = [os.path.basename(l) for l in locale_dirs]
+        locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))
+        all_locales = map(os.path.basename, locale_dirs)
+
+        # Account for excluded locales
+        if process_all:
+            locales = all_locales
+        else:
+            locales = locale or all_locales
+            locales = set(locales) - set(exclude)
+
         if locales:
         if locales:
             check_programs('msguniq', 'msgmerge', 'msgattrib')
             check_programs('msguniq', 'msgmerge', 'msgattrib')
 
 

+ 5 - 2
docs/man/django-admin.1

@@ -21,7 +21,7 @@ script found at the top level of each Django project directory.
 .BI cleanup
 .BI cleanup
 Cleans out old data from the database (only expired sessions at the moment).
 Cleans out old data from the database (only expired sessions at the moment).
 .TP
 .TP
-.BI "compilemessages [" "\-\-locale=LOCALE" "]"
+.BI "compilemessages [" "\-\-locale=LOCALE" "] [" "\-\-exclude=LOCALE" "]"
 Compiles .po files to .mo files for use with builtin gettext support.
 Compiles .po files to .mo files for use with builtin gettext support.
 .TP
 .TP
 .BI "createcachetable [" "tablename" "]"
 .BI "createcachetable [" "tablename" "]"
@@ -59,7 +59,7 @@ Executes
 .B sqlall
 .B sqlall
 for the given app(s) in the current database.
 for the given app(s) in the current database.
 .TP
 .TP
-.BI "makemessages [" "\-\-locale=LOCALE" "] [" "\-\-domain=DOMAIN" "] [" "\-\-extension=EXTENSION" "] [" "\-\-all" "] [" "\-\-symlinks" "] [" "\-\-ignore=PATTERN" "] [" "\-\-no\-default\-ignore" "] [" "\-\-no\-wrap" "] [" "\-\-no\-location" "]"
+.BI "makemessages [" "\-\-locale=LOCALE" "] [" "\-\-exclude=LOCALE" "] [" "\-\-domain=DOMAIN" "] [" "\-\-extension=EXTENSION" "] [" "\-\-all" "] [" "\-\-symlinks" "] [" "\-\-ignore=PATTERN" "] [" "\-\-no\-default\-ignore" "] [" "\-\-no\-wrap" "] [" "\-\-no\-location" "]"
 Runs over the entire source tree of the current directory and pulls out all
 Runs over the entire source tree of the current directory and pulls out all
 strings marked for translation. It creates (or updates) a message file in the
 strings marked for translation. It creates (or updates) a message file in the
 conf/locale (in the django tree) or locale (for project and application) directory.
 conf/locale (in the django tree) or locale (for project and application) directory.
@@ -176,6 +176,9 @@ output a full stack trace whenever an exception is raised.
 .I \-l, \-\-locale=LOCALE
 .I \-l, \-\-locale=LOCALE
 The locale to process when using makemessages or compilemessages.
 The locale to process when using makemessages or compilemessages.
 .TP
 .TP
+.I \-e, \-\-exclude=LOCALE
+The locale to exclude from processing when using makemessages or compilemessages.
+.TP
 .I \-d, \-\-domain=DOMAIN
 .I \-d, \-\-domain=DOMAIN
 The domain of the message files (default: "django") when using makemessages.
 The domain of the message files (default: "django") when using makemessages.
 .TP
 .TP

+ 21 - 0
docs/ref/django-admin.txt

@@ -141,12 +141,22 @@ the builtin gettext support. See :doc:`/topics/i18n/index`.
 Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to
 Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to
 specify the locale(s) to process. If not provided, all locales are processed.
 specify the locale(s) to process. If not provided, all locales are processed.
 
 
+.. versionadded:: 1.8
+
+Use the :djadminopt:`--exclude` option (or its shorter version ``-e``) to
+specify the locale(s) to exclude from processing. If not provided, no locales
+are excluded.
+
 Example usage::
 Example usage::
 
 
     django-admin.py compilemessages --locale=pt_BR
     django-admin.py compilemessages --locale=pt_BR
     django-admin.py compilemessages --locale=pt_BR --locale=fr
     django-admin.py compilemessages --locale=pt_BR --locale=fr
     django-admin.py compilemessages -l pt_BR
     django-admin.py compilemessages -l pt_BR
     django-admin.py compilemessages -l pt_BR -l fr
     django-admin.py compilemessages -l pt_BR -l fr
+    django-admin.py compilemessages --exclude=pt_BR
+    django-admin.py compilemessages --exclude=pt_BR --exclude=fr
+    django-admin.py compilemessages -e pt_BR
+    django-admin.py compilemessages -e pt_BR -e fr
 
 
 createcachetable
 createcachetable
 ----------------
 ----------------
@@ -551,12 +561,23 @@ Separate multiple extensions with commas or use -e or --extension multiple times
 Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to
 Use the :djadminopt:`--locale` option (or its shorter version ``-l``) to
 specify the locale(s) to process.
 specify the locale(s) to process.
 
 
+.. versionadded:: 1.8
+
+Use the :djadminopt:`--exclude` option (or its shorter version ``-e``) to
+specify the locale(s) to exclude from processing. If not provided, no locales
+are excluded.
+
 Example usage::
 Example usage::
 
 
     django-admin.py makemessages --locale=pt_BR
     django-admin.py makemessages --locale=pt_BR
     django-admin.py makemessages --locale=pt_BR --locale=fr
     django-admin.py makemessages --locale=pt_BR --locale=fr
     django-admin.py makemessages -l pt_BR
     django-admin.py makemessages -l pt_BR
     django-admin.py makemessages -l pt_BR -l fr
     django-admin.py makemessages -l pt_BR -l fr
+    django-admin.py makemessages --exclude=pt_BR
+    django-admin.py makemessages --exclude=pt_BR --exclude=fr
+    django-admin.py makemessages -e pt_BR
+    django-admin.py makemessages -e pt_BR -e fr
+
 
 
 .. versionchanged:: 1.7
 .. versionchanged:: 1.7
 
 

+ 4 - 0
docs/releases/1.8.txt

@@ -128,6 +128,10 @@ Management Commands
 * :djadmin:`dumpdata` now has the option :djadminopt:`--output` which allows
 * :djadmin:`dumpdata` now has the option :djadminopt:`--output` which allows
   specifying the file to which the serialized data is written.
   specifying the file to which the serialized data is written.
 
 
+* :djadmin:`makemessages` and :djadmin:`compilemessages` now have the option
+  :djadminopt:`--exclude` which allows exclusion of specific locales from
+  processing.
+
 Models
 Models
 ^^^^^^
 ^^^^^^
 
 

+ 12 - 0
tests/i18n/exclude/__init__.py

@@ -0,0 +1,12 @@
+# This package is used to test the --exclude option of
+# the makemessages and compilemessages management commands.
+# The locale directory for this app is generated automatically
+# by the test cases.
+
+from django.utils.translation import ugettext as _
+
+# Translators: This comment should be extracted
+dummy1 = _("This is a translatable string.")
+
+# This comment should not be extracted
+dummy2 = _("This is another translatable string.")

+ 27 - 0
tests/i18n/exclude/canned_locale/en/LC_MESSAGES/django.po

@@ -0,0 +1,27 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-04-25 15:39-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Translators: This comment should be extracted
+#: __init__.py:8
+msgid "This is a translatable string."
+msgstr ""
+
+#: __init__.py:11
+msgid "This is another translatable string."
+msgstr ""

+ 28 - 0
tests/i18n/exclude/canned_locale/fr/LC_MESSAGES/django.po

@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-04-25 15:39-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. Translators: This comment should be extracted
+#: __init__.py:8
+msgid "This is a translatable string."
+msgstr ""
+
+#: __init__.py:11
+msgid "This is another translatable string."
+msgstr ""

+ 28 - 0
tests/i18n/exclude/canned_locale/it/LC_MESSAGES/django.po

@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-04-25 15:39-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Translators: This comment should be extracted
+#: __init__.py:8
+msgid "This is a translatable string."
+msgstr ""
+
+#: __init__.py:11
+msgid "This is another translatable string."
+msgstr ""

+ 69 - 22
tests/i18n/test_compilation.py

@@ -1,4 +1,5 @@
 import os
 import os
+import shutil
 import stat
 import stat
 import unittest
 import unittest
 
 
@@ -10,17 +11,23 @@ from django.utils import translation
 from django.utils._os import upath
 from django.utils._os import upath
 from django.utils.six import StringIO
 from django.utils.six import StringIO
 
 
-test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'commands'))
 has_msgfmt = find_command('msgfmt')
 has_msgfmt = find_command('msgfmt')
 
 
 
 
 @unittest.skipUnless(has_msgfmt, 'msgfmt is mandatory for compilation tests')
 @unittest.skipUnless(has_msgfmt, 'msgfmt is mandatory for compilation tests')
 class MessageCompilationTests(SimpleTestCase):
 class MessageCompilationTests(SimpleTestCase):
 
 
+    test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'commands'))
+
     def setUp(self):
     def setUp(self):
         self._cwd = os.getcwd()
         self._cwd = os.getcwd()
         self.addCleanup(os.chdir, self._cwd)
         self.addCleanup(os.chdir, self._cwd)
-        os.chdir(test_dir)
+        os.chdir(self.test_dir)
+
+    def _rmrf(self, dname):
+        if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
+            return
+        shutil.rmtree(dname)
 
 
     def rmfile(self, filepath):
     def rmfile(self, filepath):
         if os.path.exists(filepath):
         if os.path.exists(filepath):
@@ -60,7 +67,7 @@ class PoFileContentsTests(MessageCompilationTests):
 
 
     def setUp(self):
     def setUp(self):
         super(PoFileContentsTests, self).setUp()
         super(PoFileContentsTests, self).setUp()
-        self.addCleanup(os.unlink, os.path.join(test_dir, self.MO_FILE))
+        self.addCleanup(os.unlink, os.path.join(self.test_dir, self.MO_FILE))
 
 
     def test_percent_symbol_in_po_file(self):
     def test_percent_symbol_in_po_file(self):
         call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
         call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
@@ -76,45 +83,85 @@ class PercentRenderingTests(MessageCompilationTests):
 
 
     def setUp(self):
     def setUp(self):
         super(PercentRenderingTests, self).setUp()
         super(PercentRenderingTests, self).setUp()
-        self.addCleanup(os.unlink, os.path.join(test_dir, self.MO_FILE))
+        self.addCleanup(os.unlink, os.path.join(self.test_dir, self.MO_FILE))
 
 
-    @override_settings(LOCALE_PATHS=(os.path.join(test_dir, 'locale'),))
     def test_percent_symbol_escaping(self):
     def test_percent_symbol_escaping(self):
-        from django.template import Template, Context
-        call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
-        with translation.override(self.LOCALE):
-            t = Template('{% load i18n %}{% trans "Looks like a str fmt spec %% o but shouldn\'t be interpreted as such" %}')
-            rendered = t.render(Context({}))
-            self.assertEqual(rendered, 'IT translation contains %% for the above string')
+        with override_settings(LOCALE_PATHS=(os.path.join(self.test_dir, 'locale'),)):
+            from django.template import Template, Context
+            call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
+            with translation.override(self.LOCALE):
+                t = Template('{% load i18n %}{% trans "Looks like a str fmt spec %% o but shouldn\'t be interpreted as such" %}')
+                rendered = t.render(Context({}))
+                self.assertEqual(rendered, 'IT translation contains %% for the above string')
 
 
-            t = Template('{% load i18n %}{% trans "Completed 50%% of all the tasks" %}')
-            rendered = t.render(Context({}))
-            self.assertEqual(rendered, 'IT translation of Completed 50%% of all the tasks')
+                t = Template('{% load i18n %}{% trans "Completed 50%% of all the tasks" %}')
+                rendered = t.render(Context({}))
+                self.assertEqual(rendered, 'IT translation of Completed 50%% of all the tasks')
 
 
 
 
-@override_settings(LOCALE_PATHS=(os.path.join(test_dir, 'locale'),))
 class MultipleLocaleCompilationTests(MessageCompilationTests):
 class MultipleLocaleCompilationTests(MessageCompilationTests):
+
     MO_FILE_HR = None
     MO_FILE_HR = None
     MO_FILE_FR = None
     MO_FILE_FR = None
 
 
     def setUp(self):
     def setUp(self):
         super(MultipleLocaleCompilationTests, self).setUp()
         super(MultipleLocaleCompilationTests, self).setUp()
-        localedir = os.path.join(test_dir, 'locale')
+        localedir = os.path.join(self.test_dir, 'locale')
         self.MO_FILE_HR = os.path.join(localedir, 'hr/LC_MESSAGES/django.mo')
         self.MO_FILE_HR = os.path.join(localedir, 'hr/LC_MESSAGES/django.mo')
         self.MO_FILE_FR = os.path.join(localedir, 'fr/LC_MESSAGES/django.mo')
         self.MO_FILE_FR = os.path.join(localedir, 'fr/LC_MESSAGES/django.mo')
         self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_HR))
         self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_HR))
         self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_FR))
         self.addCleanup(self.rmfile, os.path.join(localedir, self.MO_FILE_FR))
 
 
     def test_one_locale(self):
     def test_one_locale(self):
-        call_command('compilemessages', locale=['hr'], stdout=StringIO())
+        with override_settings(LOCALE_PATHS=(os.path.join(self.test_dir, 'locale'),)):
+            call_command('compilemessages', locale=['hr'], stdout=StringIO())
 
 
-        self.assertTrue(os.path.exists(self.MO_FILE_HR))
+            self.assertTrue(os.path.exists(self.MO_FILE_HR))
 
 
     def test_multiple_locales(self):
     def test_multiple_locales(self):
-        call_command('compilemessages', locale=['hr', 'fr'], stdout=StringIO())
+        with override_settings(LOCALE_PATHS=(os.path.join(self.test_dir, 'locale'),)):
+            call_command('compilemessages', locale=['hr', 'fr'], stdout=StringIO())
+
+            self.assertTrue(os.path.exists(self.MO_FILE_HR))
+            self.assertTrue(os.path.exists(self.MO_FILE_FR))
+
+
+class ExcludedLocaleCompilationTests(MessageCompilationTests):
+
+    test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'exclude'))
+
+    MO_FILE = 'locale/%s/LC_MESSAGES/django.mo'
+
+    def setUp(self):
+        super(ExcludedLocaleCompilationTests, self).setUp()
+
+        shutil.copytree('canned_locale', 'locale')
+        self.addCleanup(self._rmrf, os.path.join(self.test_dir, 'locale'))
+
+    def test_one_locale_excluded(self):
+        call_command('compilemessages', exclude=['it'], stdout=StringIO())
+        self.assertTrue(os.path.exists(self.MO_FILE % 'en'))
+        self.assertTrue(os.path.exists(self.MO_FILE % 'fr'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'it'))
+
+    def test_multiple_locales_excluded(self):
+        call_command('compilemessages', exclude=['it', 'fr'], stdout=StringIO())
+        self.assertTrue(os.path.exists(self.MO_FILE % 'en'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'fr'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'it'))
+
+    def test_one_locale_excluded_with_locale(self):
+        call_command('compilemessages', locale=['en', 'fr'], exclude=['fr'], stdout=StringIO())
+        self.assertTrue(os.path.exists(self.MO_FILE % 'en'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'fr'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'it'))
 
 
-        self.assertTrue(os.path.exists(self.MO_FILE_HR))
-        self.assertTrue(os.path.exists(self.MO_FILE_FR))
+    def test_multiple_locales_excluded_with_locale(self):
+        call_command('compilemessages', locale=['en', 'fr', 'it'], exclude=['fr', 'it'],
+                     stdout=StringIO())
+        self.assertTrue(os.path.exists(self.MO_FILE % 'en'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'fr'))
+        self.assertFalse(os.path.exists(self.MO_FILE % 'it'))
 
 
 
 
 class CompilationErrorHandling(MessageCompilationTests):
 class CompilationErrorHandling(MessageCompilationTests):
@@ -124,7 +171,7 @@ class CompilationErrorHandling(MessageCompilationTests):
 
 
     def setUp(self):
     def setUp(self):
         super(CompilationErrorHandling, self).setUp()
         super(CompilationErrorHandling, self).setUp()
-        self.addCleanup(self.rmfile, os.path.join(test_dir, self.MO_FILE))
+        self.addCleanup(self.rmfile, os.path.join(self.test_dir, self.MO_FILE))
 
 
     def test_error_reported_by_msgfmt(self):
     def test_error_reported_by_msgfmt(self):
         with self.assertRaises(CommandError):
         with self.assertRaises(CommandError):

+ 67 - 2
tests/i18n/test_extraction.py

@@ -5,6 +5,7 @@ import io
 import os
 import os
 import re
 import re
 import shutil
 import shutil
+import time
 from unittest import SkipTest, skipUnless
 from unittest import SkipTest, skipUnless
 import warnings
 import warnings
 
 
@@ -27,12 +28,12 @@ has_xgettext = find_command('xgettext')
 @skipUnless(has_xgettext, 'xgettext is mandatory for extraction tests')
 @skipUnless(has_xgettext, 'xgettext is mandatory for extraction tests')
 class ExtractorTests(SimpleTestCase):
 class ExtractorTests(SimpleTestCase):
 
 
+    test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'commands'))
+
     PO_FILE = 'locale/%s/LC_MESSAGES/django.po' % LOCALE
     PO_FILE = 'locale/%s/LC_MESSAGES/django.po' % LOCALE
 
 
     def setUp(self):
     def setUp(self):
         self._cwd = os.getcwd()
         self._cwd = os.getcwd()
-        self.test_dir = os.path.abspath(
-            os.path.join(os.path.dirname(upath(__file__)), 'commands'))
 
 
     def _rmrf(self, dname):
     def _rmrf(self, dname):
         if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
         if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
@@ -103,6 +104,20 @@ class ExtractorTests(SimpleTestCase):
         """Check the opposite of assertLocationComment()"""
         """Check the opposite of assertLocationComment()"""
         return self._assertPoLocComment(False, po_filename, line_number, *comment_parts)
         return self._assertPoLocComment(False, po_filename, line_number, *comment_parts)
 
 
+    def assertRecentlyModified(self, path):
+        """
+        Assert that file was recently modified (modification time was less than 10 seconds ago).
+        """
+        delta = time.time() - os.stat(path).st_mtime
+        self.assertLess(delta, 10, "%s was recently modified" % path)
+
+    def assertNotRecentlyModified(self, path):
+        """
+        Assert that file was not recently modified (modification time was more than 10 seconds ago).
+        """
+        delta = time.time() - os.stat(path).st_mtime
+        self.assertGreater(delta, 10, "%s wasn't recently modified" % path)
+
 
 
 class BasicExtractorTests(ExtractorTests):
 class BasicExtractorTests(ExtractorTests):
 
 
@@ -402,6 +417,7 @@ class SymlinkExtractorTests(ExtractorTests):
 
 
 
 
 class CopyPluralFormsExtractorTests(ExtractorTests):
 class CopyPluralFormsExtractorTests(ExtractorTests):
+
     PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'
     PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'
 
 
     def tearDown(self):
     def tearDown(self):
@@ -527,7 +543,56 @@ class MultipleLocaleExtractionTests(ExtractorTests):
         self.assertTrue(os.path.exists(self.PO_FILE_DE))
         self.assertTrue(os.path.exists(self.PO_FILE_DE))
 
 
 
 
+class ExcludedLocaleExtractionTests(ExtractorTests):
+
+    LOCALES = ['en', 'fr', 'it']
+    PO_FILE = 'locale/%s/LC_MESSAGES/django.po'
+
+    test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'exclude'))
+
+    def _set_times_for_all_po_files(self):
+        """
+        Set access and modification times to the Unix epoch time for all the .po files.
+        """
+        for locale in self.LOCALES:
+            os.utime(self.PO_FILE % locale, (0, 0))
+
+    def setUp(self):
+        super(ExcludedLocaleExtractionTests, self).setUp()
+
+        os.chdir(self.test_dir)  # ExtractorTests.tearDown() takes care of restoring.
+        shutil.copytree('canned_locale', 'locale')
+        self._set_times_for_all_po_files()
+        self.addCleanup(self._rmrf, os.path.join(self.test_dir, 'locale'))
+
+    def test_one_locale_excluded(self):
+        management.call_command('makemessages', exclude=['it'], stdout=StringIO())
+        self.assertRecentlyModified(self.PO_FILE % 'en')
+        self.assertRecentlyModified(self.PO_FILE % 'fr')
+        self.assertNotRecentlyModified(self.PO_FILE % 'it')
+
+    def test_multiple_locales_excluded(self):
+        management.call_command('makemessages', exclude=['it', 'fr'], stdout=StringIO())
+        self.assertRecentlyModified(self.PO_FILE % 'en')
+        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
+        self.assertNotRecentlyModified(self.PO_FILE % 'it')
+
+    def test_one_locale_excluded_with_locale(self):
+        management.call_command('makemessages', locale=['en', 'fr'], exclude=['fr'], stdout=StringIO())
+        self.assertRecentlyModified(self.PO_FILE % 'en')
+        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
+        self.assertNotRecentlyModified(self.PO_FILE % 'it')
+
+    def test_multiple_locales_excluded_with_locale(self):
+        management.call_command('makemessages', locale=['en', 'fr', 'it'], exclude=['fr', 'it'],
+                                stdout=StringIO())
+        self.assertRecentlyModified(self.PO_FILE % 'en')
+        self.assertNotRecentlyModified(self.PO_FILE % 'fr')
+        self.assertNotRecentlyModified(self.PO_FILE % 'it')
+
+
 class CustomLayoutExtractionTests(ExtractorTests):
 class CustomLayoutExtractionTests(ExtractorTests):
+
     def setUp(self):
     def setUp(self):
         self._cwd = os.getcwd()
         self._cwd = os.getcwd()
         self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir')
         self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir')