We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Chinese (zh_CN and zh_TW) can not be used on a source install
CKAN Version if known: master

Setting up `ckan.locale_default = zh_CN` fails with `AssertionError: default language "zh_CN" not available`.

`zh_CN` and `zh_TW` are the locale codes used on Transifex.

[This check](https://github.com/ckan/ckan/blob/master/ckan/lib/i18n.py#L51:L53) fails. The folder where babel keeps the locale data files (`<virtualenv>/lib/python2.7/site-packages/babel/locale-data`) has different codes for the Chinese locales. Bizarrely this works on the package install (2.5.x), ie the `zh_CN.dat` files are there.

Not sure if it's related to the recent upgrade to Babel (#3004) or something specific with the package install (seems unlikely)

Probably related to https://github.com/python-babel/babel/pull/25


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ckan/lib/i18n.py`
Content:
```
1 # encoding: utf-8
2 
3 import os
4 
5 from babel import Locale, localedata
6 from babel.core import LOCALE_ALIASES, get_locale_identifier
7 from babel.support import Translations
8 from paste.deploy.converters import aslist
9 from pylons import config
10 from pylons import i18n
11 import pylons
12 
13 import ckan.i18n
14 from ckan.plugins import PluginImplementations
15 from ckan.plugins.interfaces import ITranslation
16 
17 # Default Portuguese language to Brazilian territory, since
18 # we don't have a Portuguese territory translation currently.
19 LOCALE_ALIASES['pt'] = 'pt_BR'
20 
21 
22 def get_locales_from_config():
23     ''' despite the name of this function it gets the locales defined by
24     the config AND also the locals available subject to the config. '''
25     locales_offered = config.get('ckan.locales_offered', '').split()
26     filtered_out = config.get('ckan.locales_filtered_out', '').split()
27     locale_default = config.get('ckan.locale_default', 'en')
28     locale_order = config.get('ckan.locale_order', '').split()
29     known_locales = get_locales()
30     all_locales = (set(known_locales) |
31                    set(locales_offered) |
32                    set(locale_order) |
33                    set(locale_default))
34     all_locales -= set(filtered_out)
35     return all_locales
36 
37 
38 def _get_locales():
39     # FIXME this wants cleaning up and merging with get_locales_from_config()
40     assert not config.get('lang'), \
41         ('"lang" config option not supported - please use ckan.locale_default '
42          'instead.')
43     locales_offered = config.get('ckan.locales_offered', '').split()
44     filtered_out = config.get('ckan.locales_filtered_out', '').split()
45     locale_default = config.get('ckan.locale_default', 'en')
46     locale_order = config.get('ckan.locale_order', '').split()
47 
48     locales = ['en']
49     if config.get('ckan.i18n_directory'):
50         i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n')
51     else:
52         i18n_path = os.path.dirname(ckan.i18n.__file__)
53     locales += [l for l in os.listdir(i18n_path) if localedata.exists(l)]
54 
55     assert locale_default in locales, \
56         'default language "%s" not available' % locale_default
57 
58     locale_list = []
59     for locale in locales:
60         # no duplicates
61         if locale in locale_list:
62             continue
63         # if offered locales then check locale is offered
64         if locales_offered and locale not in locales_offered:
65             continue
66         # remove if filtered out
67         if locale in filtered_out:
68             continue
69         # ignore the default as it will be added first
70         if locale == locale_default:
71             continue
72         locale_list.append(locale)
73     # order the list if specified
74     ordered_list = [locale_default]
75     for locale in locale_order:
76         if locale in locale_list:
77             ordered_list.append(locale)
78             # added so remove from our list
79             locale_list.remove(locale)
80     # add any remaining locales not ordered
81     ordered_list += locale_list
82 
83     return ordered_list
84 
85 available_locales = None
86 locales = None
87 locales_dict = None
88 _non_translated_locals = None
89 
90 
91 def get_locales():
92     ''' Get list of available locales
93     e.g. [ 'en', 'de', ... ]
94     '''
95     global locales
96     if not locales:
97         locales = _get_locales()
98     return locales
99 
100 
101 def non_translated_locals():
102     ''' These are the locales that are available but for which there are
103     no translations. returns a list like ['en', 'de', ...] '''
104     global _non_translated_locals
105     if not _non_translated_locals:
106         locales = config.get('ckan.locale_order', '').split()
107         _non_translated_locals = [x for x in locales if x not in get_locales()]
108     return _non_translated_locals
109 
110 
111 def get_locales_dict():
112     ''' Get a dict of the available locales
113     e.g.  { 'en' : Locale('en'), 'de' : Locale('de'), ... } '''
114     global locales_dict
115     if not locales_dict:
116         locales = _get_locales()
117         locales_dict = {}
118         for locale in locales:
119             locales_dict[str(locale)] = Locale.parse(locale)
120     return locales_dict
121 
122 
123 def get_available_locales():
124     ''' Get a list of the available locales
125     e.g.  [ Locale('en'), Locale('de'), ... ] '''
126     global available_locales
127     if not available_locales:
128         available_locales = map(Locale.parse, get_locales())
129     # Add the full identifier (eg `pt_BR`) to the locale classes, as it does
130     # not offer a way of accessing it directly
131     for locale in available_locales:
132         setattr(locale, 'identifier', get_identifier_from_locale_class(locale))
133     return available_locales
134 
135 
136 def get_identifier_from_locale_class(locale):
137     return get_locale_identifier(
138         (locale.language,
139          locale.territory,
140          locale.script,
141          locale.variant))
142 
143 
144 def _set_lang(lang):
145     ''' Allows a custom i18n directory to be specified.
146     Creates a fake config file to pass to pylons.i18n.set_lang, which
147     sets the Pylons root path to desired i18n_directory.
148     This is needed as Pylons will only look for an i18n directory in
149     the application root.'''
150     if config.get('ckan.i18n_directory'):
151         fake_config = {'pylons.paths': {'root': config['ckan.i18n_directory']},
152                        'pylons.package': config['pylons.package']}
153         i18n.set_lang(lang, pylons_config=fake_config, class_=Translations)
154     else:
155         i18n.set_lang(lang, class_=Translations)
156 
157 
158 def handle_request(request, tmpl_context):
159     ''' Set the language for the request '''
160     lang = request.environ.get('CKAN_LANG') or \
161         config.get('ckan.locale_default', 'en')
162     if lang != 'en':
163         set_lang(lang)
164 
165     for plugin in PluginImplementations(ITranslation):
166         if lang in plugin.i18n_locales():
167             _add_extra_translations(plugin.i18n_directory(), lang,
168                                     plugin.i18n_domain())
169 
170     extra_directory = config.get('ckan.i18n.extra_directory')
171     extra_domain = config.get('ckan.i18n.extra_gettext_domain')
172     extra_locales = aslist(config.get('ckan.i18n.extra_locales'))
173     if extra_directory and extra_domain and extra_locales:
174         if lang in extra_locales:
175             _add_extra_translations(extra_directory, lang, extra_domain)
176 
177     tmpl_context.language = lang
178     return lang
179 
180 
181 def _add_extra_translations(dirname, locales, domain):
182     translator = Translations.load(dirname=dirname, locales=locales,
183                                    domain=domain)
184     try:
185         pylons.translator.merge(translator)
186     except AttributeError:
187         # this occurs when an extension has 'en' translations that
188         # replace the default strings. As set_lang has not been run,
189         # pylons.translation is the NullTranslation, so we have to
190         # replace the StackedObjectProxy ourselves manually.
191         environ = pylons.request.environ
192         environ['pylons.pylons'].translator = translator
193         if 'paste.registry' in environ:
194             environ['paste.registry'].replace(pylons.translator,
195                                               translator)
196 
197 
198 def get_lang():
199     ''' Returns the current language. Based on babel.i18n.get_lang but
200     works when set_lang has not been run (i.e. still in English). '''
201     langs = i18n.get_lang()
202     if langs:
203         return langs[0]
204     else:
205         return 'en'
206 
207 
208 def set_lang(language_code):
209     ''' Wrapper to pylons call '''
210     if language_code in non_translated_locals():
211         language_code = config.get('ckan.locale_default', 'en')
212     if language_code != 'en':
213         _set_lang(language_code)
214
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py
--- a/ckan/lib/i18n.py
+++ b/ckan/lib/i18n.py
@@ -1,7 +1,9 @@
 import os
 
-from babel import Locale, localedata
-from babel.core import LOCALE_ALIASES, get_locale_identifier
+from babel import Locale
+from babel.core import (LOCALE_ALIASES,
+                        get_locale_identifier,
+                        UnknownLocaleError)
 from babel.support import Translations
 from paste.deploy.converters import aslist
 from pylons import config
@@ -48,7 +50,18 @@
         i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n')
     else:
         i18n_path = os.path.dirname(ckan.i18n.__file__)
-    locales += [l for l in os.listdir(i18n_path) if localedata.exists(l)]
+
+    # For every file in the ckan i18n directory see if babel can understand
+    # the locale. If yes, add it to the available locales
+    for locale in os.listdir(i18n_path):
+        try:
+            Locale.parse(locale)
+            locales.append(locale)
+        except (ValueError, UnknownLocaleError):
+            # Babel does not know how to make a locale out of this.
+            # This is fine since we are passing all files in the
+            # ckan.i18n_directory here which e.g. includes the __init__.py
+            pass
 
     assert locale_default in locales, \
         'default language "%s" not available' % locale_default
@@ -123,11 +136,21 @@
     e.g.  [ Locale('en'), Locale('de'), ... ] '''
     global available_locales
     if not available_locales:
-        available_locales = map(Locale.parse, get_locales())
-    # Add the full identifier (eg `pt_BR`) to the locale classes, as it does
-    # not offer a way of accessing it directly
-    for locale in available_locales:
-        setattr(locale, 'identifier', get_identifier_from_locale_class(locale))
+        available_locales = []
+        for locale in get_locales():
+            # Add the short names for the locales. This equals the filename
+            # of the ckan translation files as opposed to the long name
+            # that includes the script which is generated by babel
+            # so e.g. `zn_CH` instead of `zn_Hans_CH` this is needed
+            # to properly construct urls with url_for
+            parsed_locale = Locale.parse(locale)
+            parsed_locale.short_name = locale
+
+            # Add the full identifier (eg `pt_BR`) to the locale classes,
+            # as it does not offer a way of accessing it directly
+            parsed_locale.identifier = \
+                get_identifier_from_locale_class(parsed_locale)
+            available_locales.append(parsed_locale)
     return available_locales

{"golden_diff": "diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py\n--- a/ckan/lib/i18n.py\n+++ b/ckan/lib/i18n.py\n@@ -1,7 +1,9 @@\n import os\n \n-from babel import Locale, localedata\n-from babel.core import LOCALE_ALIASES, get_locale_identifier\n+from babel import Locale\n+from babel.core import (LOCALE_ALIASES,\n+                        get_locale_identifier,\n+                        UnknownLocaleError)\n from babel.support import Translations\n from paste.deploy.converters import aslist\n from pylons import config\n@@ -48,7 +50,18 @@\n         i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n')\n     else:\n         i18n_path = os.path.dirname(ckan.i18n.__file__)\n-    locales += [l for l in os.listdir(i18n_path) if localedata.exists(l)]\n+\n+    # For every file in the ckan i18n directory see if babel can understand\n+    # the locale. If yes, add it to the available locales\n+    for locale in os.listdir(i18n_path):\n+        try:\n+            Locale.parse(locale)\n+            locales.append(locale)\n+        except (ValueError, UnknownLocaleError):\n+            # Babel does not know how to make a locale out of this.\n+            # This is fine since we are passing all files in the\n+            # ckan.i18n_directory here which e.g. includes the __init__.py\n+            pass\n \n     assert locale_default in locales, \\\n         'default language \"%s\" not available' % locale_default\n@@ -123,11 +136,21 @@\n     e.g.  [ Locale('en'), Locale('de'), ... ] '''\n     global available_locales\n     if not available_locales:\n-        available_locales = map(Locale.parse, get_locales())\n-    # Add the full identifier (eg `pt_BR`) to the locale classes, as it does\n-    # not offer a way of accessing it directly\n-    for locale in available_locales:\n-        setattr(locale, 'identifier', get_identifier_from_locale_class(locale))\n+        available_locales = []\n+        for locale in get_locales():\n+            # Add the short names for the locales. This equals the filename\n+            # of the ckan translation files as opposed to the long name\n+            # that includes the script which is generated by babel\n+            # so e.g. `zn_CH` instead of `zn_Hans_CH` this is needed\n+            # to properly construct urls with url_for\n+            parsed_locale = Locale.parse(locale)\n+            parsed_locale.short_name = locale\n+\n+            # Add the full identifier (eg `pt_BR`) to the locale classes,\n+            # as it does not offer a way of accessing it directly\n+            parsed_locale.identifier = \\\n+                get_identifier_from_locale_class(parsed_locale)\n+            available_locales.append(parsed_locale)\n     return available_locales\n", "issue": "Chinese (zh_CN and zh_TW) can not be used on a source install\nCKAN Version if known: master\n\nSetting up `ckan.locale_default = zh_CN` fails with `AssertionError: default language \"zh_CN\" not available`.\n\n`zh_CN` and `zh_TW` are the locale codes used on Transifex.\n\n[This check](https://github.com/ckan/ckan/blob/master/ckan/lib/i18n.py#L51:L53) fails. The folder where babel keeps the locale data files (`<virtualenv>/lib/python2.7/site-packages/babel/locale-data`) has different codes for the Chinese locales. Bizarrely this works on the package install (2.5.x), ie the `zh_CN.dat` files are there.\n\nNot sure if it's related to the recent upgrade to Babel (#3004) or something specific with the package install (seems unlikely)\n\nProbably related to https://github.com/python-babel/babel/pull/25\n\n", "before_files": [{"content": "# encoding: utf-8\n\nimport os\n\nfrom babel import Locale, localedata\nfrom babel.core import LOCALE_ALIASES, get_locale_identifier\nfrom babel.support import Translations\nfrom paste.deploy.converters import aslist\nfrom pylons import config\nfrom pylons import i18n\nimport pylons\n\nimport ckan.i18n\nfrom ckan.plugins import PluginImplementations\nfrom ckan.plugins.interfaces import ITranslation\n\n# Default Portuguese language to Brazilian territory, since\n# we don't have a Portuguese territory translation currently.\nLOCALE_ALIASES['pt'] = 'pt_BR'\n\n\ndef get_locales_from_config():\n    ''' despite the name of this function it gets the locales defined by\n    the config AND also the locals available subject to the config. '''\n    locales_offered = config.get('ckan.locales_offered', '').split()\n    filtered_out = config.get('ckan.locales_filtered_out', '').split()\n    locale_default = config.get('ckan.locale_default', 'en')\n    locale_order = config.get('ckan.locale_order', '').split()\n    known_locales = get_locales()\n    all_locales = (set(known_locales) |\n                   set(locales_offered) |\n                   set(locale_order) |\n                   set(locale_default))\n    all_locales -= set(filtered_out)\n    return all_locales\n\n\ndef _get_locales():\n    # FIXME this wants cleaning up and merging with get_locales_from_config()\n    assert not config.get('lang'), \\\n        ('\"lang\" config option not supported - please use ckan.locale_default '\n         'instead.')\n    locales_offered = config.get('ckan.locales_offered', '').split()\n    filtered_out = config.get('ckan.locales_filtered_out', '').split()\n    locale_default = config.get('ckan.locale_default', 'en')\n    locale_order = config.get('ckan.locale_order', '').split()\n\n    locales = ['en']\n    if config.get('ckan.i18n_directory'):\n        i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n')\n    else:\n        i18n_path = os.path.dirname(ckan.i18n.__file__)\n    locales += [l for l in os.listdir(i18n_path) if localedata.exists(l)]\n\n    assert locale_default in locales, \\\n        'default language \"%s\" not available' % locale_default\n\n    locale_list = []\n    for locale in locales:\n        # no duplicates\n        if locale in locale_list:\n            continue\n        # if offered locales then check locale is offered\n        if locales_offered and locale not in locales_offered:\n            continue\n        # remove if filtered out\n        if locale in filtered_out:\n            continue\n        # ignore the default as it will be added first\n        if locale == locale_default:\n            continue\n        locale_list.append(locale)\n    # order the list if specified\n    ordered_list = [locale_default]\n    for locale in locale_order:\n        if locale in locale_list:\n            ordered_list.append(locale)\n            # added so remove from our list\n            locale_list.remove(locale)\n    # add any remaining locales not ordered\n    ordered_list += locale_list\n\n    return ordered_list\n\navailable_locales = None\nlocales = None\nlocales_dict = None\n_non_translated_locals = None\n\n\ndef get_locales():\n    ''' Get list of available locales\n    e.g. [ 'en', 'de', ... ]\n    '''\n    global locales\n    if not locales:\n        locales = _get_locales()\n    return locales\n\n\ndef non_translated_locals():\n    ''' These are the locales that are available but for which there are\n    no translations. returns a list like ['en', 'de', ...] '''\n    global _non_translated_locals\n    if not _non_translated_locals:\n        locales = config.get('ckan.locale_order', '').split()\n        _non_translated_locals = [x for x in locales if x not in get_locales()]\n    return _non_translated_locals\n\n\ndef get_locales_dict():\n    ''' Get a dict of the available locales\n    e.g.  { 'en' : Locale('en'), 'de' : Locale('de'), ... } '''\n    global locales_dict\n    if not locales_dict:\n        locales = _get_locales()\n        locales_dict = {}\n        for locale in locales:\n            locales_dict[str(locale)] = Locale.parse(locale)\n    return locales_dict\n\n\ndef get_available_locales():\n    ''' Get a list of the available locales\n    e.g.  [ Locale('en'), Locale('de'), ... ] '''\n    global available_locales\n    if not available_locales:\n        available_locales = map(Locale.parse, get_locales())\n    # Add the full identifier (eg `pt_BR`) to the locale classes, as it does\n    # not offer a way of accessing it directly\n    for locale in available_locales:\n        setattr(locale, 'identifier', get_identifier_from_locale_class(locale))\n    return available_locales\n\n\ndef get_identifier_from_locale_class(locale):\n    return get_locale_identifier(\n        (locale.language,\n         locale.territory,\n         locale.script,\n         locale.variant))\n\n\ndef _set_lang(lang):\n    ''' Allows a custom i18n directory to be specified.\n    Creates a fake config file to pass to pylons.i18n.set_lang, which\n    sets the Pylons root path to desired i18n_directory.\n    This is needed as Pylons will only look for an i18n directory in\n    the application root.'''\n    if config.get('ckan.i18n_directory'):\n        fake_config = {'pylons.paths': {'root': config['ckan.i18n_directory']},\n                       'pylons.package': config['pylons.package']}\n        i18n.set_lang(lang, pylons_config=fake_config, class_=Translations)\n    else:\n        i18n.set_lang(lang, class_=Translations)\n\n\ndef handle_request(request, tmpl_context):\n    ''' Set the language for the request '''\n    lang = request.environ.get('CKAN_LANG') or \\\n        config.get('ckan.locale_default', 'en')\n    if lang != 'en':\n        set_lang(lang)\n\n    for plugin in PluginImplementations(ITranslation):\n        if lang in plugin.i18n_locales():\n            _add_extra_translations(plugin.i18n_directory(), lang,\n                                    plugin.i18n_domain())\n\n    extra_directory = config.get('ckan.i18n.extra_directory')\n    extra_domain = config.get('ckan.i18n.extra_gettext_domain')\n    extra_locales = aslist(config.get('ckan.i18n.extra_locales'))\n    if extra_directory and extra_domain and extra_locales:\n        if lang in extra_locales:\n            _add_extra_translations(extra_directory, lang, extra_domain)\n\n    tmpl_context.language = lang\n    return lang\n\n\ndef _add_extra_translations(dirname, locales, domain):\n    translator = Translations.load(dirname=dirname, locales=locales,\n                                   domain=domain)\n    try:\n        pylons.translator.merge(translator)\n    except AttributeError:\n        # this occurs when an extension has 'en' translations that\n        # replace the default strings. As set_lang has not been run,\n        # pylons.translation is the NullTranslation, so we have to\n        # replace the StackedObjectProxy ourselves manually.\n        environ = pylons.request.environ\n        environ['pylons.pylons'].translator = translator\n        if 'paste.registry' in environ:\n            environ['paste.registry'].replace(pylons.translator,\n                                              translator)\n\n\ndef get_lang():\n    ''' Returns the current language. Based on babel.i18n.get_lang but\n    works when set_lang has not been run (i.e. still in English). '''\n    langs = i18n.get_lang()\n    if langs:\n        return langs[0]\n    else:\n        return 'en'\n\n\ndef set_lang(language_code):\n    ''' Wrapper to pylons call '''\n    if language_code in non_translated_locals():\n        language_code = config.get('ckan.locale_default', 'en')\n    if language_code != 'en':\n        _set_lang(language_code)\n", "path": "ckan/lib/i18n.py"}], "after_files": [{"content": "import os\n\nfrom babel import Locale\nfrom babel.core import (LOCALE_ALIASES,\n                        get_locale_identifier,\n                        UnknownLocaleError)\nfrom babel.support import Translations\nfrom paste.deploy.converters import aslist\nfrom pylons import config\nfrom pylons import i18n\nimport pylons\n\nimport ckan.i18n\nfrom ckan.plugins import PluginImplementations\nfrom ckan.plugins.interfaces import ITranslation\n\n# Default Portuguese language to Brazilian territory, since\n# we don't have a Portuguese territory translation currently.\nLOCALE_ALIASES['pt'] = 'pt_BR'\n\n\ndef get_locales_from_config():\n    ''' despite the name of this function it gets the locales defined by\n    the config AND also the locals available subject to the config. '''\n    locales_offered = config.get('ckan.locales_offered', '').split()\n    filtered_out = config.get('ckan.locales_filtered_out', '').split()\n    locale_default = config.get('ckan.locale_default', 'en')\n    locale_order = config.get('ckan.locale_order', '').split()\n    known_locales = get_locales()\n    all_locales = (set(known_locales) |\n                   set(locales_offered) |\n                   set(locale_order) |\n                   set(locale_default))\n    all_locales -= set(filtered_out)\n    return all_locales\n\n\ndef _get_locales():\n    # FIXME this wants cleaning up and merging with get_locales_from_config()\n    assert not config.get('lang'), \\\n        ('\"lang\" config option not supported - please use ckan.locale_default '\n         'instead.')\n    locales_offered = config.get('ckan.locales_offered', '').split()\n    filtered_out = config.get('ckan.locales_filtered_out', '').split()\n    locale_default = config.get('ckan.locale_default', 'en')\n    locale_order = config.get('ckan.locale_order', '').split()\n\n    locales = ['en']\n    if config.get('ckan.i18n_directory'):\n        i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n')\n    else:\n        i18n_path = os.path.dirname(ckan.i18n.__file__)\n\n    # For every file in the ckan i18n directory see if babel can understand\n    # the locale. If yes, add it to the available locales\n    for locale in os.listdir(i18n_path):\n        try:\n            Locale.parse(locale)\n            locales.append(locale)\n        except (ValueError, UnknownLocaleError):\n            # Babel does not know how to make a locale out of this.\n            # This is fine since we are passing all files in the\n            # ckan.i18n_directory here which e.g. includes the __init__.py\n            pass\n\n    assert locale_default in locales, \\\n        'default language \"%s\" not available' % locale_default\n\n    locale_list = []\n    for locale in locales:\n        # no duplicates\n        if locale in locale_list:\n            continue\n        # if offered locales then check locale is offered\n        if locales_offered and locale not in locales_offered:\n            continue\n        # remove if filtered out\n        if locale in filtered_out:\n            continue\n        # ignore the default as it will be added first\n        if locale == locale_default:\n            continue\n        locale_list.append(locale)\n    # order the list if specified\n    ordered_list = [locale_default]\n    for locale in locale_order:\n        if locale in locale_list:\n            ordered_list.append(locale)\n            # added so remove from our list\n            locale_list.remove(locale)\n    # add any remaining locales not ordered\n    ordered_list += locale_list\n\n    return ordered_list\n\navailable_locales = None\nlocales = None\nlocales_dict = None\n_non_translated_locals = None\n\n\ndef get_locales():\n    ''' Get list of available locales\n    e.g. [ 'en', 'de', ... ]\n    '''\n    global locales\n    if not locales:\n        locales = _get_locales()\n    return locales\n\n\ndef non_translated_locals():\n    ''' These are the locales that are available but for which there are\n    no translations. returns a list like ['en', 'de', ...] '''\n    global _non_translated_locals\n    if not _non_translated_locals:\n        locales = config.get('ckan.locale_order', '').split()\n        _non_translated_locals = [x for x in locales if x not in get_locales()]\n    return _non_translated_locals\n\n\ndef get_locales_dict():\n    ''' Get a dict of the available locales\n    e.g.  { 'en' : Locale('en'), 'de' : Locale('de'), ... } '''\n    global locales_dict\n    if not locales_dict:\n        locales = _get_locales()\n        locales_dict = {}\n        for locale in locales:\n            locales_dict[str(locale)] = Locale.parse(locale)\n    return locales_dict\n\n\ndef get_available_locales():\n    ''' Get a list of the available locales\n    e.g.  [ Locale('en'), Locale('de'), ... ] '''\n    global available_locales\n    if not available_locales:\n        available_locales = []\n        for locale in get_locales():\n            # Add the short names for the locales. This equals the filename\n            # of the ckan translation files as opposed to the long name\n            # that includes the script which is generated by babel\n            # so e.g. `zn_CH` instead of `zn_Hans_CH` this is needed\n            # to properly construct urls with url_for\n            parsed_locale = Locale.parse(locale)\n            parsed_locale.short_name = locale\n\n            # Add the full identifier (eg `pt_BR`) to the locale classes,\n            # as it does not offer a way of accessing it directly\n            parsed_locale.identifier = \\\n                get_identifier_from_locale_class(parsed_locale)\n            available_locales.append(parsed_locale)\n    return available_locales\n\n\ndef get_identifier_from_locale_class(locale):\n    return get_locale_identifier(\n        (locale.language,\n         locale.territory,\n         locale.script,\n         locale.variant))\n\n\ndef _set_lang(lang):\n    ''' Allows a custom i18n directory to be specified.\n    Creates a fake config file to pass to pylons.i18n.set_lang, which\n    sets the Pylons root path to desired i18n_directory.\n    This is needed as Pylons will only look for an i18n directory in\n    the application root.'''\n    if config.get('ckan.i18n_directory'):\n        fake_config = {'pylons.paths': {'root': config['ckan.i18n_directory']},\n                       'pylons.package': config['pylons.package']}\n        i18n.set_lang(lang, pylons_config=fake_config, class_=Translations)\n    else:\n        i18n.set_lang(lang, class_=Translations)\n\n\ndef handle_request(request, tmpl_context):\n    ''' Set the language for the request '''\n    lang = request.environ.get('CKAN_LANG') or \\\n        config.get('ckan.locale_default', 'en')\n    if lang != 'en':\n        set_lang(lang)\n\n    for plugin in PluginImplementations(ITranslation):\n        if lang in plugin.i18n_locales():\n            _add_extra_translations(plugin.i18n_directory(), lang,\n                                    plugin.i18n_domain())\n\n    extra_directory = config.get('ckan.i18n.extra_directory')\n    extra_domain = config.get('ckan.i18n.extra_gettext_domain')\n    extra_locales = aslist(config.get('ckan.i18n.extra_locales'))\n    if extra_directory and extra_domain and extra_locales:\n        if lang in extra_locales:\n            _add_extra_translations(extra_directory, lang, extra_domain)\n\n    tmpl_context.language = lang\n    return lang\n\n\ndef _add_extra_translations(dirname, locales, domain):\n    translator = Translations.load(dirname=dirname, locales=locales,\n                                   domain=domain)\n    try:\n        pylons.translator.merge(translator)\n    except AttributeError:\n        # this occurs when an extension has 'en' translations that\n        # replace the default strings. As set_lang has not been run,\n        # pylons.translation is the NullTranslation, so we have to\n        # replace the StackedObjectProxy ourselves manually.\n        environ = pylons.request.environ\n        environ['pylons.pylons'].translator = translator\n        if 'paste.registry' in environ:\n            environ['paste.registry'].replace(pylons.translator,\n                                              translator)\n\n\ndef get_lang():\n    ''' Returns the current language. Based on babel.i18n.get_lang but\n    works when set_lang has not been run (i.e. still in English). '''\n    langs = i18n.get_lang()\n    if langs:\n        return langs[0]\n    else:\n        return 'en'\n\n\ndef set_lang(language_code):\n    ''' Wrapper to pylons call '''\n    if language_code in non_translated_locals():\n        language_code = config.get('ckan.locale_default', 'en')\n    if language_code != 'en':\n        _set_lang(language_code)\n", "path": "ckan/lib/i18n.py"}]}