From c32858a8ce961d276215a040ae0ab1e4409b70f8 Mon Sep 17 00:00:00 2001 From: Ronnie van den Crommenacker Date: Thu, 17 Mar 2022 12:02:59 +0100 Subject: [PATCH] Fixed #33565 -- Improved locale format validation for the makemessages command. --- AUTHORS | 1 + .../core/management/commands/makemessages.py | 45 ++++++++-- docs/releases/4.2.txt | 3 +- tests/i18n/test_extraction.py | 84 ++++++++++++++++++- 4 files changed, 124 insertions(+), 9 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8c20814b13..b726a8a67b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -829,6 +829,7 @@ answer newbie questions, and generally made Django that much better: Rodrigo Pinheiro Marques de Araújo Rohith P R Romain Garrigues + Ronnie van den Crommenacker Ronny Haryanto Ross Poulton Roxane Bellot diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 1c68fb453e..1d4947fb30 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -40,6 +40,10 @@ def check_programs(*programs): ) +def is_valid_locale(locale): + return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z].*$", locale) + + @total_ordering class TranslatableFile: def __init__(self, dirpath, file_name, locale_dir): @@ -427,14 +431,41 @@ class Command(BaseCommand): # Build po files for each selected locale for locale in locales: - if "-" in locale: - self.stdout.write( - "invalid locale %s, did you mean %s?" - % ( - locale, - locale.replace("-", "_"), - ), + if not is_valid_locale(locale): + # Try to guess what valid locale it could be + # Valid examples are: en_GB, shi_Latn_MA and nl_NL-x-informal + + # Search for characters followed by a non character (i.e. separator) + match = re.match( + r"^(?P[a-zA-Z]+)" + r"(?P[^a-zA-Z])" + r"(?P.+)$", + locale, ) + if match: + locale_parts = match.groupdict() + language = locale_parts["language"].lower() + territory = ( + locale_parts["territory"][:2].upper() + + locale_parts["territory"][2:] + ) + proposed_locale = f"{language}_{territory}" + else: + # It could be a language in uppercase + proposed_locale = locale.lower() + + # Recheck if the proposed locale is valid + if is_valid_locale(proposed_locale): + self.stdout.write( + "invalid locale %s, did you mean %s?" + % ( + locale, + proposed_locale, + ), + ) + else: + self.stdout.write("invalid locale %s" % locale) + continue if self.verbosity > 0: self.stdout.write("processing locale %s" % locale) diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index b510eaa78f..345f4d4ad8 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -155,7 +155,8 @@ Logging Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* :djadmin:`makemessages` command now supports locales with private sub-tags + such as ``nl_NL-x-informal``. Migrations ~~~~~~~~~~ diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py index c2ae10efd8..5287677bab 100644 --- a/tests/i18n/test_extraction.py +++ b/tests/i18n/test_extraction.py @@ -175,7 +175,43 @@ class BasicExtractorTests(ExtractorTests): self.assertIn("processing locale de", out.getvalue()) self.assertIs(Path(self.PO_FILE).exists(), True) - def test_invalid_locale(self): + def test_valid_locale_with_country(self): + out = StringIO() + management.call_command( + "makemessages", locale=["en_GB"], stdout=out, verbosity=1 + ) + self.assertNotIn("invalid locale en_GB", out.getvalue()) + self.assertIn("processing locale en_GB", out.getvalue()) + self.assertIs(Path("locale/en_GB/LC_MESSAGES/django.po").exists(), True) + + def test_valid_locale_tachelhit_latin_morocco(self): + out = StringIO() + management.call_command( + "makemessages", locale=["shi_Latn_MA"], stdout=out, verbosity=1 + ) + self.assertNotIn("invalid locale shi_Latn_MA", out.getvalue()) + self.assertIn("processing locale shi_Latn_MA", out.getvalue()) + self.assertIs(Path("locale/shi_Latn_MA/LC_MESSAGES/django.po").exists(), True) + + def test_valid_locale_private_subtag(self): + out = StringIO() + management.call_command( + "makemessages", locale=["nl_NL-x-informal"], stdout=out, verbosity=1 + ) + self.assertNotIn("invalid locale nl_NL-x-informal", out.getvalue()) + self.assertIn("processing locale nl_NL-x-informal", out.getvalue()) + self.assertIs( + Path("locale/nl_NL-x-informal/LC_MESSAGES/django.po").exists(), True + ) + + def test_invalid_locale_uppercase(self): + out = StringIO() + management.call_command("makemessages", locale=["PL"], stdout=out, verbosity=1) + self.assertIn("invalid locale PL, did you mean pl?", out.getvalue()) + self.assertNotIn("processing locale pl", out.getvalue()) + self.assertIs(Path("locale/pl/LC_MESSAGES/django.po").exists(), False) + + def test_invalid_locale_hyphen(self): out = StringIO() management.call_command( "makemessages", locale=["pl-PL"], stdout=out, verbosity=1 @@ -184,6 +220,52 @@ class BasicExtractorTests(ExtractorTests): self.assertNotIn("processing locale pl-PL", out.getvalue()) self.assertIs(Path("locale/pl-PL/LC_MESSAGES/django.po").exists(), False) + def test_invalid_locale_lower_country(self): + out = StringIO() + management.call_command( + "makemessages", locale=["pl_pl"], stdout=out, verbosity=1 + ) + self.assertIn("invalid locale pl_pl, did you mean pl_PL?", out.getvalue()) + self.assertNotIn("processing locale pl_pl", out.getvalue()) + self.assertIs(Path("locale/pl_pl/LC_MESSAGES/django.po").exists(), False) + + def test_invalid_locale_private_subtag(self): + out = StringIO() + management.call_command( + "makemessages", locale=["nl-nl-x-informal"], stdout=out, verbosity=1 + ) + self.assertIn( + "invalid locale nl-nl-x-informal, did you mean nl_NL-x-informal?", + out.getvalue(), + ) + self.assertNotIn("processing locale nl-nl-x-informal", out.getvalue()) + self.assertIs( + Path("locale/nl-nl-x-informal/LC_MESSAGES/django.po").exists(), False + ) + + def test_invalid_locale_plus(self): + out = StringIO() + management.call_command( + "makemessages", locale=["en+GB"], stdout=out, verbosity=1 + ) + self.assertIn("invalid locale en+GB, did you mean en_GB?", out.getvalue()) + self.assertNotIn("processing locale en+GB", out.getvalue()) + self.assertIs(Path("locale/en+GB/LC_MESSAGES/django.po").exists(), False) + + def test_invalid_locale_end_with_underscore(self): + out = StringIO() + management.call_command("makemessages", locale=["en_"], stdout=out, verbosity=1) + self.assertIn("invalid locale en_", out.getvalue()) + self.assertNotIn("processing locale en_", out.getvalue()) + self.assertIs(Path("locale/en_/LC_MESSAGES/django.po").exists(), False) + + def test_invalid_locale_start_with_underscore(self): + out = StringIO() + management.call_command("makemessages", locale=["_en"], stdout=out, verbosity=1) + self.assertIn("invalid locale _en", out.getvalue()) + self.assertNotIn("processing locale _en", out.getvalue()) + self.assertIs(Path("locale/_en/LC_MESSAGES/django.po").exists(), False) + def test_comments_extractor(self): management.call_command("makemessages", locale=[LOCALE], verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE))