blob: 6102e677ee29c4ab27d475d3de6539bd8f9b7ef7 [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
Matthew Treinishffa94d62013-09-11 18:09:17 +000035_AVAILABLE_LANGUAGES = {}
Matthew Treinish42516852014-06-19 10:51:29 -040036
37# FIXME(dhellmann): Remove this when moving to oslo.i18n.
Matthew Treinishffa94d62013-09-11 18:09:17 +000038USE_LAZY = False
39
40
Matthew Treinish42516852014-06-19 10:51:29 -040041class TranslatorFactory(object):
42 """Create translator functions
43 """
44
45 def __init__(self, domain, lazy=False, localedir=None):
46 """Establish a set of translation functions for the domain.
47
48 :param domain: Name of translation domain,
49 specifying a message catalog.
50 :type domain: str
51 :param lazy: Delays translation until a message is emitted.
52 Defaults to False.
53 :type lazy: Boolean
54 :param localedir: Directory with translation catalogs.
55 :type localedir: str
56 """
57 self.domain = domain
58 self.lazy = lazy
59 if localedir is None:
60 localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
61 self.localedir = localedir
62
63 def _make_translation_func(self, domain=None):
64 """Return a new translation function ready for use.
65
66 Takes into account whether or not lazy translation is being
67 done.
68
69 The domain can be specified to override the default from the
70 factory, but the localedir from the factory is always used
71 because we assume the log-level translation catalogs are
72 installed in the same directory as the main application
73 catalog.
74
75 """
76 if domain is None:
77 domain = self.domain
78 if self.lazy:
79 return functools.partial(Message, domain=domain)
80 t = gettext.translation(
81 domain,
82 localedir=self.localedir,
83 fallback=True,
84 )
85 if six.PY3:
86 return t.gettext
87 return t.ugettext
88
89 @property
90 def primary(self):
91 "The default translation function."
92 return self._make_translation_func()
93
94 def _make_log_translation_func(self, level):
95 return self._make_translation_func(self.domain + '-log-' + level)
96
97 @property
98 def log_info(self):
99 "Translate info-level log messages."
100 return self._make_log_translation_func('info')
101
102 @property
103 def log_warning(self):
104 "Translate warning-level log messages."
105 return self._make_log_translation_func('warning')
106
107 @property
108 def log_error(self):
109 "Translate error-level log messages."
110 return self._make_log_translation_func('error')
111
112 @property
113 def log_critical(self):
114 "Translate critical-level log messages."
115 return self._make_log_translation_func('critical')
116
117
118# NOTE(dhellmann): When this module moves out of the incubator into
119# oslo.i18n, these global variables can be moved to an integration
120# module within each application.
121
122# Create the global translation functions.
123_translators = TranslatorFactory('tempest')
124
125# The primary translation function using the well-known name "_"
126_ = _translators.primary
127
128# Translators for log levels.
129#
130# The abbreviated names are meant to reflect the usual use of a short
131# name like '_'. The "L" is for "log" and the other letter comes from
132# the level.
133_LI = _translators.log_info
134_LW = _translators.log_warning
135_LE = _translators.log_error
136_LC = _translators.log_critical
137
138# NOTE(dhellmann): End of globals that will move to the application's
139# integration module.
140
141
Matthew Treinishffa94d62013-09-11 18:09:17 +0000142def enable_lazy():
143 """Convenience function for configuring _() to use lazy gettext
144
145 Call this at the start of execution to enable the gettextutils._
146 function to use lazy gettext functionality. This is useful if
147 your project is importing _ directly instead of using the
148 gettextutils.install() way of importing the _ function.
149 """
Matthew Treinish42516852014-06-19 10:51:29 -0400150 # FIXME(dhellmann): This function will be removed in oslo.i18n,
151 # because the TranslatorFactory makes it superfluous.
152 global _, _LI, _LW, _LE, _LC, USE_LAZY
153 tf = TranslatorFactory('tempest', lazy=True)
154 _ = tf.primary
155 _LI = tf.log_info
156 _LW = tf.log_warning
157 _LE = tf.log_error
158 _LC = tf.log_critical
Matthew Treinishffa94d62013-09-11 18:09:17 +0000159 USE_LAZY = True
160
Matthew Treinish0db53772013-07-26 10:39:35 -0400161
Matthew Treinishffa94d62013-09-11 18:09:17 +0000162def install(domain, lazy=False):
Matthew Treinish0db53772013-07-26 10:39:35 -0400163 """Install a _() function using the given translation domain.
164
165 Given a translation domain, install a _() function using gettext's
166 install() function.
167
168 The main difference from gettext.install() is that we allow
169 overriding the default localedir (e.g. /usr/share/locale) using
170 a translation-domain-specific environment variable (e.g.
171 NOVA_LOCALEDIR).
Matthew Treinishffa94d62013-09-11 18:09:17 +0000172
173 :param domain: the translation domain
174 :param lazy: indicates whether or not to install the lazy _() function.
175 The lazy _() introduces a way to do deferred translation
176 of messages by installing a _ that builds Message objects,
177 instead of strings, which can then be lazily translated into
178 any available locale.
Matthew Treinish0db53772013-07-26 10:39:35 -0400179 """
Matthew Treinishffa94d62013-09-11 18:09:17 +0000180 if lazy:
Matthew Treinishf45528a2013-10-24 20:12:28 +0000181 from six import moves
Matthew Treinish42516852014-06-19 10:51:29 -0400182 tf = TranslatorFactory(domain, lazy=True)
183 moves.builtins.__dict__['_'] = tf.primary
Matthew Treinishffa94d62013-09-11 18:09:17 +0000184 else:
185 localedir = '%s_LOCALEDIR' % domain.upper()
Matthew Treinishf45528a2013-10-24 20:12:28 +0000186 if six.PY3:
187 gettext.install(domain,
188 localedir=os.environ.get(localedir))
189 else:
190 gettext.install(domain,
191 localedir=os.environ.get(localedir),
192 unicode=True)
Matthew Treinish0db53772013-07-26 10:39:35 -0400193
194
Matthew Treinish90ac9142014-03-17 14:58:37 +0000195class Message(six.text_type):
196 """A Message object is a unicode object that can be translated.
Matthew Treinish0db53772013-07-26 10:39:35 -0400197
Matthew Treinish90ac9142014-03-17 14:58:37 +0000198 Translation of Message is done explicitly using the translate() method.
199 For all non-translation intents and purposes, a Message is simply unicode,
200 and can be treated as such.
201 """
Matthew Treinish0db53772013-07-26 10:39:35 -0400202
Matthew Treinish90ac9142014-03-17 14:58:37 +0000203 def __new__(cls, msgid, msgtext=None, params=None,
204 domain='tempest', *args):
205 """Create a new Message object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400206
Matthew Treinish90ac9142014-03-17 14:58:37 +0000207 In order for translation to work gettext requires a message ID, this
208 msgid will be used as the base unicode text. It is also possible
209 for the msgid and the base unicode text to be different by passing
210 the msgtext parameter.
211 """
212 # If the base msgtext is not given, we use the default translation
213 # of the msgid (which is in English) just in case the system locale is
214 # not English, so that the base text will be in that locale by default.
215 if not msgtext:
216 msgtext = Message._translate_msgid(msgid, domain)
217 # We want to initialize the parent unicode with the actual object that
218 # would have been plain unicode if 'Message' was not enabled.
219 msg = super(Message, cls).__new__(cls, msgtext)
220 msg.msgid = msgid
221 msg.domain = domain
222 msg.params = params
223 return msg
224
225 def translate(self, desired_locale=None):
226 """Translate this message to the desired locale.
227
228 :param desired_locale: The desired locale to translate the message to,
229 if no locale is provided the message will be
230 translated to the system's default locale.
231
232 :returns: the translated message in unicode
233 """
234
235 translated_message = Message._translate_msgid(self.msgid,
236 self.domain,
237 desired_locale)
238 if self.params is None:
239 # No need for more translation
240 return translated_message
241
242 # This Message object may have been formatted with one or more
243 # Message objects as substitution arguments, given either as a single
244 # argument, part of a tuple, or as one or more values in a dictionary.
245 # When translating this Message we need to translate those Messages too
246 translated_params = _translate_args(self.params, desired_locale)
247
248 translated_message = translated_message % translated_params
249
250 return translated_message
251
252 @staticmethod
253 def _translate_msgid(msgid, domain, desired_locale=None):
254 if not desired_locale:
255 system_locale = locale.getdefaultlocale()
256 # If the system locale is not available to the runtime use English
257 if not system_locale[0]:
258 desired_locale = 'en_US'
259 else:
260 desired_locale = system_locale[0]
261
262 locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
263 lang = gettext.translation(domain,
264 localedir=locale_dir,
265 languages=[desired_locale],
266 fallback=True)
Matthew Treinishf45528a2013-10-24 20:12:28 +0000267 if six.PY3:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000268 translator = lang.gettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000269 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000270 translator = lang.ugettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000271
Matthew Treinish90ac9142014-03-17 14:58:37 +0000272 translated_message = translator(msgid)
273 return translated_message
Matthew Treinish0db53772013-07-26 10:39:35 -0400274
275 def __mod__(self, other):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000276 # When we mod a Message we want the actual operation to be performed
277 # by the parent class (i.e. unicode()), the only thing we do here is
278 # save the original msgid and the parameters in case of a translation
279 params = self._sanitize_mod_params(other)
280 unicode_mod = super(Message, self).__mod__(params)
281 modded = Message(self.msgid,
282 msgtext=unicode_mod,
283 params=params,
284 domain=self.domain)
285 return modded
Matthew Treinish0db53772013-07-26 10:39:35 -0400286
Matthew Treinish90ac9142014-03-17 14:58:37 +0000287 def _sanitize_mod_params(self, other):
288 """Sanitize the object being modded with this Message.
Matthew Treinish0db53772013-07-26 10:39:35 -0400289
Matthew Treinish90ac9142014-03-17 14:58:37 +0000290 - Add support for modding 'None' so translation supports it
291 - Trim the modded object, which can be a large dictionary, to only
292 those keys that would actually be used in a translation
293 - Snapshot the object being modded, in case the message is
294 translated, it will be used as it was when the Message was created
295 """
296 if other is None:
297 params = (other,)
298 elif isinstance(other, dict):
299 # Merge the dictionaries
300 # Copy each item in case one does not support deep copy.
301 params = {}
302 if isinstance(self.params, dict):
303 for key, val in self.params.items():
304 params[key] = self._copy_param(val)
305 for key, val in other.items():
306 params[key] = self._copy_param(val)
Matthew Treinish0db53772013-07-26 10:39:35 -0400307 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000308 params = self._copy_param(other)
309 return params
310
311 def _copy_param(self, param):
312 try:
313 return copy.deepcopy(param)
314 except Exception:
315 # Fallback to casting to unicode this will handle the
316 # python code-like objects that can't be deep-copied
317 return six.text_type(param)
318
319 def __add__(self, other):
320 msg = _('Message objects do not support addition.')
321 raise TypeError(msg)
322
323 def __radd__(self, other):
324 return self.__add__(other)
325
Matthew Treinish42516852014-06-19 10:51:29 -0400326 if six.PY2:
327 def __str__(self):
328 # NOTE(luisg): Logging in python 2.6 tries to str() log records,
329 # and it expects specifically a UnicodeError in order to proceed.
330 msg = _('Message objects do not support str() because they may '
331 'contain non-ascii characters. '
332 'Please use unicode() or translate() instead.')
333 raise UnicodeError(msg)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000334
335
336def get_available_languages(domain):
337 """Lists the available languages for the given translation domain.
338
339 :param domain: the domain to get languages for
340 """
341 if domain in _AVAILABLE_LANGUAGES:
342 return copy.copy(_AVAILABLE_LANGUAGES[domain])
343
344 localedir = '%s_LOCALEDIR' % domain.upper()
345 find = lambda x: gettext.find(domain,
346 localedir=os.environ.get(localedir),
347 languages=[x])
348
349 # NOTE(mrodden): en_US should always be available (and first in case
350 # order matters) since our in-line message strings are en_US
351 language_list = ['en_US']
352 # NOTE(luisg): Babel <1.0 used a function called list(), which was
353 # renamed to locale_identifiers() in >=1.0, the requirements master list
354 # requires >=0.9.6, uncapped, so defensively work with both. We can remove
Sean Daguefc691e32014-01-03 08:51:54 -0500355 # this check when the master list updates to >=1.0, and update all projects
Matthew Treinishffa94d62013-09-11 18:09:17 +0000356 list_identifiers = (getattr(localedata, 'list', None) or
357 getattr(localedata, 'locale_identifiers'))
358 locale_identifiers = list_identifiers()
Matthew Treinish90ac9142014-03-17 14:58:37 +0000359
Matthew Treinishffa94d62013-09-11 18:09:17 +0000360 for i in locale_identifiers:
361 if find(i) is not None:
362 language_list.append(i)
Matthew Treinish90ac9142014-03-17 14:58:37 +0000363
364 # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
365 # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
366 # are perfectly legitimate locales:
367 # https://github.com/mitsuhiko/babel/issues/37
368 # In Babel 1.3 they fixed the bug and they support these locales, but
369 # they are still not explicitly "listed" by locale_identifiers().
370 # That is why we add the locales here explicitly if necessary so that
371 # they are listed as supported.
372 aliases = {'zh': 'zh_CN',
373 'zh_Hant_HK': 'zh_HK',
374 'zh_Hant': 'zh_TW',
375 'fil': 'tl_PH'}
376 for (locale, alias) in six.iteritems(aliases):
377 if locale in language_list and alias not in language_list:
378 language_list.append(alias)
379
Matthew Treinishffa94d62013-09-11 18:09:17 +0000380 _AVAILABLE_LANGUAGES[domain] = language_list
381 return copy.copy(language_list)
382
383
Matthew Treinish90ac9142014-03-17 14:58:37 +0000384def translate(obj, desired_locale=None):
385 """Gets the translated unicode representation of the given object.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000386
Matthew Treinish90ac9142014-03-17 14:58:37 +0000387 If the object is not translatable it is returned as-is.
388 If the locale is None the object is translated to the system locale.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000389
Matthew Treinish90ac9142014-03-17 14:58:37 +0000390 :param obj: the object to translate
391 :param desired_locale: the locale to translate the message to, if None the
392 default system locale will be used
393 :returns: the translated object in unicode, or the original object if
Matthew Treinishf45528a2013-10-24 20:12:28 +0000394 it could not be translated
395 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000396 message = obj
397 if not isinstance(message, Message):
398 # If the object to translate is not already translatable,
399 # let's first get its unicode representation
400 message = six.text_type(obj)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000401 if isinstance(message, Message):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000402 # Even after unicoding() we still need to check if we are
403 # running with translatable unicode before translating
404 return message.translate(desired_locale)
405 return obj
Matthew Treinish0db53772013-07-26 10:39:35 -0400406
407
Matthew Treinish90ac9142014-03-17 14:58:37 +0000408def _translate_args(args, desired_locale=None):
409 """Translates all the translatable elements of the given arguments object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400410
Matthew Treinish90ac9142014-03-17 14:58:37 +0000411 This method is used for translating the translatable values in method
412 arguments which include values of tuples or dictionaries.
413 If the object is not a tuple or a dictionary the object itself is
414 translated if it is translatable.
415
416 If the locale is None the object is translated to the system locale.
417
418 :param args: the args to translate
419 :param desired_locale: the locale to translate the args to, if None the
420 default system locale will be used
421 :returns: a new args object with the translated contents of the original
422 """
423 if isinstance(args, tuple):
424 return tuple(translate(v, desired_locale) for v in args)
425 if isinstance(args, dict):
426 translated_dict = {}
427 for (k, v) in six.iteritems(args):
428 translated_v = translate(v, desired_locale)
429 translated_dict[k] = translated_v
430 return translated_dict
431 return translate(args, desired_locale)
432
433
434class TranslationHandler(handlers.MemoryHandler):
435 """Handler that translates records before logging them.
436
437 The TranslationHandler takes a locale and a target logging.Handler object
438 to forward LogRecord objects to after translating them. This handler
439 depends on Message objects being logged, instead of regular strings.
440
441 The handler can be configured declaratively in the logging.conf as follows:
442
443 [handlers]
444 keys = translatedlog, translator
445
446 [handler_translatedlog]
447 class = handlers.WatchedFileHandler
448 args = ('/var/log/api-localized.log',)
449 formatter = context
450
451 [handler_translator]
452 class = openstack.common.log.TranslationHandler
453 target = translatedlog
454 args = ('zh_CN',)
455
456 If the specified locale is not available in the system, the handler will
457 log in the default locale.
Matthew Treinish0db53772013-07-26 10:39:35 -0400458 """
459
Matthew Treinish90ac9142014-03-17 14:58:37 +0000460 def __init__(self, locale=None, target=None):
461 """Initialize a TranslationHandler
Matthew Treinish0db53772013-07-26 10:39:35 -0400462
463 :param locale: locale to use for translating messages
464 :param target: logging.Handler object to forward
465 LogRecord objects to after translation
466 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000467 # NOTE(luisg): In order to allow this handler to be a wrapper for
468 # other handlers, such as a FileHandler, and still be able to
469 # configure it using logging.conf, this handler has to extend
470 # MemoryHandler because only the MemoryHandlers' logging.conf
471 # parsing is implemented such that it accepts a target handler.
472 handlers.MemoryHandler.__init__(self, capacity=0, target=target)
Matthew Treinish0db53772013-07-26 10:39:35 -0400473 self.locale = locale
Matthew Treinish90ac9142014-03-17 14:58:37 +0000474
475 def setFormatter(self, fmt):
476 self.target.setFormatter(fmt)
Matthew Treinish0db53772013-07-26 10:39:35 -0400477
478 def emit(self, record):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000479 # We save the message from the original record to restore it
480 # after translation, so other handlers are not affected by this
481 original_msg = record.msg
482 original_args = record.args
483
484 try:
485 self._translate_and_log_record(record)
486 finally:
487 record.msg = original_msg
488 record.args = original_args
489
490 def _translate_and_log_record(self, record):
491 record.msg = translate(record.msg, self.locale)
492
493 # In addition to translating the message, we also need to translate
494 # arguments that were passed to the log method that were not part
495 # of the main message e.g., log.info(_('Some message %s'), this_one))
496 record.args = _translate_args(record.args, self.locale)
Matthew Treinish0db53772013-07-26 10:39:35 -0400497
498 self.target.emit(record)