1
# Copyright 2012 Red Hat, Inc.
2
# Copyright 2013 IBM Corp.
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
9
# http://www.apache.org/licenses/LICENSE-2.0
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
18
gettext for openstack-common modules.
20
Usual usage in an openstack.common module:
22
from oslo.vmware.openstack.common.gettextutils import _
29
from logging import handlers
33
from babel import localedata
36
_localedir = os.environ.get('oslo.vmware'.upper() + '_LOCALEDIR')
37
_t = gettext.translation('oslo.vmware', localedir=_localedir, fallback=True)
39
# We use separate translation catalogs for each log level, so set up a
40
# mapping between the log level name and the translator. The domain
41
# for the log level is project_name + "-log-" + log_level so messages
42
# for each level end up in their own catalog.
44
(level, gettext.translation('oslo.vmware' + '-log-' + level,
47
for level in ['info', 'warning', 'error', 'critical']
50
_AVAILABLE_LANGUAGES = {}
55
"""Convenience function for configuring _() to use lazy gettext
57
Call this at the start of execution to enable the gettextutils._
58
function to use lazy gettext functionality. This is useful if
59
your project is importing _ directly instead of using the
60
gettextutils.install() way of importing the _ function.
68
return Message(msg, domain='oslo.vmware')
71
return _t.gettext(msg)
72
return _t.ugettext(msg)
75
def _log_translation(msg, level):
76
"""Build a single translation of a log message
79
return Message(msg, domain='oslo.vmware' + '-log-' + level)
81
translator = _t_log_levels[level]
83
return translator.gettext(msg)
84
return translator.ugettext(msg)
86
# Translators for log levels.
88
# The abbreviated names are meant to reflect the usual use of a short
89
# name like '_'. The "L" is for "log" and the other letter comes from
91
_LI = functools.partial(_log_translation, level='info')
92
_LW = functools.partial(_log_translation, level='warning')
93
_LE = functools.partial(_log_translation, level='error')
94
_LC = functools.partial(_log_translation, level='critical')
97
def install(domain, lazy=False):
98
"""Install a _() function using the given translation domain.
100
Given a translation domain, install a _() function using gettext's
103
The main difference from gettext.install() is that we allow
104
overriding the default localedir (e.g. /usr/share/locale) using
105
a translation-domain-specific environment variable (e.g.
108
:param domain: the translation domain
109
:param lazy: indicates whether or not to install the lazy _() function.
110
The lazy _() introduces a way to do deferred translation
111
of messages by installing a _ that builds Message objects,
112
instead of strings, which can then be lazily translated into
113
any available locale.
116
# NOTE(mrodden): Lazy gettext functionality.
118
# The following introduces a deferred way to do translations on
119
# messages in OpenStack. We override the standard _() function
120
# and % (format string) operation to build Message objects that can
121
# later be translated when we have more information.
122
def _lazy_gettext(msg):
123
"""Create and return a Message object.
125
Lazy gettext function for a given domain, it is a factory method
126
for a project/module to get a lazy gettext function for its own
127
translation domain (i.e. nova, glance, cinder, etc.)
129
Message encapsulates a string so that we can translate
130
it later when needed.
132
return Message(msg, domain=domain)
134
from six import moves
135
moves.builtins.__dict__['_'] = _lazy_gettext
137
localedir = '%s_LOCALEDIR' % domain.upper()
139
gettext.install(domain,
140
localedir=os.environ.get(localedir))
142
gettext.install(domain,
143
localedir=os.environ.get(localedir),
147
class Message(six.text_type):
148
"""A Message object is a unicode object that can be translated.
150
Translation of Message is done explicitly using the translate() method.
151
For all non-translation intents and purposes, a Message is simply unicode,
152
and can be treated as such.
155
def __new__(cls, msgid, msgtext=None, params=None,
156
domain='oslo.vmware', *args):
157
"""Create a new Message object.
159
In order for translation to work gettext requires a message ID, this
160
msgid will be used as the base unicode text. It is also possible
161
for the msgid and the base unicode text to be different by passing
162
the msgtext parameter.
164
# If the base msgtext is not given, we use the default translation
165
# of the msgid (which is in English) just in case the system locale is
166
# not English, so that the base text will be in that locale by default.
168
msgtext = Message._translate_msgid(msgid, domain)
169
# We want to initialize the parent unicode with the actual object that
170
# would have been plain unicode if 'Message' was not enabled.
171
msg = super(Message, cls).__new__(cls, msgtext)
177
def translate(self, desired_locale=None):
178
"""Translate this message to the desired locale.
180
:param desired_locale: The desired locale to translate the message to,
181
if no locale is provided the message will be
182
translated to the system's default locale.
184
:returns: the translated message in unicode
187
translated_message = Message._translate_msgid(self.msgid,
190
if self.params is None:
191
# No need for more translation
192
return translated_message
194
# This Message object may have been formatted with one or more
195
# Message objects as substitution arguments, given either as a single
196
# argument, part of a tuple, or as one or more values in a dictionary.
197
# When translating this Message we need to translate those Messages too
198
translated_params = _translate_args(self.params, desired_locale)
200
translated_message = translated_message % translated_params
202
return translated_message
205
def _translate_msgid(msgid, domain, desired_locale=None):
206
if not desired_locale:
207
system_locale = locale.getdefaultlocale()
208
# If the system locale is not available to the runtime use English
209
if not system_locale[0]:
210
desired_locale = 'en_US'
212
desired_locale = system_locale[0]
214
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
215
lang = gettext.translation(domain,
216
localedir=locale_dir,
217
languages=[desired_locale],
220
translator = lang.gettext
222
translator = lang.ugettext
224
translated_message = translator(msgid)
225
return translated_message
227
def __mod__(self, other):
228
# When we mod a Message we want the actual operation to be performed
229
# by the parent class (i.e. unicode()), the only thing we do here is
230
# save the original msgid and the parameters in case of a translation
231
params = self._sanitize_mod_params(other)
232
unicode_mod = super(Message, self).__mod__(params)
233
modded = Message(self.msgid,
239
def _sanitize_mod_params(self, other):
240
"""Sanitize the object being modded with this Message.
242
- Add support for modding 'None' so translation supports it
243
- Trim the modded object, which can be a large dictionary, to only
244
those keys that would actually be used in a translation
245
- Snapshot the object being modded, in case the message is
246
translated, it will be used as it was when the Message was created
250
elif isinstance(other, dict):
251
params = self._trim_dictionary_parameters(other)
253
params = self._copy_param(other)
256
def _trim_dictionary_parameters(self, dict_param):
257
"""Return a dict that only has matching entries in the msgid."""
258
# NOTE(luisg): Here we trim down the dictionary passed as parameters
259
# to avoid carrying a lot of unnecessary weight around in the message
260
# object, for example if someone passes in Message() % locals() but
261
# only some params are used, and additionally we prevent errors for
262
# non-deepcopyable objects by unicoding() them.
264
# Look for %(param) keys in msgid;
265
# Skip %% and deal with the case where % is first character on the line
266
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
268
# If we don't find any %(param) keys but have a %s
269
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
270
# Apparently the full dictionary is the parameter
271
params = self._copy_param(dict_param)
274
# Save our existing parameters as defaults to protect
275
# ourselves from losing values if we are called through an
276
# (erroneous) chain that builds a valid Message with
277
# arguments, and then does something like "msg % kwds"
278
# where kwds is an empty dictionary.
280
if isinstance(self.params, dict):
281
src.update(self.params)
282
src.update(dict_param)
284
params[key] = self._copy_param(src[key])
288
def _copy_param(self, param):
290
return copy.deepcopy(param)
292
# Fallback to casting to unicode this will handle the
293
# python code-like objects that can't be deep-copied
294
return six.text_type(param)
296
def __add__(self, other):
297
msg = _('Message objects do not support addition.')
300
def __radd__(self, other):
301
return self.__add__(other)
304
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
305
# and it expects specifically a UnicodeError in order to proceed.
306
msg = _('Message objects do not support str() because they may '
307
'contain non-ascii characters. '
308
'Please use unicode() or translate() instead.')
309
raise UnicodeError(msg)
312
def get_available_languages(domain):
313
"""Lists the available languages for the given translation domain.
315
:param domain: the domain to get languages for
317
if domain in _AVAILABLE_LANGUAGES:
318
return copy.copy(_AVAILABLE_LANGUAGES[domain])
320
localedir = '%s_LOCALEDIR' % domain.upper()
321
find = lambda x: gettext.find(domain,
322
localedir=os.environ.get(localedir),
325
# NOTE(mrodden): en_US should always be available (and first in case
326
# order matters) since our in-line message strings are en_US
327
language_list = ['en_US']
328
# NOTE(luisg): Babel <1.0 used a function called list(), which was
329
# renamed to locale_identifiers() in >=1.0, the requirements master list
330
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
331
# this check when the master list updates to >=1.0, and update all projects
332
list_identifiers = (getattr(localedata, 'list', None) or
333
getattr(localedata, 'locale_identifiers'))
334
locale_identifiers = list_identifiers()
336
for i in locale_identifiers:
337
if find(i) is not None:
338
language_list.append(i)
340
# NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
341
# locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
342
# are perfectly legitimate locales:
343
# https://github.com/mitsuhiko/babel/issues/37
344
# In Babel 1.3 they fixed the bug and they support these locales, but
345
# they are still not explicitly "listed" by locale_identifiers().
346
# That is why we add the locales here explicitly if necessary so that
347
# they are listed as supported.
348
aliases = {'zh': 'zh_CN',
349
'zh_Hant_HK': 'zh_HK',
352
for (locale, alias) in six.iteritems(aliases):
353
if locale in language_list and alias not in language_list:
354
language_list.append(alias)
356
_AVAILABLE_LANGUAGES[domain] = language_list
357
return copy.copy(language_list)
360
def translate(obj, desired_locale=None):
361
"""Gets the translated unicode representation of the given object.
363
If the object is not translatable it is returned as-is.
364
If the locale is None the object is translated to the system locale.
366
:param obj: the object to translate
367
:param desired_locale: the locale to translate the message to, if None the
368
default system locale will be used
369
:returns: the translated object in unicode, or the original object if
370
it could not be translated
373
if not isinstance(message, Message):
374
# If the object to translate is not already translatable,
375
# let's first get its unicode representation
376
message = six.text_type(obj)
377
if isinstance(message, Message):
378
# Even after unicoding() we still need to check if we are
379
# running with translatable unicode before translating
380
return message.translate(desired_locale)
384
def _translate_args(args, desired_locale=None):
385
"""Translates all the translatable elements of the given arguments object.
387
This method is used for translating the translatable values in method
388
arguments which include values of tuples or dictionaries.
389
If the object is not a tuple or a dictionary the object itself is
390
translated if it is translatable.
392
If the locale is None the object is translated to the system locale.
394
:param args: the args to translate
395
:param desired_locale: the locale to translate the args to, if None the
396
default system locale will be used
397
:returns: a new args object with the translated contents of the original
399
if isinstance(args, tuple):
400
return tuple(translate(v, desired_locale) for v in args)
401
if isinstance(args, dict):
403
for (k, v) in six.iteritems(args):
404
translated_v = translate(v, desired_locale)
405
translated_dict[k] = translated_v
406
return translated_dict
407
return translate(args, desired_locale)
410
class TranslationHandler(handlers.MemoryHandler):
411
"""Handler that translates records before logging them.
413
The TranslationHandler takes a locale and a target logging.Handler object
414
to forward LogRecord objects to after translating them. This handler
415
depends on Message objects being logged, instead of regular strings.
417
The handler can be configured declaratively in the logging.conf as follows:
420
keys = translatedlog, translator
422
[handler_translatedlog]
423
class = handlers.WatchedFileHandler
424
args = ('/var/log/api-localized.log',)
428
class = openstack.common.log.TranslationHandler
429
target = translatedlog
432
If the specified locale is not available in the system, the handler will
433
log in the default locale.
436
def __init__(self, locale=None, target=None):
437
"""Initialize a TranslationHandler
439
:param locale: locale to use for translating messages
440
:param target: logging.Handler object to forward
441
LogRecord objects to after translation
443
# NOTE(luisg): In order to allow this handler to be a wrapper for
444
# other handlers, such as a FileHandler, and still be able to
445
# configure it using logging.conf, this handler has to extend
446
# MemoryHandler because only the MemoryHandlers' logging.conf
447
# parsing is implemented such that it accepts a target handler.
448
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
451
def setFormatter(self, fmt):
452
self.target.setFormatter(fmt)
454
def emit(self, record):
455
# We save the message from the original record to restore it
456
# after translation, so other handlers are not affected by this
457
original_msg = record.msg
458
original_args = record.args
461
self._translate_and_log_record(record)
463
record.msg = original_msg
464
record.args = original_args
466
def _translate_and_log_record(self, record):
467
record.msg = translate(record.msg, self.locale)
469
# In addition to translating the message, we also need to translate
470
# arguments that were passed to the log method that were not part
471
# of the main message e.g., log.info(_('Some message %s'), this_one))
472
record.args = _translate_args(record.args, self.locale)
474
self.target.emit(record)