123
class Message(_userString.UserString, object):
124
"""Class used to encapsulate translatable messages."""
125
def __init__(self, msg, domain):
126
# _msg is the gettext msgid and should never change
128
self._left_extra_msg = ''
129
self._right_extra_msg = ''
136
# NOTE(mrodden): this should always resolve to a unicode string
137
# that best represents the state of the message currently
139
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
141
lang = gettext.translation(self.domain,
143
languages=[self.locale],
146
# use system locale for translations
147
lang = gettext.translation(self.domain,
113
class Message(six.text_type):
114
"""A Message object is a unicode object that can be translated.
116
Translation of Message is done explicitly using the translate() method.
117
For all non-translation intents and purposes, a Message is simply unicode,
118
and can be treated as such.
121
def __new__(cls, msgid, msgtext=None, params=None,
122
domain='cinderclient', *args):
123
"""Create a new Message object.
125
In order for translation to work gettext requires a message ID, this
126
msgid will be used as the base unicode text. It is also possible
127
for the msgid and the base unicode text to be different by passing
128
the msgtext parameter.
130
# If the base msgtext is not given, we use the default translation
131
# of the msgid (which is in English) just in case the system locale is
132
# not English, so that the base text will be in that locale by default.
134
msgtext = Message._translate_msgid(msgid, domain)
135
# We want to initialize the parent unicode with the actual object that
136
# would have been plain unicode if 'Message' was not enabled.
137
msg = super(Message, cls).__new__(cls, msgtext)
143
def translate(self, desired_locale=None):
144
"""Translate this message to the desired locale.
146
:param desired_locale: The desired locale to translate the message to,
147
if no locale is provided the message will be
148
translated to the system's default locale.
150
:returns: the translated message in unicode
153
translated_message = Message._translate_msgid(self.msgid,
156
if self.params is None:
157
# No need for more translation
158
return translated_message
160
# This Message object may have been formatted with one or more
161
# Message objects as substitution arguments, given either as a single
162
# argument, part of a tuple, or as one or more values in a dictionary.
163
# When translating this Message we need to translate those Messages too
164
translated_params = _translate_args(self.params, desired_locale)
166
translated_message = translated_message % translated_params
168
return translated_message
171
def _translate_msgid(msgid, domain, desired_locale=None):
172
if not desired_locale:
173
system_locale = locale.getdefaultlocale()
174
# If the system locale is not available to the runtime use English
175
if not system_locale[0]:
176
desired_locale = 'en_US'
178
desired_locale = system_locale[0]
180
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
181
lang = gettext.translation(domain,
182
localedir=locale_dir,
183
languages=[desired_locale],
152
ugettext = lang.gettext
154
ugettext = lang.ugettext
156
full_msg = (self._left_extra_msg +
157
ugettext(self._msg) +
158
self._right_extra_msg)
160
if self.params is not None:
161
full_msg = full_msg % self.params
163
return six.text_type(full_msg)
170
def locale(self, value):
175
# This Message object may have been constructed with one or more
176
# Message objects as substitution parameters, given as a single
177
# Message, or a tuple or Map containing some, so when setting the
178
# locale for this Message we need to set it for those Messages too.
179
if isinstance(self.params, Message):
180
self.params.locale = value
182
if isinstance(self.params, tuple):
183
for param in self.params:
184
if isinstance(param, Message):
187
if isinstance(self.params, dict):
188
for param in self.params.values():
189
if isinstance(param, Message):
192
def _save_dictionary_parameter(self, dict_param):
194
# look for %(blah) fields in string;
195
# ignore %% and deal with the
196
# case where % is first character on the line
197
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg)
199
# if we don't find any %(blah) blocks but have a %s
200
if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg):
201
# apparently the full dictionary is the parameter
202
params = copy.deepcopy(dict_param)
186
translator = lang.gettext
188
translator = lang.ugettext
190
translated_message = translator(msgid)
191
return translated_message
193
def __mod__(self, other):
194
# When we mod a Message we want the actual operation to be performed
195
# by the parent class (i.e. unicode()), the only thing we do here is
196
# save the original msgid and the parameters in case of a translation
197
params = self._sanitize_mod_params(other)
198
unicode_mod = super(Message, self).__mod__(params)
199
modded = Message(self.msgid,
205
def _sanitize_mod_params(self, other):
206
"""Sanitize the object being modded with this Message.
208
- Add support for modding 'None' so translation supports it
209
- Trim the modded object, which can be a large dictionary, to only
210
those keys that would actually be used in a translation
211
- Snapshot the object being modded, in case the message is
212
translated, it will be used as it was when the Message was created
216
elif isinstance(other, dict):
217
params = self._trim_dictionary_parameters(other)
219
params = self._copy_param(other)
222
def _trim_dictionary_parameters(self, dict_param):
223
"""Return a dict that only has matching entries in the msgid."""
224
# NOTE(luisg): Here we trim down the dictionary passed as parameters
225
# to avoid carrying a lot of unnecessary weight around in the message
226
# object, for example if someone passes in Message() % locals() but
227
# only some params are used, and additionally we prevent errors for
228
# non-deepcopyable objects by unicoding() them.
230
# Look for %(param) keys in msgid;
231
# Skip %% and deal with the case where % is first character on the line
232
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
234
# If we don't find any %(param) keys but have a %s
235
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
236
# Apparently the full dictionary is the parameter
237
params = self._copy_param(dict_param)
240
# Save our existing parameters as defaults to protect
241
# ourselves from losing values if we are called through an
242
# (erroneous) chain that builds a valid Message with
243
# arguments, and then does something like "msg % kwds"
244
# where kwds is an empty dictionary.
246
if isinstance(self.params, dict):
247
src.update(self.params)
248
src.update(dict_param)
207
params[key] = copy.deepcopy(dict_param[key])
209
# cast uncopyable thing to unicode string
210
params[key] = six.text_type(dict_param[key])
250
params[key] = self._copy_param(src[key])
214
def _save_parameters(self, other):
215
# we check for None later to see if
216
# we actually have parameters to inject,
217
# so encapsulate if our parameter is actually None
219
self.params = (other, )
220
elif isinstance(other, dict):
221
self.params = self._save_dictionary_parameter(other)
223
# fallback to casting to unicode,
224
# this will handle the problematic python code-like
225
# objects that cannot be deep-copied
227
self.params = copy.deepcopy(other)
229
self.params = six.text_type(other)
233
# overrides to be more string-like
234
def __unicode__(self):
239
return self.__unicode__()
240
return self.data.encode('utf-8')
242
def __getstate__(self):
243
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
244
'domain', 'params', '_locale']
245
new_dict = self.__dict__.fromkeys(to_copy)
247
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
251
def __setstate__(self, state):
252
for (k, v) in state.items():
254
def _copy_param(self, param):
256
return copy.deepcopy(param)
258
# Fallback to casting to unicode this will handle the
259
# python code-like objects that can't be deep-copied
260
return six.text_type(param)
256
262
def __add__(self, other):
257
copied = copy.deepcopy(self)
258
copied._right_extra_msg += other.__str__()
263
msg = _('Message objects do not support addition.')
261
266
def __radd__(self, other):
262
copied = copy.deepcopy(self)
263
copied._left_extra_msg += other.__str__()
266
def __mod__(self, other):
267
# do a format string to catch and raise
268
# any possible KeyErrors from missing parameters
270
copied = copy.deepcopy(self)
271
return copied._save_parameters(other)
273
def __mul__(self, other):
274
return self.data * other
276
def __rmul__(self, other):
277
return other * self.data
279
def __getitem__(self, key):
280
return self.data[key]
282
def __getslice__(self, start, end):
283
return self.data.__getslice__(start, end)
285
def __getattribute__(self, name):
286
# NOTE(mrodden): handle lossy operations that we can't deal with yet
287
# These override the UserString implementation, since UserString
288
# uses our __class__ attribute to try and build a new message
289
# after running the inner data string through the operation.
290
# At that point, we have lost the gettext message id and can just
291
# safely resolve to a string instead.
292
ops = ['capitalize', 'center', 'decode', 'encode',
293
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
294
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
296
return getattr(self.data, name)
298
return _userString.UserString.__getattribute__(self, name)
267
return self.__add__(other)
270
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
271
# and it expects specifically a UnicodeError in order to proceed.
272
msg = _('Message objects do not support str() because they may '
273
'contain non-ascii characters. '
274
'Please use unicode() or translate() instead.')
275
raise UnicodeError(msg)
301
278
def get_available_languages(domain):
317
294
# NOTE(luisg): Babel <1.0 used a function called list(), which was
318
295
# renamed to locale_identifiers() in >=1.0, the requirements master list
319
296
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
320
# this check when the master list updates to >=1.0, and all projects udpate
297
# this check when the master list updates to >=1.0, and update all projects
321
298
list_identifiers = (getattr(localedata, 'list', None) or
322
299
getattr(localedata, 'locale_identifiers'))
323
300
locale_identifiers = list_identifiers()
324
302
for i in locale_identifiers:
325
303
if find(i) is not None:
326
304
language_list.append(i)
306
# NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported
307
# locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they
308
# are perfectly legitimate locales:
309
# https://github.com/mitsuhiko/babel/issues/37
310
# In Babel 1.3 they fixed the bug and they support these locales, but
311
# they are still not explicitly "listed" by locale_identifiers().
312
# That is why we add the locales here explicitly if necessary so that
313
# they are listed as supported.
314
aliases = {'zh': 'zh_CN',
315
'zh_Hant_HK': 'zh_HK',
318
for (locale, alias) in six.iteritems(aliases):
319
if locale in language_list and alias not in language_list:
320
language_list.append(alias)
327
322
_AVAILABLE_LANGUAGES[domain] = language_list
328
323
return copy.copy(language_list)
331
def get_localized_message(message, user_locale):
332
"""Gets a localized version of the given message in the given locale."""
326
def translate(obj, desired_locale=None):
327
"""Gets the translated unicode representation of the given object.
329
If the object is not translatable it is returned as-is.
330
If the locale is None the object is translated to the system locale.
332
:param obj: the object to translate
333
:param desired_locale: the locale to translate the message to, if None the
334
default system locale will be used
335
:returns: the translated object in unicode, or the original object if
336
it could not be translated
339
if not isinstance(message, Message):
340
# If the object to translate is not already translatable,
341
# let's first get its unicode representation
342
message = six.text_type(obj)
333
343
if isinstance(message, Message):
335
message.locale = user_locale
336
return six.text_type(message)
341
class LocaleHandler(logging.Handler):
342
"""Handler that can have a locale associated to translate Messages.
344
A quick example of how to utilize the Message class above.
345
LocaleHandler takes a locale and a target logging.Handler object
346
to forward LogRecord objects to after translating the internal Message.
349
def __init__(self, locale, target):
350
"""Initialize a LocaleHandler
344
# Even after unicoding() we still need to check if we are
345
# running with translatable unicode before translating
346
return message.translate(desired_locale)
350
def _translate_args(args, desired_locale=None):
351
"""Translates all the translatable elements of the given arguments object.
353
This method is used for translating the translatable values in method
354
arguments which include values of tuples or dictionaries.
355
If the object is not a tuple or a dictionary the object itself is
356
translated if it is translatable.
358
If the locale is None the object is translated to the system locale.
360
:param args: the args to translate
361
:param desired_locale: the locale to translate the args to, if None the
362
default system locale will be used
363
:returns: a new args object with the translated contents of the original
365
if isinstance(args, tuple):
366
return tuple(translate(v, desired_locale) for v in args)
367
if isinstance(args, dict):
369
for (k, v) in six.iteritems(args):
370
translated_v = translate(v, desired_locale)
371
translated_dict[k] = translated_v
372
return translated_dict
373
return translate(args, desired_locale)
376
class TranslationHandler(handlers.MemoryHandler):
377
"""Handler that translates records before logging them.
379
The TranslationHandler takes a locale and a target logging.Handler object
380
to forward LogRecord objects to after translating them. This handler
381
depends on Message objects being logged, instead of regular strings.
383
The handler can be configured declaratively in the logging.conf as follows:
386
keys = translatedlog, translator
388
[handler_translatedlog]
389
class = handlers.WatchedFileHandler
390
args = ('/var/log/api-localized.log',)
394
class = openstack.common.log.TranslationHandler
395
target = translatedlog
398
If the specified locale is not available in the system, the handler will
399
log in the default locale.
402
def __init__(self, locale=None, target=None):
403
"""Initialize a TranslationHandler
352
405
:param locale: locale to use for translating messages
353
406
:param target: logging.Handler object to forward
354
407
LogRecord objects to after translation
356
logging.Handler.__init__(self)
409
# NOTE(luisg): In order to allow this handler to be a wrapper for
410
# other handlers, such as a FileHandler, and still be able to
411
# configure it using logging.conf, this handler has to extend
412
# MemoryHandler because only the MemoryHandlers' logging.conf
413
# parsing is implemented such that it accepts a target handler.
414
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
357
415
self.locale = locale
417
def setFormatter(self, fmt):
418
self.target.setFormatter(fmt)
360
420
def emit(self, record):
361
if isinstance(record.msg, Message):
362
# set the locale and resolve to a string
363
record.msg.locale = self.locale
421
# We save the message from the original record to restore it
422
# after translation, so other handlers are not affected by this
423
original_msg = record.msg
424
original_args = record.args
427
self._translate_and_log_record(record)
429
record.msg = original_msg
430
record.args = original_args
432
def _translate_and_log_record(self, record):
433
record.msg = translate(record.msg, self.locale)
435
# In addition to translating the message, we also need to translate
436
# arguments that were passed to the log method that were not part
437
# of the main message e.g., log.info(_('Some message %s'), this_one))
438
record.args = _translate_args(record.args, self.locale)
365
440
self.target.emit(record)