Repository: liberapay/liberapay.com

Issue:
UI: donation amount wrongly formatted when ending with 0
E.g. if I want to donate 0,30 (e.g. €) per month, this is how it looks when the form is submitted/later opened:
![grafik](https://user-images.githubusercontent.com/11966684/37768666-baa8bb6e-2dce-11e8-9b5c-d4a7523b0722.png)

This is how it should look:
![grafik](https://user-images.githubusercontent.com/11966684/37768685-cbbb59a2-2dce-11e8-85a4-f92cfb4f8bab.png)



Discussion:

Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -3,6 +3,7 @@
 from decimal import Decimal, ROUND_FLOOR, ROUND_HALF_UP, ROUND_UP
 import re
 
+from babel.numbers import get_currency_precision
 from mangopay.utils import Money
 from markupsafe import Markup
 from pando.utils import utc
@@ -433,7 +434,8 @@ def __missing__(self, currency):
 
 
 def make_standard_tip(label, weekly, currency):
-    minimum = Money.MINIMUMS[currency].amount if hasattr(Money, 'MINIMUMS') else D_CENT
+    precision = get_currency_precision(currency)
+    minimum = D_CENT if precision == 2 else Decimal(10) ** (-precision)
     return StandardTip(
         label,
         Money(weekly, currency),
diff --git a/liberapay/i18n/base.py b/liberapay/i18n/base.py
--- a/liberapay/i18n/base.py
+++ b/liberapay/i18n/base.py
@@ -6,7 +6,7 @@
 import babel.core
 from babel.dates import format_date, format_datetime, format_time, format_timedelta
 from babel.messages.pofile import Catalog
-from babel.numbers import format_currency, format_decimal, format_percent
+from babel.numbers import parse_pattern
 from markupsafe import Markup
 from pando.utils import utcnow
 
@@ -16,6 +16,10 @@
 from .currencies import Money, MoneyBasket
 
 
+MONEY_AMOUNT_FORMAT = parse_pattern('#,##0.00')
+ONLY_ZERO = {'0'}
+
+
 def no_escape(s):
     return s
 
@@ -64,7 +68,7 @@ class Locale(babel.core.Locale):
 
     def __init__(self, *a, **kw):
         super(Locale, self).__init__(*a, **kw)
-        self.decimal_symbol = self.number_symbols['decimal']
+        self.currency_formats['amount_only'] = MONEY_AMOUNT_FORMAT
         delta_p = self.currency_formats['standard'].pattern
         minus_sign = self.number_symbols.get('minusSign', '-')
         plus_sign = self.number_symbols.get('plusSign', '+')
@@ -73,9 +77,9 @@ def __init__(self, *a, **kw):
             assert len(neg) > len(pos)
             assert minus_sign in neg
             pos = neg.replace(minus_sign, plus_sign)
-            self.currency_delta_pattern = '%s;%s' % (pos, neg)
+            self.currency_delta_pattern = parse_pattern('%s;%s' % (pos, neg))
         else:
-            self.currency_delta_pattern = (
+            self.currency_delta_pattern = parse_pattern(
                 '{0}{2};{1}{2}'.format(plus_sign, minus_sign, delta_p)
             )
 
@@ -116,7 +120,7 @@ def ngettext(self, state, s, p, n, *a, **kw):
             if self is not LOCALE_EN:
                 self = LOCALE_EN
                 state['partial_translation'] = True
-        kw['n'] = format_decimal(n, locale=self) or n
+        kw['n'] = self.format_decimal(n) or n
         if wrapper:
             kw['n'] = wrapper % kw['n']
         try:
@@ -137,7 +141,7 @@ def format(self, s, *a, **kw):
                     elif isinstance(o, Currency):
                         o = self.currencies.get(o, o)
                 elif isinstance(o, (Decimal, int)):
-                    o = format_decimal(o, locale=self)
+                    o = self.format_decimal(o)
                 elif isinstance(o, Money):
                     o = self.format_money(o)
                 elif isinstance(o, MoneyBasket):
@@ -161,10 +165,15 @@ def format(self, s, *a, **kw):
                     c[k] = o
         return s.format(*a, **kw)
 
-    def format_money(self, m, format=None, trailing_zeroes=True):
-        s = format_currency(m.amount, m.currency, format, locale=self)
-        if not trailing_zeroes:
-            s = s.replace(self.decimal_symbol + '00', '')
+    def format_money(self, m, format='standard', trailing_zeroes=True):
+        s = self.currency_formats[format].apply(
+            m.amount, self, currency=m.currency, currency_digits=True,
+            decimal_quantization=True
+        )
+        if trailing_zeroes is False:
+            i = s.find(self.number_symbols['decimal'])
+            if i != -1 and set(s[i+1:]) == ONLY_ZERO:
+                s = s[:i]
         return s
 
     def format_date(self, date, format='medium'):
@@ -175,9 +184,8 @@ def format_date(self, date, format='medium'):
     def format_datetime(self, *a):
         return format_datetime(*a, locale=self)
 
-    def format_decimal(self, *a, **kw):
-        kw['locale'] = self
-        return format_decimal(*a, **kw)
+    def format_decimal(self, number, **kw):
+        return self.decimal_formats[None].apply(number, self, **kw)
 
     def format_list(self, l, pattern='standard', escape=no_escape):
         n = len(l)
@@ -197,8 +205,9 @@ def format_list(self, l, pattern='standard', escape=no_escape):
     def format_money_basket(self, basket, sep=','):
         if basket is None:
             return '0'
+        pattern = self.currency_formats['standard']
         items = (
-            format_currency(money.amount, money.currency, locale=self)
+            pattern.apply(money.amount, self, currency=money.currency)
             for money in basket if money
         )
         if sep == ',':
@@ -207,14 +216,14 @@ def format_money_basket(self, basket, sep=','):
             r = sep.join(items)
         return r or '0'
 
-    def format_money_delta(self, money, *a):
-        return format_currency(
-            money.amount, money.currency, *a,
-            format=self.currency_delta_pattern, locale=self
+    def format_money_delta(self, money):
+        return self.currency_delta_pattern.apply(
+            money.amount, self, currency=money.currency, currency_digits=True,
+            decimal_quantization=True
         )
 
-    def format_percent(self, *a):
-        return format_percent(*a, locale=self)
+    def format_percent(self, number, **kw):
+        return self.percent_formats[None].apply(number, self, **kw)
 
     def format_time(self, t, format='medium'):
         return format_time(t, format=format, locale=self)
diff --git a/liberapay/i18n/currencies.py b/liberapay/i18n/currencies.py
--- a/liberapay/i18n/currencies.py
+++ b/liberapay/i18n/currencies.py
@@ -3,6 +3,7 @@
 from numbers import Number
 import operator
 
+from babel.numbers import get_currency_precision
 from mangopay.exceptions import CurrencyMismatch
 from mangopay.utils import Money
 import requests
@@ -82,7 +83,7 @@ def _Money_round(self, rounding=ROUND_HALF_UP):
 
 class _Minimums(defaultdict):
     def __missing__(self, currency):
-        exponent = website.db.one("SELECT get_currency_exponent(%s)", (currency,))
+        exponent = get_currency_precision(currency)
         minimum = Money((D_CENT if exponent == 2 else Decimal(10) ** (-exponent)), currency)
         self[currency] = minimum
         return minimum


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
@@ -34,15 +34,17 @@ def test_default_donation_currency(self):
         r = self.client.GET('/alice/donate')
         assert '<input type="hidden" name="currency" value="KRW" />' in r.text, r.text
 
-    def test_format_currency_without_trailing_zeroes(self):
-        expected = '$16'
-        actual = LOCALE_EN.format_money(Money(16, 'USD'), trailing_zeroes=False)
-        assert actual == expected
+    def test_format_money_without_trailing_zeroes(self):
+        result = LOCALE_EN.format_money(Money(16, 'USD'), trailing_zeroes=False)
+        assert result == '$16'
+        result = LOCALE_EN.format_money(Money(5555, 'KRW'), trailing_zeroes=False)
+        assert result == '₩5,555'
 
-    def test_format_currency_defaults_to_trailing_zeroes(self):
-        expected = '$16.00'
-        actual = LOCALE_EN.format_money(Money(16, 'USD'))
-        assert actual == expected
+    def test_format_money_defaults_to_trailing_zeroes(self):
+        result = LOCALE_EN.format_money(Money(16, 'USD'))
+        assert result == '$16.00'
+        result = LOCALE_EN.format_money(Money(5555, 'KRW'))
+        assert result == '₩5,555'
 
     def test_locales_share_message_keys(self):
         msgkey1 = self.website.locales['de'].catalog['Save'].id