Repository: django/django
Issue category: Bug Report

Issue:
makemessages: is_valid_locale regex fails to validate locales with numeric region codes
Description
		
(last modified by Juan Pablo Mallarino)		
The is_valid_locale function in the makemessages management command uses a regular expression that doesn't account for locales with numeric region codes, such as es_419. The current regex only allows uppercase letters after the underscore. This excludes valid locale codes that use numbers in the region subtag.
This can cause issues when trying to generate message files for these locales. For example, running makemessages with es_419 will not generate the expected message files, potentially leading to missing translations.
Proposed Solution
Modify the regular expression in is_valid_locale to include numeric characters in the region subtag validation. The suggested change is:
From:
r"^[a-z]+_[A-Z].*$"
To:
r"^[a-z]+_[A-Z0-9].*$"
This change would allow the validation of locales while still maintaining the expected format for other locale codes. This simple modification would ensure broader compatibility and avoid unexpected behavior when working with valid locales containing numeric region codes.


Hints:
['This is the PR that introduced the validating function: \u200bhttps://github.com/django/django/pull/15521 and the issue https://code.djangoproject.com/ticket/33565', 1733325193.0]
["Support for regions was added in #33078 (see also test case like de-1996) The validation added in #33565 doesn't match this", 1733370439.0]

Patch:
diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
index 076667d41a8c..23ad424c5c31 100644
--- a/django/core/management/commands/makemessages.py
+++ b/django/core/management/commands/makemessages.py
@@ -40,7 +40,7 @@ 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)
+    return re.match(r"^[a-z]+$", locale) or re.match(r"^[a-z]+_[A-Z0-9].*$", locale)
 
 
 @total_ordering


Test patch:
diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py
index 7aa600c4c1e2..e4a6260c336a 100644
--- a/tests/i18n/test_extraction.py
+++ b/tests/i18n/test_extraction.py
@@ -179,6 +179,15 @@ def test_valid_locale_with_country(self):
         self.assertIn("processing locale en_GB", out.getvalue())
         self.assertIs(Path("locale/en_GB/LC_MESSAGES/django.po").exists(), True)
 
+    def test_valid_locale_with_numeric_region_code(self):
+        out = StringIO()
+        management.call_command(
+            "makemessages", locale=["ar_002"], stdout=out, verbosity=1
+        )
+        self.assertNotIn("invalid locale ar_002", out.getvalue())
+        self.assertIn("processing locale ar_002", out.getvalue())
+        self.assertIs(Path("locale/ar_002/LC_MESSAGES/django.po").exists(), True)
+
     def test_valid_locale_tachelhit_latin_morocco(self):
         out = StringIO()
         management.call_command(