1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2012 Red Hat, Inc.
4
# Copyright 2013 IBM Corp.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
20
gettext for openstack-common modules.
22
Usual usage in an openstack.common module:
24
from libraclient.openstack.common.gettextutils import _
33
import UserString as _userString
35
import collections as _userString
37
from babel import localedata
40
_localedir = os.environ.get('libraclient'.upper() + '_LOCALEDIR')
41
_t = gettext.translation('libraclient', localedir=_localedir, fallback=True)
43
_AVAILABLE_LANGUAGES = {}
48
"""Convenience function for configuring _() to use lazy gettext
50
Call this at the start of execution to enable the gettextutils._
51
function to use lazy gettext functionality. This is useful if
52
your project is importing _ directly instead of using the
53
gettextutils.install() way of importing the _ function.
61
return Message(msg, 'libraclient')
64
return _t.gettext(msg)
65
return _t.ugettext(msg)
68
def install(domain, lazy=False):
69
"""Install a _() function using the given translation domain.
71
Given a translation domain, install a _() function using gettext's
74
The main difference from gettext.install() is that we allow
75
overriding the default localedir (e.g. /usr/share/locale) using
76
a translation-domain-specific environment variable (e.g.
79
:param domain: the translation domain
80
:param lazy: indicates whether or not to install the lazy _() function.
81
The lazy _() introduces a way to do deferred translation
82
of messages by installing a _ that builds Message objects,
83
instead of strings, which can then be lazily translated into
87
# NOTE(mrodden): Lazy gettext functionality.
89
# The following introduces a deferred way to do translations on
90
# messages in OpenStack. We override the standard _() function
91
# and % (format string) operation to build Message objects that can
92
# later be translated when we have more information.
94
# Also included below is an example LocaleHandler that translates
95
# Messages to an associated locale, effectively allowing many logs,
96
# each with their own locale.
98
def _lazy_gettext(msg):
99
"""Create and return a Message object.
101
Lazy gettext function for a given domain, it is a factory method
102
for a project/module to get a lazy gettext function for its own
103
translation domain (i.e. nova, glance, cinder, etc.)
105
Message encapsulates a string so that we can translate
106
it later when needed.
108
return Message(msg, domain)
110
from six import moves
111
moves.builtins.__dict__['_'] = _lazy_gettext
113
localedir = '%s_LOCALEDIR' % domain.upper()
115
gettext.install(domain,
116
localedir=os.environ.get(localedir))
118
gettext.install(domain,
119
localedir=os.environ.get(localedir),
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,
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)
207
params[key] = copy.deepcopy(dict_param[key])
209
# cast uncopyable thing to unicode string
210
params[key] = six.text_type(dict_param[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():
256
def __add__(self, other):
257
copied = copy.deepcopy(self)
258
copied._right_extra_msg += other.__str__()
261
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)
301
def get_available_languages(domain):
302
"""Lists the available languages for the given translation domain.
304
:param domain: the domain to get languages for
306
if domain in _AVAILABLE_LANGUAGES:
307
return copy.copy(_AVAILABLE_LANGUAGES[domain])
309
localedir = '%s_LOCALEDIR' % domain.upper()
310
find = lambda x: gettext.find(domain,
311
localedir=os.environ.get(localedir),
314
# NOTE(mrodden): en_US should always be available (and first in case
315
# order matters) since our in-line message strings are en_US
316
language_list = ['en_US']
317
# NOTE(luisg): Babel <1.0 used a function called list(), which was
318
# renamed to locale_identifiers() in >=1.0, the requirements master list
319
# 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
321
list_identifiers = (getattr(localedata, 'list', None) or
322
getattr(localedata, 'locale_identifiers'))
323
locale_identifiers = list_identifiers()
324
for i in locale_identifiers:
325
if find(i) is not None:
326
language_list.append(i)
327
_AVAILABLE_LANGUAGES[domain] = language_list
328
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.
334
If the message is not a Message object the message is returned as-is.
335
If the locale is None the message is translated to the default locale.
337
:returns: the translated message in unicode, or the original message if
338
it could not be translated
341
if isinstance(message, Message):
342
original_locale = message.locale
343
message.locale = user_locale
344
translated = six.text_type(message)
345
message.locale = original_locale
349
class LocaleHandler(logging.Handler):
350
"""Handler that can have a locale associated to translate Messages.
352
A quick example of how to utilize the Message class above.
353
LocaleHandler takes a locale and a target logging.Handler object
354
to forward LogRecord objects to after translating the internal Message.
357
def __init__(self, locale, target):
358
"""Initialize a LocaleHandler
360
:param locale: locale to use for translating messages
361
:param target: logging.Handler object to forward
362
LogRecord objects to after translation
364
logging.Handler.__init__(self)
368
def emit(self, record):
369
if isinstance(record.msg, Message):
370
# set the locale and resolve to a string
371
record.msg.locale = self.locale
373
self.target.emit(record)