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/>.
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
)
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.
52 if plugin_name
is None:
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
60 return plugin
.translation_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
]
75 translations
= get_translation_domain(plugin_name
).get_translations()
76 res
= getattr(translations
, func_name
)(*args
, **kwargs
)
78 res
= res
.encode('utf-8')
82 def lazy_gettext(string
, plugin_name
=None):
83 if is_lazy_string(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
):
91 Returns either a translated string or a lazy-translatable object,
92 depending on whether there is a session language or not (respectively)
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
)
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
)
104 _wrap
.__name
__ = '<smart {} bound to {}>'.format(func_name
, plugin_name
or 'indico')
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
)
119 _
= ugettext
= gettext
= make_bound_gettext(None)
120 ungettext
= ngettext
= make_bound_ngettext(None)
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"""
135 super(NullDomain
, self
).__init
__()
136 self
.null
= NullTranslations()
138 def get_translations(self
):
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]
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)
174 raise RuntimeError(msg
)
176 def ugettext(self
, message
):
177 if Config
.getInstance().getDebug():
179 return gettext(message
)
181 def ungettext(self
, msgid1
, msgid2
, n
):
182 if Config
.getInstance().getDebug():
184 return ngettext(msgid1
, msgid2
, n
)
187 IndicoTranslations().install(unicode=True)
190 @babel.localeselector
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:
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']:
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
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:
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
243 def session_language(lang
):
245 Context manager that temporarily sets session language
247 old_lang
= session
.lang
249 set_session_lang(lang
)
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
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
)
287 yield (node
.lineno
, '', line
.split('\n'), ['old style recursive strings'])
289 for cnode
in ast
.iter_child_nodes(node
):
290 for rslt
in extract_node(cnode
, keywords
, commentTags
, options
, parents
=(parents
+ [node
])):
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
)
305 'domain': po_data
.domain
,
306 'lang': str(po_data
.locale
),
307 'plural_forms': po_data
.plural_forms
311 (po_data
.domain
or ''): messages