blob: 872d58ea51fb554b2598444dfe68c0793a5af258 [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
26import gettext
Matthew Treinish90ac9142014-03-17 14:58:37 +000027import locale
28from logging import handlers
Matthew Treinish0db53772013-07-26 10:39:35 -040029import os
Matthew Treinish0db53772013-07-26 10:39:35 -040030
Matthew Treinishffa94d62013-09-11 18:09:17 +000031from babel import localedata
Matthew Treinish0db53772013-07-26 10:39:35 -040032import six
33
Matthew Treinishffa94d62013-09-11 18:09:17 +000034_AVAILABLE_LANGUAGES = {}
Matthew Treinish42516852014-06-19 10:51:29 -040035
36# FIXME(dhellmann): Remove this when moving to oslo.i18n.
Matthew Treinishffa94d62013-09-11 18:09:17 +000037USE_LAZY = False
38
39
Matthew Treinish42516852014-06-19 10:51:29 -040040class TranslatorFactory(object):
41 """Create translator functions
42 """
43
Sean Dague2bbdf422014-07-11 07:58:33 -040044 def __init__(self, domain, localedir=None):
Matthew Treinish42516852014-06-19 10:51:29 -040045 """Establish a set of translation functions for the domain.
46
47 :param domain: Name of translation domain,
48 specifying a message catalog.
49 :type domain: str
50 :param lazy: Delays translation until a message is emitted.
51 Defaults to False.
52 :type lazy: Boolean
53 :param localedir: Directory with translation catalogs.
54 :type localedir: str
55 """
56 self.domain = domain
Matthew Treinish42516852014-06-19 10:51:29 -040057 if localedir is None:
58 localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
59 self.localedir = localedir
60
61 def _make_translation_func(self, domain=None):
62 """Return a new translation function ready for use.
63
64 Takes into account whether or not lazy translation is being
65 done.
66
67 The domain can be specified to override the default from the
68 factory, but the localedir from the factory is always used
69 because we assume the log-level translation catalogs are
70 installed in the same directory as the main application
71 catalog.
72
73 """
74 if domain is None:
75 domain = self.domain
Sean Dague2bbdf422014-07-11 07:58:33 -040076 t = gettext.translation(domain,
77 localedir=self.localedir,
78 fallback=True)
79 # Use the appropriate method of the translation object based
80 # on the python version.
81 m = t.gettext if six.PY3 else t.ugettext
82
83 def f(msg):
84 """oslo.i18n.gettextutils translation function."""
85 if USE_LAZY:
86 return Message(msg, domain=domain)
87 return m(msg)
88 return f
Matthew Treinish42516852014-06-19 10:51:29 -040089
90 @property
91 def primary(self):
92 "The default translation function."
93 return self._make_translation_func()
94
95 def _make_log_translation_func(self, level):
96 return self._make_translation_func(self.domain + '-log-' + level)
97
98 @property
99 def log_info(self):
100 "Translate info-level log messages."
101 return self._make_log_translation_func('info')
102
103 @property
104 def log_warning(self):
105 "Translate warning-level log messages."
106 return self._make_log_translation_func('warning')
107
108 @property
109 def log_error(self):
110 "Translate error-level log messages."
111 return self._make_log_translation_func('error')
112
113 @property
114 def log_critical(self):
115 "Translate critical-level log messages."
116 return self._make_log_translation_func('critical')
117
118
119# NOTE(dhellmann): When this module moves out of the incubator into
120# oslo.i18n, these global variables can be moved to an integration
121# module within each application.
122
123# Create the global translation functions.
124_translators = TranslatorFactory('tempest')
125
126# The primary translation function using the well-known name "_"
127_ = _translators.primary
128
129# Translators for log levels.
130#
131# The abbreviated names are meant to reflect the usual use of a short
132# name like '_'. The "L" is for "log" and the other letter comes from
133# the level.
134_LI = _translators.log_info
135_LW = _translators.log_warning
136_LE = _translators.log_error
137_LC = _translators.log_critical
138
139# NOTE(dhellmann): End of globals that will move to the application's
140# integration module.
141
142
Matthew Treinishffa94d62013-09-11 18:09:17 +0000143def enable_lazy():
144 """Convenience function for configuring _() to use lazy gettext
145
146 Call this at the start of execution to enable the gettextutils._
147 function to use lazy gettext functionality. This is useful if
148 your project is importing _ directly instead of using the
149 gettextutils.install() way of importing the _ function.
150 """
Sean Dague2bbdf422014-07-11 07:58:33 -0400151 global USE_LAZY
Matthew Treinishffa94d62013-09-11 18:09:17 +0000152 USE_LAZY = True
153
Matthew Treinish0db53772013-07-26 10:39:35 -0400154
Sean Dague2bbdf422014-07-11 07:58:33 -0400155def install(domain):
Matthew Treinish0db53772013-07-26 10:39:35 -0400156 """Install a _() function using the given translation domain.
157
158 Given a translation domain, install a _() function using gettext's
159 install() function.
160
161 The main difference from gettext.install() is that we allow
162 overriding the default localedir (e.g. /usr/share/locale) using
163 a translation-domain-specific environment variable (e.g.
164 NOVA_LOCALEDIR).
Matthew Treinishffa94d62013-09-11 18:09:17 +0000165
Sean Dague2bbdf422014-07-11 07:58:33 -0400166 Note that to enable lazy translation, enable_lazy must be
167 called.
168
Matthew Treinishffa94d62013-09-11 18:09:17 +0000169 :param domain: the translation domain
Matthew Treinish0db53772013-07-26 10:39:35 -0400170 """
Sean Dague2bbdf422014-07-11 07:58:33 -0400171 from six import moves
172 tf = TranslatorFactory(domain)
173 moves.builtins.__dict__['_'] = tf.primary
Matthew Treinish0db53772013-07-26 10:39:35 -0400174
175
Matthew Treinish90ac9142014-03-17 14:58:37 +0000176class Message(six.text_type):
177 """A Message object is a unicode object that can be translated.
Matthew Treinish0db53772013-07-26 10:39:35 -0400178
Matthew Treinish90ac9142014-03-17 14:58:37 +0000179 Translation of Message is done explicitly using the translate() method.
180 For all non-translation intents and purposes, a Message is simply unicode,
181 and can be treated as such.
182 """
Matthew Treinish0db53772013-07-26 10:39:35 -0400183
Matthew Treinish90ac9142014-03-17 14:58:37 +0000184 def __new__(cls, msgid, msgtext=None, params=None,
185 domain='tempest', *args):
186 """Create a new Message object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400187
Matthew Treinish90ac9142014-03-17 14:58:37 +0000188 In order for translation to work gettext requires a message ID, this
189 msgid will be used as the base unicode text. It is also possible
190 for the msgid and the base unicode text to be different by passing
191 the msgtext parameter.
192 """
193 # If the base msgtext is not given, we use the default translation
194 # of the msgid (which is in English) just in case the system locale is
195 # not English, so that the base text will be in that locale by default.
196 if not msgtext:
197 msgtext = Message._translate_msgid(msgid, domain)
198 # We want to initialize the parent unicode with the actual object that
199 # would have been plain unicode if 'Message' was not enabled.
200 msg = super(Message, cls).__new__(cls, msgtext)
201 msg.msgid = msgid
202 msg.domain = domain
203 msg.params = params
204 return msg
205
206 def translate(self, desired_locale=None):
207 """Translate this message to the desired locale.
208
209 :param desired_locale: The desired locale to translate the message to,
210 if no locale is provided the message will be
211 translated to the system's default locale.
212
213 :returns: the translated message in unicode
214 """
215
216 translated_message = Message._translate_msgid(self.msgid,
217 self.domain,
218 desired_locale)
219 if self.params is None:
220 # No need for more translation
221 return translated_message
222
223 # This Message object may have been formatted with one or more
224 # Message objects as substitution arguments, given either as a single
225 # argument, part of a tuple, or as one or more values in a dictionary.
226 # When translating this Message we need to translate those Messages too
227 translated_params = _translate_args(self.params, desired_locale)
228
229 translated_message = translated_message % translated_params
230
231 return translated_message
232
233 @staticmethod
234 def _translate_msgid(msgid, domain, desired_locale=None):
235 if not desired_locale:
236 system_locale = locale.getdefaultlocale()
237 # If the system locale is not available to the runtime use English
238 if not system_locale[0]:
239 desired_locale = 'en_US'
240 else:
241 desired_locale = system_locale[0]
242
243 locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
244 lang = gettext.translation(domain,
245 localedir=locale_dir,
246 languages=[desired_locale],
247 fallback=True)
Matthew Treinishf45528a2013-10-24 20:12:28 +0000248 if six.PY3:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000249 translator = lang.gettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000250 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000251 translator = lang.ugettext
Matthew Treinishf45528a2013-10-24 20:12:28 +0000252
Matthew Treinish90ac9142014-03-17 14:58:37 +0000253 translated_message = translator(msgid)
254 return translated_message
Matthew Treinish0db53772013-07-26 10:39:35 -0400255
256 def __mod__(self, other):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000257 # When we mod a Message we want the actual operation to be performed
258 # by the parent class (i.e. unicode()), the only thing we do here is
259 # save the original msgid and the parameters in case of a translation
260 params = self._sanitize_mod_params(other)
261 unicode_mod = super(Message, self).__mod__(params)
262 modded = Message(self.msgid,
263 msgtext=unicode_mod,
264 params=params,
265 domain=self.domain)
266 return modded
Matthew Treinish0db53772013-07-26 10:39:35 -0400267
Matthew Treinish90ac9142014-03-17 14:58:37 +0000268 def _sanitize_mod_params(self, other):
269 """Sanitize the object being modded with this Message.
Matthew Treinish0db53772013-07-26 10:39:35 -0400270
Matthew Treinish90ac9142014-03-17 14:58:37 +0000271 - Add support for modding 'None' so translation supports it
272 - Trim the modded object, which can be a large dictionary, to only
273 those keys that would actually be used in a translation
274 - Snapshot the object being modded, in case the message is
275 translated, it will be used as it was when the Message was created
276 """
277 if other is None:
278 params = (other,)
279 elif isinstance(other, dict):
280 # Merge the dictionaries
281 # Copy each item in case one does not support deep copy.
282 params = {}
283 if isinstance(self.params, dict):
284 for key, val in self.params.items():
285 params[key] = self._copy_param(val)
286 for key, val in other.items():
287 params[key] = self._copy_param(val)
Matthew Treinish0db53772013-07-26 10:39:35 -0400288 else:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000289 params = self._copy_param(other)
290 return params
291
292 def _copy_param(self, param):
293 try:
294 return copy.deepcopy(param)
295 except Exception:
296 # Fallback to casting to unicode this will handle the
297 # python code-like objects that can't be deep-copied
298 return six.text_type(param)
299
300 def __add__(self, other):
301 msg = _('Message objects do not support addition.')
302 raise TypeError(msg)
303
304 def __radd__(self, other):
305 return self.__add__(other)
306
Matthew Treinish42516852014-06-19 10:51:29 -0400307 if six.PY2:
308 def __str__(self):
309 # NOTE(luisg): Logging in python 2.6 tries to str() log records,
310 # and it expects specifically a UnicodeError in order to proceed.
311 msg = _('Message objects do not support str() because they may '
312 'contain non-ascii characters. '
313 'Please use unicode() or translate() instead.')
314 raise UnicodeError(msg)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000315
316
317def get_available_languages(domain):
318 """Lists the available languages for the given translation domain.
319
320 :param domain: the domain to get languages for
321 """
322 if domain in _AVAILABLE_LANGUAGES:
323 return copy.copy(_AVAILABLE_LANGUAGES[domain])
324
325 localedir = '%s_LOCALEDIR' % domain.upper()
326 find = lambda x: gettext.find(domain,
327 localedir=os.environ.get(localedir),
328 languages=[x])
329
330 # NOTE(mrodden): en_US should always be available (and first in case
331 # order matters) since our in-line message strings are en_US
332 language_list = ['en_US']
333 # NOTE(luisg): Babel <1.0 used a function called list(), which was
334 # renamed to locale_identifiers() in >=1.0, the requirements master list
335 # requires >=0.9.6, uncapped, so defensively work with both. We can remove
Sean Daguefc691e32014-01-03 08:51:54 -0500336 # this check when the master list updates to >=1.0, and update all projects
Matthew Treinishffa94d62013-09-11 18:09:17 +0000337 list_identifiers = (getattr(localedata, 'list', None) or
338 getattr(localedata, 'locale_identifiers'))
339 locale_identifiers = list_identifiers()
Matthew Treinish90ac9142014-03-17 14:58:37 +0000340
Matthew Treinishffa94d62013-09-11 18:09:17 +0000341 for i in locale_identifiers:
342 if find(i) is not None:
343 language_list.append(i)
Matthew Treinish90ac9142014-03-17 14:58:37 +0000344
345 # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
346 # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
347 # are perfectly legitimate locales:
348 # https://github.com/mitsuhiko/babel/issues/37
349 # In Babel 1.3 they fixed the bug and they support these locales, but
350 # they are still not explicitly "listed" by locale_identifiers().
351 # That is why we add the locales here explicitly if necessary so that
352 # they are listed as supported.
353 aliases = {'zh': 'zh_CN',
354 'zh_Hant_HK': 'zh_HK',
355 'zh_Hant': 'zh_TW',
356 'fil': 'tl_PH'}
Sean Dague2bbdf422014-07-11 07:58:33 -0400357 for (locale_, alias) in six.iteritems(aliases):
358 if locale_ in language_list and alias not in language_list:
Matthew Treinish90ac9142014-03-17 14:58:37 +0000359 language_list.append(alias)
360
Matthew Treinishffa94d62013-09-11 18:09:17 +0000361 _AVAILABLE_LANGUAGES[domain] = language_list
362 return copy.copy(language_list)
363
364
Matthew Treinish90ac9142014-03-17 14:58:37 +0000365def translate(obj, desired_locale=None):
366 """Gets the translated unicode representation of the given object.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000367
Matthew Treinish90ac9142014-03-17 14:58:37 +0000368 If the object is not translatable it is returned as-is.
369 If the locale is None the object is translated to the system locale.
Matthew Treinishf45528a2013-10-24 20:12:28 +0000370
Matthew Treinish90ac9142014-03-17 14:58:37 +0000371 :param obj: the object to translate
372 :param desired_locale: the locale to translate the message to, if None the
373 default system locale will be used
374 :returns: the translated object in unicode, or the original object if
Matthew Treinishf45528a2013-10-24 20:12:28 +0000375 it could not be translated
376 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000377 message = obj
378 if not isinstance(message, Message):
379 # If the object to translate is not already translatable,
380 # let's first get its unicode representation
381 message = six.text_type(obj)
Matthew Treinishffa94d62013-09-11 18:09:17 +0000382 if isinstance(message, Message):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000383 # Even after unicoding() we still need to check if we are
384 # running with translatable unicode before translating
385 return message.translate(desired_locale)
386 return obj
Matthew Treinish0db53772013-07-26 10:39:35 -0400387
388
Matthew Treinish90ac9142014-03-17 14:58:37 +0000389def _translate_args(args, desired_locale=None):
390 """Translates all the translatable elements of the given arguments object.
Matthew Treinish0db53772013-07-26 10:39:35 -0400391
Matthew Treinish90ac9142014-03-17 14:58:37 +0000392 This method is used for translating the translatable values in method
393 arguments which include values of tuples or dictionaries.
394 If the object is not a tuple or a dictionary the object itself is
395 translated if it is translatable.
396
397 If the locale is None the object is translated to the system locale.
398
399 :param args: the args to translate
400 :param desired_locale: the locale to translate the args to, if None the
401 default system locale will be used
402 :returns: a new args object with the translated contents of the original
403 """
404 if isinstance(args, tuple):
405 return tuple(translate(v, desired_locale) for v in args)
406 if isinstance(args, dict):
407 translated_dict = {}
408 for (k, v) in six.iteritems(args):
409 translated_v = translate(v, desired_locale)
410 translated_dict[k] = translated_v
411 return translated_dict
412 return translate(args, desired_locale)
413
414
415class TranslationHandler(handlers.MemoryHandler):
416 """Handler that translates records before logging them.
417
418 The TranslationHandler takes a locale and a target logging.Handler object
419 to forward LogRecord objects to after translating them. This handler
420 depends on Message objects being logged, instead of regular strings.
421
422 The handler can be configured declaratively in the logging.conf as follows:
423
424 [handlers]
425 keys = translatedlog, translator
426
427 [handler_translatedlog]
428 class = handlers.WatchedFileHandler
429 args = ('/var/log/api-localized.log',)
430 formatter = context
431
432 [handler_translator]
433 class = openstack.common.log.TranslationHandler
434 target = translatedlog
435 args = ('zh_CN',)
436
437 If the specified locale is not available in the system, the handler will
438 log in the default locale.
Matthew Treinish0db53772013-07-26 10:39:35 -0400439 """
440
Matthew Treinish90ac9142014-03-17 14:58:37 +0000441 def __init__(self, locale=None, target=None):
442 """Initialize a TranslationHandler
Matthew Treinish0db53772013-07-26 10:39:35 -0400443
444 :param locale: locale to use for translating messages
445 :param target: logging.Handler object to forward
446 LogRecord objects to after translation
447 """
Matthew Treinish90ac9142014-03-17 14:58:37 +0000448 # NOTE(luisg): In order to allow this handler to be a wrapper for
449 # other handlers, such as a FileHandler, and still be able to
450 # configure it using logging.conf, this handler has to extend
451 # MemoryHandler because only the MemoryHandlers' logging.conf
452 # parsing is implemented such that it accepts a target handler.
453 handlers.MemoryHandler.__init__(self, capacity=0, target=target)
Matthew Treinish0db53772013-07-26 10:39:35 -0400454 self.locale = locale
Matthew Treinish90ac9142014-03-17 14:58:37 +0000455
456 def setFormatter(self, fmt):
457 self.target.setFormatter(fmt)
Matthew Treinish0db53772013-07-26 10:39:35 -0400458
459 def emit(self, record):
Matthew Treinish90ac9142014-03-17 14:58:37 +0000460 # We save the message from the original record to restore it
461 # after translation, so other handlers are not affected by this
462 original_msg = record.msg
463 original_args = record.args
464
465 try:
466 self._translate_and_log_record(record)
467 finally:
468 record.msg = original_msg
469 record.args = original_args
470
471 def _translate_and_log_record(self, record):
472 record.msg = translate(record.msg, self.locale)
473
474 # In addition to translating the message, we also need to translate
475 # arguments that were passed to the log method that were not part
476 # of the main message e.g., log.info(_('Some message %s'), this_one))
477 record.args = _translate_args(record.args, self.locale)
Matthew Treinish0db53772013-07-26 10:39:35 -0400478
479 self.target.emit(record)