~ubuntu-branches/ubuntu/wily/python-oslo.vmware/wily

« back to all changes in this revision

Viewing changes to oslo/vmware/openstack/common/gettextutils.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2014-03-05 15:29:17 UTC
  • Revision ID: package-import@ubuntu.com-20140305152917-9n6zp4cktcwyr3ul
Tags: upstream-0.2
ImportĀ upstreamĀ versionĀ 0.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2012 Red Hat, Inc.
 
2
# Copyright 2013 IBM Corp.
 
3
# All Rights Reserved.
 
4
#
 
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
"""
 
18
gettext for openstack-common modules.
 
19
 
 
20
Usual usage in an openstack.common module:
 
21
 
 
22
    from oslo.vmware.openstack.common.gettextutils import _
 
23
"""
 
24
 
 
25
import copy
 
26
import functools
 
27
import gettext
 
28
import locale
 
29
from logging import handlers
 
30
import os
 
31
import re
 
32
 
 
33
from babel import localedata
 
34
import six
 
35
 
 
36
_localedir = os.environ.get('oslo.vmware'.upper() + '_LOCALEDIR')
 
37
_t = gettext.translation('oslo.vmware', localedir=_localedir, fallback=True)
 
38
 
 
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.
 
43
_t_log_levels = dict(
 
44
    (level, gettext.translation('oslo.vmware' + '-log-' + level,
 
45
                                localedir=_localedir,
 
46
                                fallback=True))
 
47
    for level in ['info', 'warning', 'error', 'critical']
 
48
)
 
49
 
 
50
_AVAILABLE_LANGUAGES = {}
 
51
USE_LAZY = False
 
52
 
 
53
 
 
54
def enable_lazy():
 
55
    """Convenience function for configuring _() to use lazy gettext
 
56
 
 
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.
 
61
    """
 
62
    global USE_LAZY
 
63
    USE_LAZY = True
 
64
 
 
65
 
 
66
def _(msg):
 
67
    if USE_LAZY:
 
68
        return Message(msg, domain='oslo.vmware')
 
69
    else:
 
70
        if six.PY3:
 
71
            return _t.gettext(msg)
 
72
        return _t.ugettext(msg)
 
73
 
 
74
 
 
75
def _log_translation(msg, level):
 
76
    """Build a single translation of a log message
 
77
    """
 
78
    if USE_LAZY:
 
79
        return Message(msg, domain='oslo.vmware' + '-log-' + level)
 
80
    else:
 
81
        translator = _t_log_levels[level]
 
82
        if six.PY3:
 
83
            return translator.gettext(msg)
 
84
        return translator.ugettext(msg)
 
85
 
 
86
# Translators for log levels.
 
87
#
 
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
 
90
# the level.
 
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')
 
95
 
 
96
 
 
97
def install(domain, lazy=False):
 
98
    """Install a _() function using the given translation domain.
 
99
 
 
100
    Given a translation domain, install a _() function using gettext's
 
101
    install() function.
 
102
 
 
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.
 
106
    NOVA_LOCALEDIR).
 
107
 
 
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.
 
114
    """
 
115
    if lazy:
 
116
        # NOTE(mrodden): Lazy gettext functionality.
 
117
        #
 
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.
 
124
 
 
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.)
 
128
 
 
129
            Message encapsulates a string so that we can translate
 
130
            it later when needed.
 
131
            """
 
132
            return Message(msg, domain=domain)
 
133
 
 
134
        from six import moves
 
135
        moves.builtins.__dict__['_'] = _lazy_gettext
 
136
    else:
 
137
        localedir = '%s_LOCALEDIR' % domain.upper()
 
138
        if six.PY3:
 
139
            gettext.install(domain,
 
140
                            localedir=os.environ.get(localedir))
 
141
        else:
 
142
            gettext.install(domain,
 
143
                            localedir=os.environ.get(localedir),
 
144
                            unicode=True)
 
145
 
 
146
 
 
147
class Message(six.text_type):
 
148
    """A Message object is a unicode object that can be translated.
 
149
 
 
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.
 
153
    """
 
154
 
 
155
    def __new__(cls, msgid, msgtext=None, params=None,
 
156
                domain='oslo.vmware', *args):
 
157
        """Create a new Message object.
 
158
 
 
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.
 
163
        """
 
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.
 
167
        if not msgtext:
 
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)
 
172
        msg.msgid = msgid
 
173
        msg.domain = domain
 
174
        msg.params = params
 
175
        return msg
 
176
 
 
177
    def translate(self, desired_locale=None):
 
178
        """Translate this message to the desired locale.
 
179
 
 
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.
 
183
 
 
184
        :returns: the translated message in unicode
 
