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>
     Arthur Koziel <http://arthurkoziel.com>
     Meir Kriheli <http://mksoft.co.il/>
+    Ana Krivokapic <https://github.com/infraredgirl>
     Bruce Kroeze <http://coderseye.com/>
     krzysiek.pawlik@silvermedia.pl
     konrad@gwu.edu

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

@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 import codecs
+import glob
 import os
 from optparse import make_option
 
@@ -30,8 +31,11 @@ def is_writable(path):
 
 class Command(BaseCommand):
     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.'
 
@@ -43,6 +47,7 @@ class Command(BaseCommand):
 
     def handle(self, **options):
         locale = options.get('locale')
+        exclude = options.get('exclude')
         self.verbosity = int(options.get('verbosity'))
 
         if find_command(self.program) is None:
@@ -62,9 +67,19 @@ class Command(BaseCommand):
                                "checkout or your project or app tree, or with "
                                "the settings module specified.")
 
+        # Build locale list
+        all_locales = []
         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:
                 dirs = [basedir]
             locations = []
@@ -90,8 +105,8 @@ class Command(BaseCommand):
 
             # Check writability on first location
             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
 
             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):
     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). '
                  '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',
             help='The domain of the message files (default: "django").'),
         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 "
 "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 "
-"--locale or --all options.")
+"--locale, --exclude or --all options.")
 
     requires_system_checks = False
     leave_locale_alone = True
@@ -201,6 +203,7 @@ class Command(NoArgsCommand):
 
     def handle_noargs(self, *args, **options):
         locale = options.get('locale')
+        exclude = options.get('exclude')
         self.domain = options.get('domain')
         self.verbosity = int(options.get('verbosity'))
         process_all = options.get('all')
@@ -235,7 +238,7 @@ class Command(NoArgsCommand):
             exts = extensions if extensions else ['html', 'txt']
         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." % (
                 os.path.basename(sys.argv[0]), sys.argv[1]))
 
@@ -270,12 +273,16 @@ class Command(NoArgsCommand):
                     os.makedirs(self.default_locale_path)
 
         # 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:
             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
 Cleans out old data from the database (only expired sessions at the moment).
 .TP
-.BI "compilemessages [" "\-\-locale=LOCALE" "]"
+.BI "compilemessages [" "\-\-locale=LOCALE" "] [" "\-\-exclude=LOCALE" "]"
 Compiles .po files to .mo files for use with builtin gettext support.
 .TP
 .BI "createcachetable [" "tablename" "]"
@@ -59,7 +59,7 @@ Executes
 .B sqlall
 for the given app(s) in the current database.
 .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
 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.
@@ -176,6 +176,9 @@ output a full stack trace whenever an exception is raised.
 .I \-l, \-\-locale=LOCALE
 The locale to process when using makemessages or compilemessages.
 .TP
+.I \-e, \-\-exclude=LOCALE
+The locale to exclude from processing when using makemessages or compilemessages.
+.TP
 .I \-d, \-\-domain=DOMAIN
 The domain of the message files (default: "django") when using makemessages.
 .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
 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::
 
     django-admin.py compilemessages --locale=pt_BR
     django-admin.py compilemessages --locale=pt_BR --locale=fr
     django-admin.py compilemessages -l pt_BR
     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
 ----------------
@@ -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
 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::
 
     django-admin.py makemessages --locale=pt_BR
     django-admin.py makemessages --locale=pt_BR --locale=fr
     django-admin.py makemessages -l pt_BR
     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
 

+ 4 - 0
docs/releases/1.8.txt

@@ -128,6 +128,10 @@ Management Commands
 * :djadmin:`dumpdata` now has the option :djadminopt:`--output` which allows
   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
 ^^^^^^
 

+ 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 shutil
 import stat
 import unittest
 
@@ -10,17 +11,23 @@ from django.utils import translation
 from django.utils._os import upath
 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')
 
 
 @unittest.skipUnless(has_msgfmt, 'msgfmt is mandatory for compilation tests')
 class MessageCompilationTests(SimpleTestCase):
 
+    test_dir = os.path.abspath(os.path.join(os.path.dirname(upath(__file__)), 'commands'))
+
     def setUp(self):
         self._cwd = os.getcwd()
         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):
         if os.path.exists(filepath):
@@ -60,7 +67,7 @@ class PoFileContentsTests(MessageCompilationTests):
 
     def setUp(self):
         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):
         call_command('compilemessages', locale=[self.LOCALE], stdout=StringIO())
@@ -76,45 +83,85 @@ class PercentRenderingTests(MessageCompilationTests):
 
     def setUp(self):
         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):
-        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):
+
     MO_FILE_HR = None
     MO_FILE_FR = None
 
     def setUp(self):
         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_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_FR))
 
     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):
-        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):
@@ -124,7 +171,7 @@ class CompilationErrorHandling(MessageCompilationTests):
 
     def setUp(self):
         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):
         with self.assertRaises(CommandError):

+ 67 - 2
tests/i18n/test_extraction.py

@@ -5,6 +5,7 @@ import io
 import os
 import re
 import shutil
+import time
 from unittest import SkipTest, skipUnless
 import warnings
 
@@ -27,12 +28,12 @@ has_xgettext = find_command('xgettext')
 @skipUnless(has_xgettext, 'xgettext is mandatory for extraction tests')
 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
 
     def setUp(self):
         self._cwd = os.getcwd()
-        self.test_dir = os.path.abspath(
-            os.path.join(os.path.dirname(upath(__file__)), 'commands'))
 
     def _rmrf(self, dname):
         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()"""
         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):
 
@@ -402,6 +417,7 @@ class SymlinkExtractorTests(ExtractorTests):
 
 
 class CopyPluralFormsExtractorTests(ExtractorTests):
+
     PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'
 
     def tearDown(self):
@@ -527,7 +543,56 @@ class MultipleLocaleExtractionTests(ExtractorTests):
         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):
+
     def setUp(self):
         self._cwd = os.getcwd()
         self.test_dir = os.path.join(os.path.dirname(upath(__file__)), 'project_dir')