Repository: liberapay/liberapay.com

Issue:
Mistakes in translations can lead to 500s (internal server errors)
https://sentry.changaco.oy.lc/share/issue/322e3437/

We should catch the exception, log it to sentry, and fall back to english.



Discussion:
I'd like to take a look at this one, but I lack some context. Is there anyway you could share the stack trace of the error ? (I have no access to the sentry)
Everyone has access to the Sentry issues when the link has `/share/` in it. Unfortunately Sentry often fails to display issues that are too old. Maybe upgrading Sentry would show at least an error message.
I can't give you a traceback, I don't think I have one, but basically the line that can trigger an uncaught exception is <https://github.com/liberapay/liberapay.com/blob/144/liberapay/utils/i18n.py#L149>. Because for example `"{1}".format(0)` raises `IndexError: tuple index out of range`. The solution is to wrap the calls to `i_format()` in `get_text()` and others.
Note that since this issue was opened our PO files and Weblate project configuration were modified to properly treat messages as `python-brace-format` instead of `python-format` (see #403), and the `pytest-i18n-browse` Makefile target was created (see #404). All that has added protections against bad translations reaching production.
https://sentry.io/share/issue/cd44bc9622494ea3a1c41f0e68b3fce7/

Resolving patch:
diff --git a/liberapay/utils/i18n.py b/liberapay/utils/i18n.py
--- a/liberapay/utils/i18n.py
+++ b/liberapay/utils/i18n.py
@@ -201,6 +201,10 @@ def get_function_from_rule(rule):
     return eval('lambda n: ' + rule, {'__builtins__': {}})
 
 
+def _decode(o):
+    return o.decode('ascii') if isinstance(o, bytes) else o
+
+
 def i_format(loc, s, *a, **kw):
     if a:
         a = list(a)
@@ -226,23 +230,24 @@ def i_format(loc, s, *a, **kw):
     return s.format(*a, **kw)
 
 
-def get_text(context, loc, s, *a, **kw):
-    escape = context['escape']
+def get_text(state, loc, s, *a, **kw):
+    escape = state['escape']
     msg = loc.catalog.get(s)
     s2 = None
     if msg:
         s2 = msg.string
         if isinstance(s2, tuple):
             s2 = s2[0]
-    if s2:
-        s = s2
-    else:
+    if not s2:
+        s2 = s
         loc = LOCALE_EN
     if a or kw:
-        if isinstance(s, bytes):
-            s = s.decode('ascii')
-        return i_format(loc, escape(s), *a, **kw)
-    return escape(s)
+        try:
+            return i_format(loc, escape(_decode(s2)), *a, **kw)
+        except Exception as e:
+            website.tell_sentry(e, state)
+            return i_format(LOCALE_EN, escape(_decode(s)), *a, **kw)
+    return escape(s2)
 
 
 def n_get_text(state, loc, s, p, n, *a, **kw):
@@ -262,9 +267,11 @@ def n_get_text(state, loc, s, p, n, *a, **kw):
     kw['n'] = format_number(n, locale=loc) or n
     if wrapper:
         kw['n'] = wrapper % kw['n']
-    if isinstance(s2, bytes):
-        s2 = s2.decode('ascii')
-    return i_format(loc, escape(s2), *a, **kw)
+    try:
+        return i_format(loc, escape(_decode(s2)), *a, **kw)
+    except Exception as e:
+        website.tell_sentry(e, state)
+        return i_format(LOCALE_EN, escape(_decode(s if n == 1 else p)), *a, **kw)
 
 
 def getdoc(state, name):


Test patch: