~ubuntu-branches/ubuntu/trusty/python-cinderclient/trusty

« back to all changes in this revision

Viewing changes to cinderclient/openstack/common/gettextutils.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2014-03-06 15:15:14 UTC
  • mfrom: (1.1.11)
  • Revision ID: package-import@ubuntu.com-20140306151514-ihbixyaopbhaqdju
Tags: 1:1.0.8-0ubuntu1
* New upstream release.
* debian/patches/fix-search-opts.patch: Dropped no longer needed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
 
 
3
1
# Copyright 2012 Red Hat, Inc.
4
2
# Copyright 2013 IBM Corp.
5
3
# All Rights Reserved.
26
24
 
27
25
import copy
28
26
import gettext
29
 
import logging
 
27
import locale
 
28
from logging import handlers
30
29
import os
31
30
import re
32
 
try:
33
 
    import UserString as _userString
34
 
except ImportError:
35
 
    import collections as _userString
36
31
 
37
32
from babel import localedata
38
33
import six
58
53
 
59
54
def _(msg):
60
55
    if USE_LAZY:
61
 
        return Message(msg, 'cinderclient')
 
56
        return Message(msg, domain='cinderclient')
62
57
    else:
63
58
        if six.PY3:
64
59
            return _t.gettext(msg)
90
85
        # messages in OpenStack. We override the standard _() function
91
86
        # and % (format string) operation to build Message objects that can
92
87
        # later be translated when we have more information.
93
 
        #
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.
97
 
 
98
88
        def _lazy_gettext(msg):
99
89
            """Create and return a Message object.
100
90
 
105
95
            Message encapsulates a string so that we can translate
106
96
            it later when needed.
107
97
            """
108
 
            return Message(msg, domain)
 
98
            return Message(msg, domain=domain)
109
99
 
110
100
        from six import moves
111
101
        moves.builtins.__dict__['_'] = _lazy_gettext
120
110
                            unicode=True)
121
111
 
122
112
 
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
127
 
        self._msg = msg
128
 
        self._left_extra_msg = ''
129
 
        self._right_extra_msg = ''
130
 
        self._locale = None
131
 
        self.params = None
132
 
        self.domain = domain
133
 
 
134
 
    @property
135
 
    def data(self):
136
 
        # NOTE(mrodden): this should always resolve to a unicode string
137
 
        # that best represents the state of the message currently
138
 
 
139
 
        localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
140
 
        if self.locale:
141
 
            lang = gettext.translation(self.domain,
142
 
                                       localedir=localedir,
143
 
                                       languages=[self.locale],
144
 
                                       fallback=True)
145
 
        else:
146
 
            # use system locale for translations
147
 
            lang = gettext.translation(self.domain,
148
 
                                       localedir=localedir,
149
 
                                       fallback=True)
150
 
 
 
113
class Message(six.text_type):
 
114
    """A Message object is a unicode object that can be translated.
 
115
 
 
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.
 
119
    """
 
120
 
 
121
    def __new__(cls, msgid, msgtext=None, params=None,
 
122
                domain='cinderclient', *args):
 
123
        """Create a new Message object.
 
124
 
 
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.
 
129
        """
 
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.
 
133
        if not msgtext:
 
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)
 
138
        msg.msgid = msgid
 
139
        msg.domain = domain
 
140
        msg.params = params
 
141
        return msg
 
142
 
 
143
    def translate(self, desired_locale=None):
 
144
        """Translate this message to the desired locale.
 
145
 
 
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.
 
149
 
 
150
        :returns: the translated message in unicode
 
151
        """
 
152
 
 
153
        translated_message = Message._translate_msgid(self.msgid,
 
154
                                                      self.domain,
 
155
                                                      desired_locale)
 
156
        if self.params is None:
 
157
            # No need for more translation
 
158
            return translated_message
 
159
 
 
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)
 
165
 
 
166
        translated_message = translated_message % translated_params
 
167
 
 
168
        return translated_message
 
169
 
 
170
    @staticmethod
 
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'
 
177
            else:
 
178
                desired_locale = system_locale[0]
 
179
 
 
180
        locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
 
181
        lang = gettext.translation(domain,
 
182
                                   localedir=locale_dir,
 
183
                                   languages=[desired_locale],
 
184
                                   fallback=True)