185
        """
 
186
 
 
187
        translated_message = Message._translate_msgid(self.msgid,
 
188
                                                      self.domain,
 
189
                                                      desired_locale)
 
190
        if self.params is None:
 
191
            # No need for more translation
 
192
            return translated_message
 
193
 
 
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)
 
199
 
 
200
        translated_message = translated_message % translated_params
 
201
 
 
202
        return translated_message
 
203
 
 
204
    @staticmethod
 
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'
 
211
            else:
 
212
                desired_locale = system_locale[0]
 
213
 
 
214
        locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
 
215
        lang = gettext.translation(domain,
 
216
                                   localedir=locale_dir,
 
217
                                   languages=[desired_locale],
 
218
                                   fallback=True)
 
219
        if six.PY3:
 
220
            translator = lang.gettext
 
221
        else:
 
222
            translator = lang.ugettext
 
223
 
 
224
        translated_message = translator(msgid)
 
225
        return translated_message
 
226
 
 
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,
 
234
                         msgtext=unicode_mod,
 
235
                         params=params,
 
236
                         domain=self.domain)
 
237
        return modded
 
238
 
 
239
    def _sanitize_mod_params(self, other):
 
240
        """Sanitize the object being modded with this Message.
 
241
 
 
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
 
247
        """
 
248
        if other is None:
 
249
            params = (other,)
 
250
        elif isinstance(other, dict):
 
251
            params = self._trim_dictionary_parameters(other)
 
252
        else:
 
253
            params = self._copy_param(other)
 
254
        return params
 
255
 
 
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.
 
263
 
 
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)
 
267
 
 
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)
 
272
        else:
 
273
            params = {}
 
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.
 
279
            src = {}
 
280
            if isinstance(self.params, dict):
 
281
                src.update(self.params)
 
282
            src.update(dict_param)
 
283
            for key in keys:
 
284
                params[key] = self._copy_param(src[key])
 
285
 
 
286
        return params
 
287
 
 
288
    def _copy_param(self, param):
 
289
        try:
 
290
            return copy.deepcopy(param)
 
291
        except TypeError:
 
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)
 
295
 
 
296
    def __add__(self, other):
 
297
        msg = _('Message objects do not support addition.')
 
298
        raise TypeError(msg)
 
299
 
 
300
    def __radd__(self, other):
 
301
        return self.__add__(other)
 
302
 
 
303
    def __str__(self):
 
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)
 
310
 
 
311
 
 
312
def get_available_languages(domain):
 
313
    """Lists the available languages for the given translation domain.
 
314
 
 
315
    :param domain: the domain to get languages for
 
316
    """
 
317
    if domain in _AVAILABLE_LANGUAGES:
 
318
        return copy.copy(_AVAILABLE_LANGUAGES[domain])
 
319
 
 
320
    localedir = '%s_LOCALEDIR' % domain.upper()
 
321
    find = lambda x: gettext.find(domain,
 
322
                                  localedir=os.environ.get(localedir),
 
323
                                  languages=[x])
 
324
 
 
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()
 
335
 
 
336
    for i in locale_identifiers:
 
337
        if find(i) is not None:
 
338
            language_list.append(i)
 
339
 
 
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',
 
350
               'zh_Hant': 'zh_TW',
 
351
               'fil': 'tl_PH'}
 
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)
 
355
 
 
356
    _AVAILABLE_LANGUAGES[domain] = language_list
 
357
    return copy.copy(language_list)
 
358
 
 
359
 
 
360
def translate(obj, desired_locale=None):
 
361
    """Gets the translated unicode representation of the given object.
 
362
 
 
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.
 
365
 
 
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
 
371
    """
 
372
    message = obj
 
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)
 
381
    return obj
 
382
 
 
383
 
 
384
def _translate_args(args, desired_locale=None):
 
385
    """Translates all the translatable elements of the given arguments object.
 
386
 
 
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.
 
391
 
 
392
    If the locale is None the object is translated to the system locale.
 
393
 
 
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
 
398
    """
 
399
    if isinstance(args, tuple):
 
400
        return tuple(translate(v, desired_locale) for v in args)
 
401
    if isinstance(args, dict):
 
402
        translated_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)
 
408
 
 
409
 
 
410
class TranslationHandler(handlers.MemoryHandler):
 
411
    """Handler that translates records before logging them.
 
412
 
 
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.
 
416
 
 
417
    The handler can be configured declaratively in the logging.conf as follows:
 
418
 
 
419
        [handlers]
 
420
        keys = translatedlog, translator
 
421
 
 
422
        [handler_translatedlog]
 
423
        class = handlers.WatchedFileHandler
 
424
        args = ('/var/log/api-localized.log',)
 
425
        formatter = context
 
426
 
 
427
        [handler_translator]
 
428
        class = openstack.common.log.TranslationHandler
 
429
        target = translatedlog
 
430
        args = ('zh_CN',)
 
431
 
 
432
    If the specified locale is not available in the system, the handler will
 
433
    log in the default locale.
 
434
    """
 
435
 
 
436
    def __init__(self, locale=None, target=None):
 
437
        """Initialize a TranslationHandler
 
438
 
 
439
        :param locale: locale to use for translating messages
 
440
        :param target: logging.Handler object to forward
 
441
                       LogRecord objects to after translation
 
442
        """
 
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)
 
449
        self.locale = locale
 
450
 
 
451
    def setFormatter(self, fmt):
 
452
        self.target.setFormatter(fmt)
 
453
 
 
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
 
459
 
 
460
        try:
 
461
            self._translate_and_log_record(record)
 
462
        finally:
 
463
            record.msg = original_msg
 
464
            record.args = original_args
 
465
 
 
466
    def _translate_and_log_record(self, record):
 
467
        record.msg = translate(record.msg, self.locale)
 
468
 
 
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)
 
473
 
 
474
        self.target.emit(record)