Repository: ckan/ckan

Issue:
Flask-Babel 2.0.0 breaks ITranslation
Upgrading Flask-Babel from 1.0.0 to [2.0.0](https://github.com/python-babel/flask-babel/releases/tag/v2.0.0) makes the `ITranslation` tests fail:

https://app.circleci.com/pipelines/github/ckan/ckan/1902/workflows/8ef66c69-6def-4d1b-8bbf-1678e93f56aa/jobs/9630

Basically our custom override to support multiple translation domains is never called:

https://github.com/ckan/ckan/blob/d47eb0972a6f99460926e2869bd858caee1f74bf/ckan/config/middleware/flask_app.py#L97-L106

@TkTech I *think* that the way this works in Flask-babel changed as part of this change: https://github.com/python-babel/flask-babel/pull/163 but I couldn't find any docs on how the new approach works.

Do you have any pointers on how to refactor this on our end?


Discussion:
@amercader I'll look into this, however it'll have to wait until Saturday.
I'm not entirely sure how this works as-is. It seems to depend on the order it's called (?) which is prone to breaking. Under v2.0.0, the approach I think will work here is to create a:
 ```domain = flask_babel.Domain(translation_directories=plugin.i18n_directory(), domain=plugin.i18n_domain())```
 for each plugin that defines `ITranslation`, then pass in `domain.gettext` to the templates instead of `flask_babel.gettext`.
@TkTech I wasn't able to figure out your approach, so I proposed this on the Flask-Babel side:

https://github.com/python-babel/flask-babel/pull/194

Resolving patch:
diff --git a/ckan/common.py b/ckan/common.py
--- a/ckan/common.py
+++ b/ckan/common.py
@@ -68,14 +68,14 @@ def streaming_response(data: Iterable[Any],
 
 
 def ugettext(*args: Any, **kwargs: Any) -> str:
-    return cast(str, flask_ugettext(*args, **kwargs))
+    return flask_ugettext(*args, **kwargs)
 
 
 _ = ugettext
 
 
 def ungettext(*args: Any, **kwargs: Any) -> str:
-    return cast(str, flask_ungettext(*args, **kwargs))
+    return flask_ungettext(*args, **kwargs)
 
 
 class CKANConfig(MutableMapping):
diff --git a/ckan/config/middleware/flask_app.py b/ckan/config/middleware/flask_app.py
--- a/ckan/config/middleware/flask_app.py
+++ b/ckan/config/middleware/flask_app.py
@@ -10,7 +10,7 @@
 import logging
 
 from logging.handlers import SMTPHandler
-from typing import Any, Iterable, Optional, Union, cast
+from typing import Any, Optional, Union, cast
 
 from flask import Blueprint, send_from_directory, current_app
 from flask.ctx import _AppCtxGlobals
@@ -82,33 +82,6 @@ def __call__(self, environ: Any, start_response: Any):
         return self.app(environ, start_response)
 
 
-class CKANBabel(Babel):
-    app: CKANApp
-
-    def __init__(self, *pargs: Any, **kwargs: Any):
-        super(CKANBabel, self).__init__(*pargs, **kwargs)
-        self._i18n_path_idx = 0
-
-    @property
-    def domain(self) -> str:
-        default = super(CKANBabel, self).domain
-        multiple = self.app.config.get('BABEL_MULTIPLE_DOMAINS')
-        if not multiple:
-            return default
-        domains = multiple.split(';')
-        try:
-            return domains[self._i18n_path_idx]
-        except IndexError:
-            return default
-
-    @property
-    def translation_directories(self) -> Iterable[str]:
-        self._i18n_path_idx = 0
-        for path in super(CKANBabel, self).translation_directories:
-            yield path
-            self._i18n_path_idx += 1
-
-
 def _ungettext_alias():
     u'''
     Provide `ungettext` as an alias of `ngettext` for backwards
@@ -246,13 +219,10 @@ def make_flask_stack(conf: Union[Config, CKANConfig]) -> CKANApp:
     i18n_dirs, i18n_domains = zip(*pairs)
 
     app.config[u'BABEL_TRANSLATION_DIRECTORIES'] = ';'.join(i18n_dirs)
-    app.config[u'BABEL_DOMAIN'] = 'ckan'
-    app.config[u'BABEL_MULTIPLE_DOMAINS'] = ';'.join(i18n_domains)
+    app.config[u'BABEL_DOMAIN'] = ';'.join(i18n_domains)
     app.config[u'BABEL_DEFAULT_TIMEZONE'] = str(h.get_display_timezone())
 
-    babel = CKANBabel(app)
-
-    babel.localeselector(get_locale)
+    Babel(app, locale_selector=get_locale)
 
     # WebAssets
     _setup_webassets(app)
diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py
--- a/ckan/lib/i18n.py
+++ b/ckan/lib/i18n.py
@@ -46,7 +46,7 @@
 
 import six
 from babel import Locale
-from babel.core import (LOCALE_ALIASES,  # type: ignore
+from babel.core import (LOCALE_ALIASES,
                         get_locale_identifier,
                         UnknownLocaleError)
 import polib


Test patch:
diff --git a/ckan/tests/controllers/test_package.py b/ckan/tests/controllers/test_package.py
--- a/ckan/tests/controllers/test_package.py
+++ b/ckan/tests/controllers/test_package.py
@@ -2,6 +2,7 @@
 
 from bs4 import BeautifulSoup
 from werkzeug.routing import BuildError
+from flask_babel import refresh as refresh_babel
 import unittest.mock as mock
 
 import ckan.authz as authz
@@ -89,6 +90,8 @@ def test_change_locale(self, app, user):
         url = url_for("dataset.new")
         env = {"Authorization": user["token"]}
         res = app.get(url, extra_environ=env)
+        # See https://github.com/python-babel/flask-babel/issues/214
+        refresh_babel()
         res = app.get("/de/dataset/new", extra_environ=env)
         assert helpers.body_contains(res, "Datensatz")
 
diff --git a/ckan/tests/lib/test_formatters.py b/ckan/tests/lib/test_formatters.py
--- a/ckan/tests/lib/test_formatters.py
+++ b/ckan/tests/lib/test_formatters.py
@@ -58,7 +58,7 @@ class TestLocalizedNiceDate(object):
             (_now, False, False, False, u"Just now"),
             (_now, True, False, False, u"October 23, 2017"),
             (_now, True, True, False, u"October 23, 2017, 16:03 (UTC)"),
-            (_now, True, True, True, u"October 23, 2017 at 4:03:52 PM UTC"),
+            (_now, True, True, True, u"October 23, 2017, 4:03:52\u202fPM UTC"),
             (_now, False, True, True, u"Just now"),
             (_now, False, False, True, u"Just now"),
             (_now, False, True, False, u"Just now"),
@@ -97,7 +97,7 @@ def test_relative_dates(self, dt, expected):
             (_now, False, False, u"MMM, YY", u"Oct, 17"),
             (_now, True, False, None, u"October 23, 2017, 16:03 (UTC)"),
             (_now, True, False, u"EEE, HH:mm", u"Mon, 16:03"),
-            (_now, True, True, None, u"October 23, 2017 at 4:03:52 PM UTC"),
+            (_now, True, True, None, u"October 23, 2017, 4:03:52\u202fPM UTC"),
             (
                 _now,
                 True,
diff --git a/ckan/tests/lib/test_helpers.py b/ckan/tests/lib/test_helpers.py
--- a/ckan/tests/lib/test_helpers.py
+++ b/ckan/tests/lib/test_helpers.py
@@ -495,7 +495,7 @@ def test_named_timezone(self):
         (
             datetime.datetime(2008, 4, 13, 20, 40, 59, 123456),
             {"with_seconds": True},
-            "April 13, 2008 at 8:40:59 PM UTC",
+            "April 13, 2008, 8:40:59\u202fPM UTC",
         ),
         ("2008-04-13T20:40:20.123456", {}, "April 13, 2008"),
         (None, {}, ""),
diff --git a/ckanext/activity/tests/test_helpers.py b/ckanext/activity/tests/test_helpers.py
--- a/ckanext/activity/tests/test_helpers.py
+++ b/ckanext/activity/tests/test_helpers.py
@@ -21,8 +21,7 @@ def test_simple(self):
         html = out[0]
         assert (
             str(html)
-            == '<option value="id1" >February 1, 2018 at 10:58:59 AM UTC'
-            "</option>"
+            == '<option value="id1" >February 1, 2018, 10:58:59\u202fAM UTC</option>'
         )
         assert hasattr(html, "__html__")  # shows it is safe Markup
 
@@ -37,9 +36,7 @@ def test_selected(self):
         html = out[0]
         assert (
             str(html)
-            == '<option value="id1" selected>February 1, 2018 at 10:58:59'
-            " AM UTC"
-            "</option>"
+            == '<option value="id1" selected>February 1, 2018, 10:58:59\u202fAM UTC</option>'
         )
         assert hasattr(html, "__html__")  # shows it is safe Markup