151
185
        if six.PY3:
152
 
            ugettext = lang.gettext
153
 
        else:
154
 
            ugettext = lang.ugettext
155
 
 
156
 
        full_msg = (self._left_extra_msg +
157
 
                    ugettext(self._msg) +
158
 
                    self._right_extra_msg)
159
 
 
160
 
        if self.params is not None:
161
 
            full_msg = full_msg % self.params
162
 
 
163
 
        return six.text_type(full_msg)
164
 
 
165
 
    @property
166
 
    def locale(self):
167
 
        return self._locale
168
 
 
169
 
    @locale.setter
170
 
    def locale(self, value):
171
 
        self._locale = value
172
 
        if not self.params:
173
 
            return
174
 
 
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
181
 
            return
182
 
        if isinstance(self.params, tuple):
183
 
            for param in self.params:
184
 
                if isinstance(param, Message):
185
 
                    param.locale = value
186
 
            return
187
 
        if isinstance(self.params, dict):
188
 
            for param in self.params.values():
189
 
                if isinstance(param, Message):
190
 
                    param.locale = value
191
 
 
192
 
    def _save_dictionary_parameter(self, dict_param):
193
 
        full_msg = self.data
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)
198
 
 
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
 
187
        else:
 
188
            translator = lang.ugettext
 
189
 
 
190
        translated_message = translator(msgid)
 
191
        return translated_message
 
192
 
 
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,
 
200
                         msgtext=unicode_mod,
 
201
                         params=params,
 
202
                         domain=self.domain)
 
203
        return modded
 
204
 
 
205
    def _sanitize_mod_params(self, other):
 
206
        """Sanitize the object being modded with this Message.
 
207
 
 
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
 
213
        """
 
214
        if other is None:
 
215
            params = (other,)
 
216
        elif isinstance(other, dict):
 
217
            params = self._trim_dictionary_parameters(other)
 
218
        else:
 
219
            params = self._copy_param(other)
 
220
        return params
 
221
 
 
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.
 
229
 
 
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)
 
233
 
 
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)
203
238
        else:
204
239
            params = {}
 
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.
 
245
            src = {}
 
246
            if isinstance(self.params, dict):
 
247
                src.update(self.params)
 
248
            src.update(dict_param)
205
249
            for key in keys:
206
 
                try:
207
 
                    params[key] = copy.deepcopy(dict_param[key])
208
 
                except TypeError:
209
 
                    # cast uncopyable thing to unicode string
210
 
                    params[key] = six.text_type(dict_param[key])
 
250
                params[key] = self._copy_param(src[key])
211
251
 
212
252
        return params
213
253
 
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
218
 
        if other is None:
219
 
            self.params = (other, )
220
 
        elif isinstance(other, dict):
221
 
            self.params = self._save_dictionary_parameter(other)
222
 
        else:
223
 
            # fallback to casting to unicode,
224
 
            # this will handle the problematic python code-like
225
 
            # objects that cannot be deep-copied
226
 
            try:
227
 
                self.params = copy.deepcopy(other)
228
 
            except TypeError:
229
 
                self.params = six.text_type(other)
230
 
 
231
 
        return self
232
 
 
233
 
    # overrides to be more string-like
234
 
    def __unicode__(self):
235
 
        return self.data
236
 
 
237
 
    def __str__(self):
238
 
        if six.PY3:
239
 
            return self.__unicode__()
240
 
        return self.data.encode('utf-8')
241
 
 
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)
246
 
        for attr in to_copy:
247
 
            new_dict[attr] = copy.deepcopy(self.__dict__[attr])
248
 
 
249
 
        return new_dict
250
 
 
251
 
    def __setstate__(self, state):
252
 
        for (k, v) in state.items():
253
 
            setattr(self, k, v)
254
 
 
255
 
    # operator overloads
 
254
    def _copy_param(self, param):
 
255
        try:
 
256
            return copy.deepcopy(param)
 
257
        except TypeError:
 
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)
 
261
 
256
262
    def __add__(self, other):
257
 
        copied = copy.deepcopy(self)
258
 
        copied._right_extra_msg += other.__str__()
259
 
        return copied
 
