~linuxjedi/libra/python-libraclient

« back to all changes in this revision

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

  • Committer: Monty Taylor
  • Date: 2015-10-17 20:04:33 UTC
  • Revision ID: git-v1:1aff44b7b81b60c659b6ba7e14086e1788d55d9d
Retire stackforge/python-libraclient

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
 
 
3
 
# Copyright 2012 Red Hat, Inc.
4
 
# Copyright 2013 IBM Corp.
5
 
# All Rights Reserved.
6
 
#
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
10
 
#
11
 
#         http://www.apache.org/licenses/LICENSE-2.0
12
 
#
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
17
 
#    under the License.
18
 
 
19
 
"""
20
 
gettext for openstack-common modules.
21
 
 
22
 
Usual usage in an openstack.common module:
23
 
 
24
 
    from libraclient.openstack.common.gettextutils import _
25
 
"""
26
 
 
27
 
import copy
28
 
import gettext
29
 
import logging
30
 
import os
31
 
import re
32
 
try:
33
 
    import UserString as _userString
34
 
except ImportError:
35
 
    import collections as _userString
36
 
 
37
 
from babel import localedata
38
 
import six
39
 
 
40
 
_localedir = os.environ.get('libraclient'.upper() + '_LOCALEDIR')
41
 
_t = gettext.translation('libraclient', localedir=_localedir, fallback=True)
42
 
 
43
 
_AVAILABLE_LANGUAGES = {}
44
 
USE_LAZY = False
45
 
 
46
 
 
47
 
def enable_lazy():
48
 
    """Convenience function for configuring _() to use lazy gettext
49
 
 
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.
54
 
    """
55
 
    global USE_LAZY
56
 
    USE_LAZY = True
57
 
 
58
 
 
59
 
def _(msg):
60
 
    if USE_LAZY:
61
 
        return Message(msg, 'libraclient')
62
 
    else:
63
 
        if six.PY3:
64
 
            return _t.gettext(msg)
65
 
        return _t.ugettext(msg)
66
 
 
67
 
 
68
 
def install(domain, lazy=False):
69
 
    """Install a _() function using the given translation domain.
70
 
 
71
 
    Given a translation domain, install a _() function using gettext's
72
 
    install() function.
73
 
 
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.
77
 
    NOVA_LOCALEDIR).
78
 
 
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
84
 
                 any available locale.
85
 
    """
86
 
    if lazy:
87
 
        # NOTE(mrodden): Lazy gettext functionality.
88
 
        #
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.
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
 
        def _lazy_gettext(msg):
99
 
            """Create and return a Message object.
100
 
 
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.)
104
 
 
105
 
            Message encapsulates a string so that we can translate
106
 
            it later when needed.
107
 
            """
108
 
            return Message(msg, domain)
109
 
 
110
 
        from six import moves
111
 
        moves.builtins.__dict__['_'] = _lazy_gettext
112
 
    else:
113
 
        localedir = '%s_LOCALEDIR' % domain.upper()
114
 
        if six.PY3:
115
 
            gettext.install(domain,
116
 
                            localedir=os.environ.get(localedir))
117
 
        else:
118
 
            gettext.install(domain,
119
 
                            localedir=os.environ.get(localedir),
120
 
                            unicode=True)
121
 
 
122
 
 
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
 
 
151
 
        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)
203
 
        else:
204
 
            params = {}
205
 
            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])
211
 
 
212
 
        return params
213
 
 
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
256
 
    def __add__(self, other):
257
 
        copied = copy.deepcopy(self)
258
 
        copied._right_extra_msg += other.__str__()
259
 
        return copied
260
 
 
261
 
    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)
299
 
 
300
 
 
301
 
def get_available_languages(domain):
302
 
    """Lists the available languages for the given translation domain.
303
 
 
304
 
    :param domain: the domain to get languages for
305
 
    """
306
 
    if domain in _AVAILABLE_LANGUAGES:
307
 
        return copy.copy(_AVAILABLE_LANGUAGES[domain])
308
 
 
309
 
    localedir = '%s_LOCALEDIR' % domain.upper()
310
 
    find = lambda x: gettext.find(domain,
311
 
                                  localedir=os.environ.get(localedir),
312
 
                                  languages=[x])
313
 
 
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)
329
 
 
330
 
 
331
 
def get_localized_message(message, user_locale):
332
 
    """Gets a localized version of the given message in the given locale.
333
 
 
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.
336
 
 
337
 
    :returns: the translated message in unicode, or the original message if
338
 
              it could not be translated
339
 
    """
340
 
    translated = message
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
346
 
    return translated
347
 
 
348
 
 
349
 
class LocaleHandler(logging.Handler):
350
 
    """Handler that can have a locale associated to translate Messages.
351
 
 
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.
355
 
    """
356
 
 
357
 
    def __init__(self, locale, target):
358
 
        """Initialize a LocaleHandler
359
 
 
360
 
        :param locale: locale to use for translating messages
361
 
        :param target: logging.Handler object to forward
362
 
                       LogRecord objects to after translation
363
 
        """
364
 
        logging.Handler.__init__(self)
365
 
        self.locale = locale
366
 
        self.target = target
367
 
 
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
372
 
 
373
 
        self.target.emit(record)