Fix day filter
[cds-indico.git] / indico / util / i18n.py
blobc24b97069f754ebbcbce680c4b375ad9eea31915
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 import ast
18 import traceback
19 import re
20 import textwrap
21 import warnings
22 from contextlib import contextmanager
24 from babel import negotiate_locale
25 from babel.core import Locale, LOCALE_ALIASES
26 from babel.messages.pofile import read_po
27 from babel.support import Translations, NullTranslations
28 from flask import session, request, has_request_context, current_app, has_app_context
29 from flask_babelex import Babel, get_domain, Domain
30 from flask_pluginengine import current_plugin
31 from speaklater import is_lazy_string, make_lazy_string
33 from indico.core.config import Config
34 from indico.core.db import DBMgr
36 from MaKaC.common.info import HelperMaKaCInfo
39 LOCALE_ALIASES = dict(LOCALE_ALIASES, en='en_GB')
40 RE_TR_FUNCTION = re.compile(r'''_\("([^"]*)"\)|_\('([^']*)'\)''', re.DOTALL | re.MULTILINE)
42 babel = Babel()
43 _use_context = object()
46 def get_translation_domain(plugin_name=_use_context):
47 """Get the translation domain for the given plugin
49 If `plugin_name` is omitted, the plugin will be taken from current_plugin.
50 If `plugin_name` is None, the core translation domain ('indico') will be used.
51 """
52 if plugin_name is None:
53 return get_domain()
54 else:
55 plugin = None
56 if has_app_context():
57 from indico.core.plugins import plugin_engine
58 plugin = plugin_engine.get_plugin(plugin_name) if plugin_name is not _use_context else current_plugin
59 if plugin:
60 return plugin.translation_domain
61 else:
62 return get_domain()
65 def gettext_unicode(*args, **kwargs):
66 func_name = kwargs.pop('func_name', 'ugettext')
67 plugin_name = kwargs.pop('plugin_name', None)
69 if not isinstance(args[0], unicode):
70 args = [(text.decode('utf-8') if isinstance(text, str) else text) for text in args]
71 using_unicode = False
72 else:
73 using_unicode = True
75 translations = get_translation_domain(plugin_name).get_translations()
76 res = getattr(translations, func_name)(*args, **kwargs)
77 if not using_unicode:
78 res = res.encode('utf-8')
79 return res
82 def lazy_gettext(string, plugin_name=None):
83 if is_lazy_string(string):
84 return string
85 return make_lazy_string(gettext_unicode, string, plugin_name=plugin_name)
88 def smart_func(func_name, plugin_name=None):
89 def _wrap(*args, **kwargs):
90 """
91 Returns either a translated string or a lazy-translatable object,
92 depending on whether there is a session language or not (respectively)
93 """
94 if (has_request_context() and session.lang) or func_name != 'ugettext':
95 # straight translation
96 return gettext_unicode(*args, func_name=func_name, plugin_name=plugin_name, **kwargs)
98 else:
99 # otherwise, defer translation to eval time
100 return lazy_gettext(*args, plugin_name=plugin_name)
101 if plugin_name is _use_context:
102 _wrap.__name__ = '<smart {}>'.format(func_name)
103 else:
104 _wrap.__name__ = '<smart {} bound to {}>'.format(func_name, plugin_name or 'indico')
105 return _wrap
108 def make_bound_gettext(plugin_name):
109 """Creates a smart gettext callable bound to the domain of the specified plugin"""
110 return smart_func('ugettext', plugin_name=plugin_name)
113 def make_bound_ngettext(plugin_name):
114 """Creates a smart ngettext callable bound to the domain of the specified plugin"""
115 return smart_func('ungettext', plugin_name=plugin_name)
118 # Shortcuts
119 _ = ugettext = gettext = make_bound_gettext(None)
120 ungettext = ngettext = make_bound_ngettext(None)
121 L_ = lazy_gettext
123 # Plugin-context-sensitive gettext
124 gettext_context = make_bound_gettext(_use_context)
125 ngettext_context = make_bound_ngettext(_use_context)
127 # Just a marker for message extraction
128 N_ = lambda text: text
131 class NullDomain(Domain):
132 """A `Domain` that doesn't contain any translations"""
134 def __init__(self):
135 super(NullDomain, self).__init__()
136 self.null = NullTranslations()
138 def get_translations(self):
139 return self.null
142 class IndicoLocale(Locale):
144 Extends the Babel Locale class with some utility methods
147 def weekday(self, daynum, short=True):
149 Returns the week day given the index
151 return self.days['format']['abbreviated' if short else 'wide'][daynum].encode('utf-8')
154 class IndicoTranslations(Translations):
156 Routes translations through the 'smart' translators defined above
159 def _check_stack(self):
160 stack = traceback.extract_stack()
161 # [..., the caller we are looking for, ugettext/ungettext, this function]
162 frame = stack[-3]
163 frame_msg = textwrap.dedent("""
164 File "{}", line {}, in {}
166 """).strip().format(*frame)
167 msg = ('Using the gettext function (`_`) patched into the builtins is disallowed.\n'
168 'Please import it from `indico.util.i18n` instead.\n'
169 'The offending code was found in this location:\n{}').format(frame_msg)
170 if 'MaKaC/' in frame[0]:
171 # legacy code gets off with a warning
172 warnings.warn(msg, RuntimeWarning)
173 else:
174 raise RuntimeError(msg)
176 def ugettext(self, message):
177 if Config.getInstance().getDebug():
178 self._check_stack()
179 return gettext(message)
181 def ungettext(self, msgid1, msgid2, n):
182 if Config.getInstance().getDebug():
183 self._check_stack()
184 return ngettext(msgid1, msgid2, n)
187 IndicoTranslations().install(unicode=True)
190 @babel.localeselector
191 def set_best_lang():
193 Get the best language/locale for the current user. This means that first
194 the session will be checked, and then in the absence of an explicitly-set
195 language, we will try to guess it from the browser settings and only
196 after that fall back to the server's default.
199 if not has_request_context():
200 return 'en_GB' if current_app.config['TESTING'] else HelperMaKaCInfo.getMaKaCInfoInstance().getLang()
201 elif session.lang is not None:
202 return session.lang
204 # try to use browser language
205 preferred = [x.replace('-', '_') for x in request.accept_languages.values()]
206 resolved_lang = negotiate_locale(preferred, list(get_all_locales()), aliases=LOCALE_ALIASES)
208 if not resolved_lang:
209 if current_app.config['TESTING']:
210 return 'en_GB'
212 with DBMgr.getInstance().global_connection():
213 # fall back to server default
214 minfo = HelperMaKaCInfo.getMaKaCInfoInstance()
215 resolved_lang = minfo.getLang()
217 session.lang = resolved_lang
218 return resolved_lang
221 def get_current_locale():
222 return IndicoLocale.parse(set_best_lang())
225 def get_all_locales():
227 List all available locales/names e.g. {'pt_PT': 'Portuguese'}
229 if babel.app is None:
230 return {}
231 else:
232 return {str(t): t.language_name.title() for t in babel.list_translations()}
235 def set_session_lang(lang):
237 Set the current language in the current request context
239 session.lang = lang
242 @contextmanager
243 def session_language(lang):
245 Context manager that temporarily sets session language
247 old_lang = session.lang
249 set_session_lang(lang)
250 yield
251 set_session_lang(old_lang)
254 def parse_locale(locale):
256 Get a Locale object from a locale id
258 return IndicoLocale.parse(locale)
261 def i18nformat(text):
263 ATTENTION: only used for backward-compatibility
264 Parses old '_() inside strings hack', translating as needed
267 # this is a bit of a dirty hack, but cannot risk emitting lazy proxies here
268 if not session.lang:
269 set_best_lang()
271 return RE_TR_FUNCTION.sub(lambda x: _(next(y for y in x.groups() if y is not None)), text)
274 def extract(fileobj, keywords, commentTags, options):
276 Babel mini-extractor for old-style _() inside strings
278 astree = ast.parse(fileobj.read())
279 return extract_node(astree, keywords, commentTags, options)
282 def extract_node(node, keywords, commentTags, options, parents=[None]):
283 if isinstance(node, ast.Str) and isinstance(parents[-1], (ast.Assign, ast.Call)):
284 matches = RE_TR_FUNCTION.findall(node.s)
285 for m in matches:
286 line = m[0] or m[1]
287 yield (node.lineno, '', line.split('\n'), ['old style recursive strings'])
288 else:
289 for cnode in ast.iter_child_nodes(node):
290 for rslt in extract_node(cnode, keywords, commentTags, options, parents=(parents + [node])):
291 yield rslt
294 def po_to_json(po_file, locale=None, domain=None):
296 Converts *.po file to a json-like data structure
298 with open(po_file, 'rb') as f:
299 po_data = read_po(f, locale=locale, domain=domain)
301 messages = dict((message.id[0], message.string) if message.pluralizable else (message.id, [message.string])
302 for message in po_data)
304 messages[''] = {
305 'domain': po_data.domain,
306 'lang': str(po_data.locale),
307 'plural_forms': po_data.plural_forms
310 return {
311 (po_data.domain or ''): messages