263
        msg = _('Message objects do not support addition.')
 
264
        raise TypeError(msg)
260
265
 
261
266
    def __radd__(self, other):
262
 
        copied = copy.deepcopy(self)
263
 
        copied._left_extra_msg += other.__str__()
264
 
        return copied
265
 
 
266
 
    def __mod__(self, other):
267
 
        # do a format string to catch and raise
268
 
        # any possible KeyErrors from missing parameters
269
 
        self.data % other
270
 
        copied = copy.deepcopy(self)
271
 
        return copied._save_parameters(other)
272
 
 
273
 
    def __mul__(self, other):
274
 
        return self.data * other
275
 
 
276
 
    def __rmul__(self, other):
277
 
        return other * self.data
278
 
 
279
 
    def __getitem__(self, key):
280
 
        return self.data[key]
281
 
 
282
 
    def __getslice__(self, start, end):
283
 
        return self.data.__getslice__(start, end)
284
 
 
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']
295
 
        if name in ops:
296
 
            return getattr(self.data, name)
297
 
        else:
298
 
            return _userString.UserString.__getattribute__(self, name)
 
267
        return self.__add__(other)
 
268
 
 
269
    def __str__(self):
 
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)
299
276
 
300
277
 
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()
 
301
 
324
302
    for i in locale_identifiers:
325
303
        if find(i) is not None:
326
304
            language_list.append(i)
 
305
 
 
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',
 
316
               'zh_Hant': 'zh_TW',
 
317
               'fil': 'tl_PH'}
 
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)
 
321
 
327
322
    _AVAILABLE_LANGUAGES[domain] = language_list
328
323
    return copy.copy(language_list)
329
324
 
330
325
 
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.
 
328
 
 
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.
 
331
 
 
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
 
337
    """
 
338
    message = obj
 
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):
334
 
        if user_locale:
335
 
            message.locale = user_locale
336
 
        return six.text_type(message)
337
 
    else:
338
 
        return message
339
 
 
340
 
 
341
 
class LocaleHandler(logging.Handler):
342
 
    """Handler that can have a locale associated to translate Messages.
343
 
 
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.
347
 
    """
348
 
 
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)
 
347
    return obj
 
348
 
 
349
 
 
350
def _translate_args(args, desired_locale=None):
 
351
    """Translates all the translatable elements of the given arguments object.
 
352
 
 
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.
 
357
 
 
358
    If the locale is None the object is translated to the system locale.
 
359
 
 
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
 
364
    """
 
365
    if isinstance(args, tuple):
 
366
        return tuple(translate(v, desired_locale) for v in args)
 
367
    if isinstance(args, dict):
 
368
        translated_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)
 
374
 
 
375
 
 
376
class TranslationHandler(handlers.MemoryHandler):
 
377
    """Handler that translates records before logging them.
 
378
 
 
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.
 
382
 
 
383
    The handler can be configured declaratively in the logging.conf as follows:
 
384
 
 
385
        [handlers]
 
386
        keys = translatedlog, translator
 
387
 
 
388
        [handler_translatedlog]
 
389
        class = handlers.WatchedFileHandler
 
390
        args = ('/var/log/api-localized.log',)
 
391
        formatter = context
 
392
 
 
393
        [handler_translator]
 
394
        class = openstack.common.log.TranslationHandler
 
395
        target = translatedlog
 
396
        args = ('zh_CN',)
 
397
 
 
398
    If the specified locale is not available in the system, the handler will
 
399
    log in the default locale.
 
400
    """
 
401
 
 
402
    def __init__(self, locale=None, target=None):
 
403
        """Initialize a TranslationHandler
351
404
 
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
355
408
        """
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
358
 
        self.target = target
 
416
 
 
417
    def setFormatter(self, fmt):
 
418
        self.target.setFormatter(fmt)
359
419
 
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
 
425
 
 
426
        try:
 
427
            self._translate_and_log_record(record)
 
428
        finally:
 
429
            record.msg = original_msg
 
430
            record.args = original_args
 
431
 
 
432
    def _translate_and_log_record(self, record):
 
433
        record.msg = translate(record.msg, self.locale)
 
434
 
 
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)
364
439
 
365
440
        self.target.emit(record)