Repository: python-babel/babel
Pull request #361 (2016-03-02 11:17:14)

Parsing of mixed-cased locale ID.
Following #351, it seems that mixed-cased locale IDs are not properly checked as existing depending on the platform.

The issue lies in `babel.localedata.exists()` which rely on filesystem assets. As a result, mixed-cased locale IDs are properly parsed on OSX (case-insensitive filesystem) but not on Linux.

The first commit [demonstrate the issue](https://travis-ci.org/python-babel/babel/builds/113096167) depending on the system.
The second commit ensure that locale IDs recognized by Babel are uniques, even if lower-cased.
The third commit fix to the issue.


Patch:
diff --git a/babel/localedata.py b/babel/localedata.py
index 437f49f..1f43737 100644
--- a/babel/localedata.py
+++ b/babel/localedata.py
@@ -15,6 +15,7 @@
 import os
 import threading
 from collections import MutableMapping
+from itertools import chain
 
 from babel._compat import pickle
 
@@ -24,15 +25,29 @@ _cache_lock = threading.RLock()
 _dirname = os.path.join(os.path.dirname(__file__), 'locale-data')
 
 
+def normalize_locale(name):
+    """Normalize a locale ID by stripping spaces and apply proper casing.
+
+    Returns the normalized locale ID string or `None` if the ID is not
+    recognized.
+    """
+    name = name.strip().lower()
+    for locale_id in chain.from_iterable([_cache, locale_identifiers()]):
+        if name == locale_id.lower():
+            return locale_id
+
+
 def exists(name):
-    """Check whether locale data is available for the given locale.  Ther
-    return value is `True` if it exists, `False` otherwise.
+    """Check whether locale data is available for the given locale.
+
+    Returns `True` if it exists, `False` otherwise.
 
     :param name: the locale identifier string
     """
     if name in _cache:
         return True
-    return os.path.exists(os.path.join(_dirname, '%s.dat' % name))
+    file_found = os.path.exists(os.path.join(_dirname, '%s.dat' % name))
+    return True if file_found else bool(normalize_locale(name))
 
 
 def locale_identifiers():


Test patch:
diff --git a/tests/test_localedata.py b/tests/test_localedata.py
index 80dd118..d0d92f6 100644
--- a/tests/test_localedata.py
+++ b/tests/test_localedata.py
@@ -13,6 +13,8 @@
 
 import doctest
 import unittest
+import random
+from operator import methodcaller
 
 from babel import localedata
 
@@ -73,6 +75,23 @@ def test_merge():
     localedata.merge(d, {1: 'Foo', 2: 'Bar'})
     assert d == {1: 'Foo', 2: 'Bar', 3: 'baz'}
 
+
 def test_locale_identification():
     for l in localedata.locale_identifiers():
         assert localedata.exists(l)
+
+
+def test_unique_ids():
+    # Check all locale IDs are uniques.
+    all_ids = localedata.locale_identifiers()
+    assert len(all_ids) == len(set(all_ids))
+    # Check locale IDs don't collide after lower-case normalization.
+    lower_case_ids = list(map(methodcaller('lower'), all_ids))
+    assert len(lower_case_ids) == len(set(lower_case_ids))
+
+
+def test_mixedcased_locale():
+    for l in localedata.locale_identifiers():
+        locale_id = ''.join([
+            methodcaller(random.choice(['lower', 'upper']))(c) for c in l])
+        assert localedata.exists(locale_id)