Repository: liberapay/liberapay.com

Issue:
Unable to input custom amount on mobile
Sorry for the bad title, I'm not sure how to put the problem in words.

So I tried to donate a custom amount to a project, but I'm unable to because liberapay doesn't recognize the number format 3.50 as valid. I'd have to type in 3,50, but I'm unable to do so on my mobile phone due to localization. Where I live, 3,50 is 3.50.
Unable to input custom amount on mobile
Sorry for the bad title, I'm not sure how to put the problem in words.

So I tried to donate a custom amount to a project, but I'm unable to because liberapay doesn't recognize the number format 3.50 as valid. I'd have to type in 3,50, but I'm unable to do so on my mobile phone due to localization. Where I live, 3,50 is 3.50.


Discussion:
In which language are you viewing the Liberapay website? Can you copy the exact error message you see when you input 3.50?
> In which language are you viewing the Liberapay website? Can you copy the exact error message you see when you input 3.50?

![image](https://user-images.githubusercontent.com/23403917/131995573-44b7cb79-2100-4a42-91d9-fce389cb7e75.png)
![image](https://user-images.githubusercontent.com/23403917/131995586-1bd3a807-29cc-4724-a53f-897a89633768.png)

Language is DE-CH
In which language are you viewing the Liberapay website? Can you copy the exact error message you see when you input 3.50?
> In which language are you viewing the Liberapay website? Can you copy the exact error message you see when you input 3.50?

![image](https://user-images.githubusercontent.com/23403917/131995573-44b7cb79-2100-4a42-91d9-fce389cb7e75.png)
![image](https://user-images.githubusercontent.com/23403917/131995586-1bd3a807-29cc-4724-a53f-897a89633768.png)

Language is DE-CH

Resolving patch:
diff --git a/liberapay/i18n/base.py b/liberapay/i18n/base.py
--- a/liberapay/i18n/base.py
+++ b/liberapay/i18n/base.py
@@ -98,7 +98,7 @@ def __init__(self, *a, **kw):
         plus_sign = self.number_symbols.get('plusSign', '+')
         if ';' in delta_p:
             pos, neg = delta_p.split(';')
-            assert len(neg) > len(pos)
+            assert len(neg) >= len(pos), (self, neg, pos)
             assert minus_sign in neg
             pos = neg.replace(minus_sign, plus_sign)
             self.currency_delta_pattern = parse_pattern('%s;%s' % (pos, neg))
@@ -431,12 +431,10 @@ def to_age(dt, **kw):
 
 def regularize_locale(loc):
     if loc == 'no':
-
         # There are two forms of written Norwegian, Bokmål and Nynorsk, and
         # while ISO 639 includes `no` as a "macrolanguage", the CLDR (upon
         # which Babel, our i18n/l10n library, depends), does not include it at
         # all. Therefore, if a client sends `no` we interpret it as `nb_NO`.
-
         loc = 'nb_NO'
     return loc.replace('-', '_').lower()
 
diff --git a/liberapay/wireup.py b/liberapay/wireup.py
--- a/liberapay/wireup.py
+++ b/liberapay/wireup.py
@@ -657,7 +657,7 @@ def share_source_strings(catalog, shared_strings):
 
 
 def load_i18n(canonical_host, canonical_scheme, project_root, tell_sentry):
-    # Load the locales
+    # Load the base locales
     localeDir = os.path.join(project_root, 'i18n', 'core')
     locales = LOCALES
     source_strings = {}
@@ -685,6 +685,20 @@ def load_i18n(canonical_host, canonical_scheme, project_root, tell_sentry):
             tell_sentry(e)
     del source_strings
 
+    # Load the variants
+    for loc_id in babel.localedata.locale_identifiers():
+        if loc_id in locales:
+            continue
+        i = loc_id.rfind('_')
+        if i == -1:
+            continue
+        base = locales.get(loc_id[:i])
+        if base:
+            l = locales[loc_id.lower()] = Locale.parse(loc_id)
+            l.catalog = base.catalog
+            l.countries = base.countries
+            l.languages_2 = base.languages_2
+
     # Unload the Babel data that we no longer need
     # We load a lot of data to populate the LANGUAGE_NAMES dict, we don't want
     # to keep it all in RAM.


Test patch:
diff --git a/tests/py/test_i18n.py b/tests/py/test_i18n.py
--- a/tests/py/test_i18n.py
+++ b/tests/py/test_i18n.py
@@ -73,6 +73,13 @@ def test_chinese_visitor_gets_chinese_locale(self):
         state = self.client.GET('/', HTTP_ACCEPT_LANGUAGE=b'zh-CN', want='state')
         assert state['locale'] == self.website.locales['zh']
 
+    def test_swiss_german(self):
+        state = self.client.GET('/', HTTP_ACCEPT_LANGUAGE=b'de-ch', want='state')
+        locale = state['locale']
+        assert locale is self.website.locales['de_ch']
+        assert locale.format_money(Money('5200.00', 'EUR')) == '€ 5’200.00'
+        assert locale.parse_money_amount('5’200.00', 'EUR') == Money('5200.00', 'EUR')
+
     def test_get_currencies_for(self):
         # Unidentified donor with a Swiss IP address, giving to a creator in France.
         alice = self.make_participant('alice', main_currency='EUR', accepted_currencies='EUR,USD')