blob: 17f66f7c29c0816a96f7ece61407ab7e0cdd4f32 [file] [log] [blame]
Matthew Treinish0db53772013-07-26 10:39:35 -04001# Copyright 2012 Red Hat, Inc.
Matthew Treinish0db53772013-07-26 10:39:35 -04002# Copyright 2013 IBM Corp.
Matthew Treinishffa94d62013-09-11 18:09:17 +00003# All Rights Reserved.
Matthew Treinish0db53772013-07-26 10:39:35 -04004#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17"""
18gettext for openstack-common modules.
19
20Usual usage in an openstack.common module:
21
22 from tempest.openstack.common.gettextutils import _
23"""
24
25import copy
Matthew Treinish90ac9142014-03-17 14:58:37 +000026import functools
Matthew Treinish0db53772013-07-26 10:39:35 -040027import gettext
Matthew Treinish90ac9142014-03-17 14:58:37 +000028import locale
29from logging import handlers
Matthew Treinish0db53772013-07-26 10:39:35 -040030import os
Matthew Treinish0db53772013-07-26 10:39:35 -040031
Matthew Treinishffa94d62013-09-11 18:09:17 +000032from babel import localedata
Matthew Treinish0db53772013-07-26 10:39:35 -040033import six
34
35_localedir = os.environ.get('tempest'.upper() + '_LOCALEDIR')
36_t = gettext.translation('tempest', localedir=_localedir, fallback=True)
37
Matthew Treinish90ac9142014-03-17 14:58:37 +000038# We use separate translation catalogs for each log level, so set up a
39# mapping between the log level name and the translator. The domain
40# for the log level is project_name + "-log-" + log_level so messages
41# for each level end up in their own catalog.
42_t_log_levels = dict(
43 (level, gettext.translation('tempest' + '-log-' + level,
44 localedir=_localedir,
45 fallback=True))
46 for level in ['info', 'warning', 'error', 'critical']
47)
48
Matthew Treinishffa94d62013-09-11 18:09:17 +000049_AVAILABLE_LANGUAGES = {}
50USE_LAZY = False
51
52
53def enable_lazy():
54 """Convenience function for configuring _() to use lazy gettext
55
56 Call this at the start of execution to enable the gettextutils._
57 function to use lazy gettext functionality. This is useful if
58 your project is importing _ directly instead of using the
59 gettextutils.install() way of importing the _ function.
60 """
61 global USE_LAZY
62 USE_LAZY = True
63
Matthew Treinish0db53772013-07-26 10:39:35 -040064
65def _(msg):
Matthew Treinishffa94d62013-09-11 18:09:17 +000066 if USE_LAZY:
Matthew Treinish90ac9142014-03-17 14:58:37 +000067 return Message(msg, domain='tempest')
Matthew Treinishffa94d62013-09-11 18:09:17 +000068 else:
Matthew Treinishf45528a2013-10-24 20:12:28 +000069 if six.PY3:
70 return _t.gettext(msg)
Matthew Treinishffa94d62013-09-11 18:09:17 +000071 return _t.ugettext(msg)
Matthew Treinish0db53772013-07-26 10:39:35 -040072
73
Matthew Treinish90ac9142014-03-17 14:58:37 +000074def _log_translation(msg, level):
75 """Build a single translation of a log message
76 """
77 if USE_LAZY:
78 return Message(msg, domain='tempest' + '-log-' + level)
79 else:
80 translator = _t_log_levels[level]
81 if six.PY3:
82 return translator.gettext(msg)
83 return translator.ugettext(msg)
84
85# Translators for log levels.
86#
87# The abbreviated names are meant to reflect the usual use of a short
88# name like '_'. The "L" is for "log" and the other letter comes from
89# the level.
90_LI = functools.partial(_log_translation, level='info')
91_LW = functools.partial(_log_translation, level='warning')
92_LE = functools.partial(_log_translation, level='error')
93_LC = functools.partial(_log_translation, level='critical')
94
95
Matthew Treinishffa94d62013-09-11 18:09:17 +000096def install(domain, lazy=False):
Matthew Treinish0db53772013-07-26 10:39:35 -040097 """Install a _() function using the given translation domain.
98
99 Given a translation domain, install a _() function using gettext's
100 install() function.
101
102 The main difference from gettext.install() is that we allow
103 overriding the default localedir (e.g. /usr/share/locale) using
104 a translation-domain-specific environment variable (e.g.
105 NOVA_LOCALEDIR).
Matthew Treinishffa94d62013-09-11 18:09:17 +0000106
107 :param domain: the translation domain
108 :param lazy: indicates whether or not to install the lazy _() function.
109 The lazy _() introduces a way to do deferred translation
110 of messages by installing a _ that builds Message objects,
111 instead of strings, which can then be lazily translated into
112 any available locale.
Matthew Treinish0db53772013-07-26 10:39:35 -0400113 """
Matthew Treinishffa94d62013-09-11 18:09:17 +0000114 if lazy:
115 # NOTE(mrodden): Lazy gettext functionality.
116 #
117 # The following introduces a deferred way to do translations on
118 # messages in OpenStack. We override the standard _() function
119 # and % (format string) operation to build Message objects that can
120 # later be translated when we have more information.
Matthew Treinishffa94d62013-09-11 18:09:17 +0000121 def _lazy_gettext(msg):
122 """Create and return a Message object.
123
124 Lazy gettext function for a given domain, it is a factory method
125 for a project/module to get a lazy gettext function for its own
126 translation domain (i.e. nova, glance, cinder, etc.)
127
128 Message encapsulates a string so that we can translate
129 it later when needed.
130 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000131 return Message(msg, domain=domain)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000132
Matthew Treinishf45528a2013-10-24 20:12:28 +0000133 from six import moves
134 moves.builtins.__dict__['_'] = _lazy_gettext
Matthew Treinishffa94d62013-09-11 18:09:17 +0000135 else:
136 localedir = '%s_LOCALEDIR' % domain.upper()
Matthew Treinishf45528a2013-10-24 20:12:28 +0000137 if six.PY3:
138 gettext.install(domain,
139 localedir=os.environ.get(localedir))
140 else:
141 gettext.install(domain,
142 localedir=os.environ.get(localedir),
143 unicode=True)
Matthew Treinish0db53772013-07-26 10:39:35 -0400144
145
Matthew Treinish90ac9142014-03-17 14:58:37 +0000146class Message(six.text_type):
147 """A Message object is a unicode object that can be translated.
Matthew Treinish0db53772013-07-26 10:39:35 -0400148
Matthew Treinish90ac9142014-03-17 14:58:37 +0000149 Translation of Message is done explicitly using the translate() method.
150 For all non-translation intents and purposes, a Message is simply unicode,
151 and can be treated as such.
152 """
Matthew Treinish0db53772013-07-26 10:39:35 -0400153
Matthew Treinish90ac9142014-03-17 14:58:37 +0000154 def __new__(cls, msgid, msgtext=None, params=None,
155 domain='tempest', *args):
156 """Create a new Message object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400157
Matthew Treinish90ac9142014-03-17 14:58:37 +0000158 In order for translation to work gettext requires a message ID, this
159 msgid will be used as the base unicode text. It is also possible
160 for the msgid and the base unicode text to be different by passing
161 the msgtext parameter.
162 """
163 # If the base msgtext is not given, we use the default translation
164 # of the msgid (which is in English) just in case the system locale is
165 # not English, so that the base text will be in that locale by default.
166 if not msgtext:
167 msgtext = Message._translate_msgid(msgid, domain)
168 # We want to initialize the parent unicode with the actual object that
169 # would have been plain unicode if 'Message' was not enabled.
170 msg = super(Message, cls).__new__(cls, msgtext)
171 msg.msgid = msgid
172 msg.domain = domain
173 msg.params = params
174 return msg
175
176 def translate(self, desired_locale=None):
177 """Translate this message to the desired locale.
178
179 :param desired_locale: The desired locale to translate the message to,
180 if no locale is provided the message will be
181 translated to the system's default locale.
182
183 :returns: the translated message in unicode
184 """
185
186 translated_message = Message._translate_msgid(self.msgid,
187 self.domain,
188 desired_locale)
189 if self.params is None:
190 # No need for more translation
191 return translated_message
192
193 # This Message object may have been formatted with one or more
194 # Message objects as substitution arguments, given either as a single
195 # argument, part of a tuple, or as one or more values in a dictionary.
196 # When translating this Message we need to translate those Messages too
197 translated_params = _translate_args(self.params, desired_locale)
198
199 translated_message = translated_message % translated_params
200
201 return translated_message
202
203 @staticmethod
204 def _translate_msgid(msgid, domain, desired_locale=None):
205 if not desired_locale:
206 system_locale = locale.getdefaultlocale()
207 # If the system locale is not available to the runtime use English
208 if not system_locale[0]:
209 desired_locale = 'en_US'
210 else:
211 desired_locale = system_locale[0]
212
213 locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
214 lang = gettext.translation(domain,
215 localedir=locale_dir,
216 languages=[desired_locale],
217 fallback=True)
Matthew Treinishf45528a2013-10-24 20:12:28 +0000218 if six.PY3:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000219 translator = lang.gettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000220 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000221 translator = lang.ugettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000222
Matthew Treinish90ac9142014-03-17 14:58:37 +0000223 translated_message = translator(msgid)
224 return translated_message
Matthew Treinish0db53772013-07-26 10:39:35 -0400225
226 def __mod__(self, other):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000227 # When we mod a Message we want the actual operation to be performed
228 # by the parent class (i.e. unicode()), the only thing we do here is
229 # save the original msgid and the parameters in case of a translation
230 params = self._sanitize_mod_params(other)
231 unicode_mod = super(Message, self).__mod__(params)
232 modded = Message(self.msgid,
233 msgtext=unicode_mod,
234 params=params,
235 domain=self.domain)
236 return modded
Matthew Treinish0db53772013-07-26 10:39:35 -0400237
Matthew Treinish90ac9142014-03-17 14:58:37 +0000238 def _sanitize_mod_params(self, other):
239 """Sanitize the object being modded with this Message.
Matthew Treinish0db53772013-07-26 10:39:35 -0400240
Matthew Treinish90ac9142014-03-17 14:58:37 +0000241 - Add support for modding 'None' so translation supports it
242 - Trim the modded object, which can be a large dictionary, to only
243 those keys that would actually be used in a translation
244 - Snapshot the object being modded, in case the message is
245 translated, it will be used as it was when the Message was created
246 """
247 if other is None:
248 params = (other,)
249 elif isinstance(other, dict):
250 # Merge the dictionaries
251 # Copy each item in case one does not support deep copy.
252 params = {}
253 if isinstance(self.params, dict):
254 for key, val in self.params.items():
255 params[key] = self._copy_param(val)
256 for key, val in other.items():
257 params[key] = self._copy_param(val)
Matthew Treinish0db53772013-07-26 10:39:35 -0400258 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000259 params = self._copy_param(other)
260 return params
261
262 def _copy_param(self, param):
263 try:
264 return copy.deepcopy(param)
265 except Exception:
266 # Fallback to casting to unicode this will handle the
267 # python code-like objects that can't be deep-copied
268 return six.text_type(param)
269
270 def __add__(self, other):
271 msg = _('Message objects do not support addition.')
272 raise TypeError(msg)
273
274 def __radd__(self, other):
275 return self.__add__(other)
276
277 def __str__(self):
278 # NOTE(luisg): Logging in python 2.6 tries to str() log records,
279 # and it expects specifically a UnicodeError in order to proceed.
280 msg = _('Message objects do not support str() because they may '
281 'contain non-ascii characters. '
282 'Please use unicode() or translate() instead.')
283 raise UnicodeError(msg)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000284
285
286def get_available_languages(domain):
287 """Lists the available languages for the given translation domain.
288
289 :param domain: the domain to get languages for
290 """
291 if domain in _AVAILABLE_LANGUAGES:
292 return copy.copy(_AVAILABLE_LANGUAGES[domain])
293
294 localedir = '%s_LOCALEDIR' % domain.upper()
295 find = lambda x: gettext.find(domain,
296 localedir=os.environ.get(localedir),
297 languages=[x])
298
299 # NOTE(mrodden): en_US should always be available (and first in case
300 # order matters) since our in-line message strings are en_US
301 language_list = ['en_US']
302 # NOTE(luisg): Babel <1.0 used a function called list(), which was
303 # renamed to locale_identifiers() in >=1.0, the requirements master list
304 # requires >=0.9.6, uncapped, so defensively work with both. We can remove
Sean Daguefc691e32014-01-03 08:51:54 -0500305 # this check when the master list updates to >=1.0, and update all projects
Matthew Treinishffa94d62013-09-11 18:09:17 +0000306 list_identifiers = (getattr(localedata, 'list', None) or
307 getattr(localedata, 'locale_identifiers'))
308 locale_identifiers = list_identifiers()
Matthew Treinish90ac9142014-03-17 14:58:37 +0000309
Matthew Treinishffa94d62013-09-11 18:09:17 +0000310 for i in locale_identifiers:
311 if find(i) is not None:
312 language_list.append(i)
Matthew Treinish90ac9142014-03-17 14:58:37 +0000313
314 # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
315 # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
316 # are perfectly legitimate locales:
317 # https://github.com/mitsuhiko/babel/issues/37
318 # In Babel 1.3 they fixed the bug and they support these locales, but
319 # they are still not explicitly "listed" by locale_identifiers().
320 # That is why we add the locales here explicitly if necessary so that
321 # they are listed as supported.
322 aliases = {'zh': 'zh_CN',
323 'zh_Hant_HK': 'zh_HK',
324 'zh_Hant': 'zh_TW',
325 'fil': 'tl_PH'}
326 for (locale, alias) in six.iteritems(aliases):
327 if locale in language_list and alias not in language_list:
328 language_list.append(alias)
329
Matthew Treinishffa94d62013-09-11 18:09:17 +0000330 _AVAILABLE_LANGUAGES[domain] = language_list
331 return copy.copy(language_list)
332
333
Matthew Treinish90ac9142014-03-17 14:58:37 +0000334def translate(obj, desired_locale=None):
335 """Gets the translated unicode representation of the given object.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000336
Matthew Treinish90ac9142014-03-17 14:58:37 +0000337 If the object is not translatable it is returned as-is.
338 If the locale is None the object is translated to the system locale.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000339
Matthew Treinish90ac9142014-03-17 14:58:37 +0000340 :param obj: the object to translate
341 :param desired_locale: the locale to translate the message to, if None the
342 default system locale will be used
343 :returns: the translated object in unicode, or the original object if
Matthew Treinishf45528a2013-10-24 20:12:28 +0000344 it could not be translated
345 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000346 message = obj
347 if not isinstance(message, Message):
348 # If the object to translate is not already translatable,
349 # let's first get its unicode representation
350 message = six.text_type(obj)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000351 if isinstance(message, Message):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000352 # Even after unicoding() we still need to check if we are
353 # running with translatable unicode before translating
354 return message.translate(desired_locale)
355 return obj
Matthew Treinish0db53772013-07-26 10:39:35 -0400356
357
Matthew Treinish90ac9142014-03-17 14:58:37 +0000358def _translate_args(args, desired_locale=None):
359 """Translates all the translatable elements of the given arguments object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400360
Matthew Treinish90ac9142014-03-17 14:58:37 +0000361 This method is used for translating the translatable values in method
362 arguments which include values of tuples or dictionaries.
363 If the object is not a tuple or a dictionary the object itself is
364 translated if it is translatable.
365
366 If the locale is None the object is translated to the system locale.
367
368 :param args: the args to translate
369 :param desired_locale: the locale to translate the args to, if None the
370 default system locale will be used
371 :returns: a new args object with the translated contents of the original
372 """
373 if isinstance(args, tuple):
374 return tuple(translate(v, desired_locale) for v in args)
375 if isinstance(args, dict):
376 translated_dict = {}
377 for (k, v) in six.iteritems(args):
378 translated_v = translate(v, desired_locale)
379 translated_dict[k] = translated_v
380 return translated_dict
381 return translate(args, desired_locale)
382
383
384class TranslationHandler(handlers.MemoryHandler):
385 """Handler that translates records before logging them.
386
387 The TranslationHandler takes a locale and a target logging.Handler object
388 to forward LogRecord objects to after translating them. This handler
389 depends on Message objects being logged, instead of regular strings.
390
391 The handler can be configured declaratively in the logging.conf as follows:
392
393 [handlers]
394 keys = translatedlog, translator
395
396 [handler_translatedlog]
397 class = handlers.WatchedFileHandler
398 args = ('/var/log/api-localized.log',)
399 formatter = context
400
401 [handler_translator]
402 class = openstack.common.log.TranslationHandler
403 target = translatedlog
404 args = ('zh_CN',)
405
406 If the specified locale is not available in the system, the handler will
407 log in the default locale.
Matthew Treinish0db53772013-07-26 10:39:35 -0400408 """
409
Matthew Treinish90ac9142014-03-17 14:58:37 +0000410 def __init__(self, locale=None, target=None):
411 """Initialize a TranslationHandler
Matthew Treinish0db53772013-07-26 10:39:35 -0400412
413 :param locale: locale to use for translating messages
414 :param target: logging.Handler object to forward
415 LogRecord objects to after translation
416 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000417 # NOTE(luisg): In order to allow this handler to be a wrapper for
418 # other handlers, such as a FileHandler, and still be able to
419 # configure it using logging.conf, this handler has to extend
420 # MemoryHandler because only the MemoryHandlers' logging.conf
421 # parsing is implemented such that it accepts a target handler.
422 handlers.MemoryHandler.__init__(self, capacity=0, target=target)
Matthew Treinish0db53772013-07-26 10:39:35 -0400423 self.locale = locale
Matthew Treinish90ac9142014-03-17 14:58:37 +0000424
425 def setFormatter(self, fmt):
426 self.target.setFormatter(fmt)
Matthew Treinish0db53772013-07-26 10:39:35 -0400427
428 def emit(self, record):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000429 # We save the message from the original record to restore it
430 # after translation, so other handlers are not affected by this
431 original_msg = record.msg
432 original_args = record.args
433
434 try:
435 self._translate_and_log_record(record)
436 finally:
437 record.msg = original_msg
438 record.args = original_args
439
440 def _translate_and_log_record(self, record):
441 record.msg = translate(record.msg, self.locale)
442
443 # In addition to translating the message, we also need to translate
444 # arguments that were passed to the log method that were not part
445 # of the main message e.g., log.info(_('Some message %s'), this_one))
446 record.args = _translate_args(record.args, self.locale)
Matthew Treinish0db53772013-07-26 10:39:35 -0400447
448 self.target.emit(record)