diff options
-rw-r--r-- | oslo/i18n/_factory.py | 109 | ||||
-rw-r--r-- | oslo/i18n/_i18n.py | 25 | ||||
-rw-r--r-- | oslo/i18n/_lazy.py | 34 | ||||
-rw-r--r-- | oslo/i18n/_locale.py | 22 | ||||
-rw-r--r-- | oslo/i18n/_message.py | 170 | ||||
-rw-r--r-- | oslo/i18n/_translate.py | 71 | ||||
-rw-r--r-- | oslo/i18n/gettextutils.py | 395 | ||||
-rw-r--r-- | oslo/i18n/log.py | 89 | ||||
-rw-r--r-- | tests/test_factory.py | 14 | ||||
-rw-r--r-- | tests/test_gettextutils.py | 45 | ||||
-rw-r--r-- | tests/test_handler.py | 17 | ||||
-rw-r--r-- | tests/test_lazy.py | 41 | ||||
-rw-r--r-- | tests/test_locale_dir_variable.py | 4 | ||||
-rw-r--r-- | tests/test_message.py | 93 | ||||
-rw-r--r-- | tests/test_translate.py | 46 | ||||
-rw-r--r-- | tox.ini | 6 |
16 files changed, 692 insertions, 489 deletions
diff --git a/oslo/i18n/_factory.py b/oslo/i18n/_factory.py new file mode 100644 index 0000000..acd0367 --- /dev/null +++ b/oslo/i18n/_factory.py @@ -0,0 +1,109 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Translation function factory +""" + +import gettext +import os + +import six + +from oslo.i18n import _lazy +from oslo.i18n import _locale +from oslo.i18n import _message + + +class TranslatorFactory(object): + """Create translator functions + """ + + def __init__(self, domain, localedir=None): + """Establish a set of translation functions for the domain. + + :param domain: Name of translation domain, + specifying a message catalog. + :type domain: str + :param localedir: Directory with translation catalogs. + :type localedir: str + """ + self.domain = domain + if localedir is None: + localedir = os.environ.get(_locale.get_locale_dir_variable_name( + domain + )) + self.localedir = localedir + + def _make_translation_func(self, domain=None): + """Return a translation function ready for use with messages. + + The returned function takes a single value, the unicode string + to be translated. The return type varies depending on whether + lazy translation is being done. When lazy translation is + enabled, :class:`Message` objects are returned instead of + regular :class:`unicode` strings. + + The domain argument can be specified to override the default + from the factory, but the localedir from the factory is always + used because we assume the log-level translation catalogs are + installed in the same directory as the main application + catalog. + + """ + if domain is None: + domain = self.domain + t = gettext.translation( + domain, + localedir=self.localedir, + fallback=True, + ) + # Use the appropriate method of the translation object based + # on the python version. + m = t.gettext if six.PY3 else t.ugettext + + def f(msg): + """oslo.i18n.gettextutils translation function.""" + if _lazy.USE_LAZY: + return _message.Message(msg, domain=domain) + return m(msg) + return f + + @property + def primary(self): + "The default translation function." + return self._make_translation_func() + + def _make_log_translation_func(self, level): + return self._make_translation_func(self.domain + '-log-' + level) + + @property + def log_info(self): + "Translate info-level log messages." + return self._make_log_translation_func('info') + + @property + def log_warning(self): + "Translate warning-level log messages." + return self._make_log_translation_func('warning') + + @property + def log_error(self): + "Translate error-level log messages." + return self._make_log_translation_func('error') + + @property + def log_critical(self): + "Translate critical-level log messages." + return self._make_log_translation_func('critical') diff --git a/oslo/i18n/_i18n.py b/oslo/i18n/_i18n.py new file mode 100644 index 0000000..2dd2e4f --- /dev/null +++ b/oslo/i18n/_i18n.py @@ -0,0 +1,25 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Translation support for messages in this library. +""" + +from oslo.i18n import _factory + +# Create the global translation functions. +_translators = _factory.TranslatorFactory('oslo.i18n') + +# The primary translation function using the well-known name "_" +_ = _translators.primary diff --git a/oslo/i18n/_lazy.py b/oslo/i18n/_lazy.py new file mode 100644 index 0000000..f7e0398 --- /dev/null +++ b/oslo/i18n/_lazy.py @@ -0,0 +1,34 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +USE_LAZY = False + + +def enable_lazy(enable=True): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + + :param enable: Flag indicating whether lazy translation should be + turned on or off. Defaults to True. + :type enable: bool + + """ + global USE_LAZY + USE_LAZY = enable diff --git a/oslo/i18n/_locale.py b/oslo/i18n/_locale.py new file mode 100644 index 0000000..79d12fb --- /dev/null +++ b/oslo/i18n/_locale.py @@ -0,0 +1,22 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def get_locale_dir_variable_name(domain): + """Convert a translation domain name to a variable for specifying + a separate locale dir. + """ + return domain.upper().replace('.', '_').replace('-', '_') + '_LOCALEDIR' diff --git a/oslo/i18n/_message.py b/oslo/i18n/_message.py new file mode 100644 index 0000000..73cb7bf --- /dev/null +++ b/oslo/i18n/_message.py @@ -0,0 +1,170 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Private Message class for lazy translation support. +""" + +import copy +import gettext +import locale +import os + +import six + +from oslo.i18n import _translate + + +class Message(six.text_type): + """A Message object is a unicode object that can be translated. + + Translation of Message is done explicitly using the translate() method. + For all non-translation intents and purposes, a Message is simply unicode, + and can be treated as such. + """ + + def __new__(cls, msgid, msgtext=None, params=None, + domain='oslo', *args): + """Create a new Message object. + + In order for translation to work gettext requires a message ID, this + msgid will be used as the base unicode text. It is also possible + for the msgid and the base unicode text to be different by passing + the msgtext parameter. + """ + # If the base msgtext is not given, we use the default translation + # of the msgid (which is in English) just in case the system locale is + # not English, so that the base text will be in that locale by default. + if not msgtext: + msgtext = Message._translate_msgid(msgid, domain) + # We want to initialize the parent unicode with the actual object that + # would have been plain unicode if 'Message' was not enabled. + msg = super(Message, cls).__new__(cls, msgtext) + msg.msgid = msgid + msg.domain = domain + msg.params = params + return msg + + def translate(self, desired_locale=None): + """Translate this message to the desired locale. + + :param desired_locale: The desired locale to translate the message to, + if no locale is provided the message will be + translated to the system's default locale. + + :returns: the translated message in unicode + """ + + translated_message = Message._translate_msgid(self.msgid, + self.domain, + desired_locale) + if self.params is None: + # No need for more translation + return translated_message + + # This Message object may have been formatted with one or more + # Message objects as substitution arguments, given either as a single + # argument, part of a tuple, or as one or more values in a dictionary. + # When translating this Message we need to translate those Messages too + translated_params = _translate.translate_args(self.params, + desired_locale) + + translated_message = translated_message % translated_params + + return translated_message + + @staticmethod + def _translate_msgid(msgid, domain, desired_locale=None): + if not desired_locale: + system_locale = locale.getdefaultlocale() + # If the system locale is not available to the runtime use English + if not system_locale[0]: + desired_locale = 'en_US' + else: + desired_locale = system_locale[0] + + locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) + if six.PY3: + translator = lang.gettext + else: + translator = lang.ugettext + + translated_message = translator(msgid) + return translated_message + + def __mod__(self, other): + # When we mod a Message we want the actual operation to be performed + # by the parent class (i.e. unicode()), the only thing we do here is + # save the original msgid and the parameters in case of a translation + params = self._sanitize_mod_params(other) + unicode_mod = super(Message, self).__mod__(params) + modded = Message(self.msgid, + msgtext=unicode_mod, + params=params, + domain=self.domain) + return modded + + def _sanitize_mod_params(self, other): + """Sanitize the object being modded with this Message. + + - Add support for modding 'None' so translation supports it + - Trim the modded object, which can be a large dictionary, to only + those keys that would actually be used in a translation + - Snapshot the object being modded, in case the message is + translated, it will be used as it was when the Message was created + """ + if other is None: + params = (other,) + elif isinstance(other, dict): + # Merge the dictionaries + # Copy each item in case one does not support deep copy. + params = {} + if isinstance(self.params, dict): + for key, val in self.params.items(): + params[key] = self._copy_param(val) + for key, val in other.items(): + params[key] = self._copy_param(val) + else: + params = self._copy_param(other) + return params + + def _copy_param(self, param): + try: + return copy.deepcopy(param) + except Exception: + # Fallback to casting to unicode this will handle the + # python code-like objects that can't be deep-copied + return six.text_type(param) + + def __add__(self, other): + from oslo.i18n._i18n import _ + msg = _('Message objects do not support addition.') + raise TypeError(msg) + + def __radd__(self, other): + return self.__add__(other) + + if six.PY2: + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + from oslo.i18n._i18n import _ + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) diff --git a/oslo/i18n/_translate.py b/oslo/i18n/_translate.py new file mode 100644 index 0000000..bd81b85 --- /dev/null +++ b/oslo/i18n/_translate.py @@ -0,0 +1,71 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + + +def translate(obj, desired_locale=None): + """Gets the translated unicode representation of the given object. + + If the object is not translatable it is returned as-is. + + If the desired_locale argument is None the object is translated to + the system locale. + + :param obj: the object to translate + :param desired_locale: the locale to translate the message to, if None the + default system locale will be used + :returns: the translated object in unicode, or the original object if + it could not be translated + + """ + from oslo.i18n import _message # avoid circular dependency at module level + message = obj + if not isinstance(message, _message.Message): + # If the object to translate is not already translatable, + # let's first get its unicode representation + message = six.text_type(obj) + if isinstance(message, _message.Message): + # Even after unicoding() we still need to check if we are + # running with translatable unicode before translating + return message.translate(desired_locale) + return obj + + +def translate_args(args, desired_locale=None): + """Translates all the translatable elements of the given arguments object. + + This method is used for translating the translatable values in method + arguments which include values of tuples or dictionaries. + If the object is not a tuple or a dictionary the object itself is + translated if it is translatable. + + If the locale is None the object is translated to the system locale. + + :param args: the args to translate + :param desired_locale: the locale to translate the args to, if None the + default system locale will be used + :returns: a new args object with the translated contents of the original + """ + if isinstance(args, tuple): + return tuple(translate(v, desired_locale) for v in args) + if isinstance(args, dict): + translated_dict = {} + for (k, v) in six.iteritems(args): + translated_v = translate(v, desired_locale) + translated_dict[k] = translated_v + return translated_dict + return translate(args, desired_locale) diff --git a/oslo/i18n/gettextutils.py b/oslo/i18n/gettextutils.py index a8134fc..a7285f5 100644 --- a/oslo/i18n/gettextutils.py +++ b/oslo/i18n/gettextutils.py @@ -19,145 +19,15 @@ import copy import gettext -import locale -from logging import handlers import os from babel import localedata import six -_AVAILABLE_LANGUAGES = {} - -_USE_LAZY = False - - -def _get_locale_dir_variable_name(domain): - """Convert a translation domain name to a variable for specifying - a separate locale dir. - """ - return domain.upper().replace('.', '_').replace('-', '_') + '_LOCALEDIR' - - -class TranslatorFactory(object): - """Create translator functions - """ - - def __init__(self, domain, localedir=None): - """Establish a set of translation functions for the domain. - - :param domain: Name of translation domain, - specifying a message catalog. - :type domain: str - :param localedir: Directory with translation catalogs. - :type localedir: str - """ - self.domain = domain - if localedir is None: - localedir = os.environ.get(_get_locale_dir_variable_name(domain)) - self.localedir = localedir - - def _make_translation_func(self, domain=None): - """Return a translation function ready for use with messages. - - The returned function takes a single value, the unicode string - to be translated. The return type varies depending on whether - lazy translation is being done. When lazy translation is - enabled, :class:`Message` objects are returned instead of - regular :class:`unicode` strings. - - The domain argument can be specified to override the default - from the factory, but the localedir from the factory is always - used because we assume the log-level translation catalogs are - installed in the same directory as the main application - catalog. - - """ - if domain is None: - domain = self.domain - t = gettext.translation( - domain, - localedir=self.localedir, - fallback=True, - ) - # Use the appropriate method of the translation object based - # on the python version. - m = t.gettext if six.PY3 else t.ugettext - - def f(msg): - """oslo.i18n.gettextutils translation function.""" - if _USE_LAZY: - return Message(msg, domain=domain) - return m(msg) - return f - - @property - def primary(self): - "The default translation function." - return self._make_translation_func() - - def _make_log_translation_func(self, level): - return self._make_translation_func(self.domain + '-log-' + level) - - @property - def log_info(self): - "Translate info-level log messages." - return self._make_log_translation_func('info') - - @property - def log_warning(self): - "Translate warning-level log messages." - return self._make_log_translation_func('warning') - - @property - def log_error(self): - "Translate error-level log messages." - return self._make_log_translation_func('error') - - @property - def log_critical(self): - "Translate critical-level log messages." - return self._make_log_translation_func('critical') - - -# NOTE(dhellmann): When this module moves out of the incubator into -# oslo.i18n, these global variables can be moved to an integration -# module within each application. - -# Create the global translation functions. -_translators = TranslatorFactory('oslo') - -# The primary translation function using the well-known name "_" -_ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical - -# NOTE(dhellmann): End of globals that will move to the application's -# integration module. - - -def enable_lazy(enable=True): - """Convenience function for configuring _() to use lazy gettext - - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. - - :param enable: Flag indicating whether lazy translation should be - turned on or off. Defaults to True. - :type enable: bool - - """ - global _USE_LAZY - _USE_LAZY = enable +# Expose a few internal pieces as part of our public API. +from oslo.i18n._factory import TranslatorFactory # noqa +from oslo.i18n._lazy import enable_lazy # noqa +from oslo.i18n._translate import translate # noqa def install(domain): @@ -183,145 +53,7 @@ def install(domain): moves.builtins.__dict__['_'] = tf.primary -class Message(six.text_type): - """A Message object is a unicode object that can be translated. - - Translation of Message is done explicitly using the translate() method. - For all non-translation intents and purposes, a Message is simply unicode, - and can be treated as such. - """ - - def __new__(cls, msgid, msgtext=None, params=None, - domain='oslo', *args): - """Create a new Message object. - - In order for translation to work gettext requires a message ID, this - msgid will be used as the base unicode text. It is also possible - for the msgid and the base unicode text to be different by passing - the msgtext parameter. - """ - # If the base msgtext is not given, we use the default translation - # of the msgid (which is in English) just in case the system locale is - # not English, so that the base text will be in that locale by default. - if not msgtext: - msgtext = Message._translate_msgid(msgid, domain) - # We want to initialize the parent unicode with the actual object that - # would have been plain unicode if 'Message' was not enabled. - msg = super(Message, cls).__new__(cls, msgtext) - msg.msgid = msgid - msg.domain = domain - msg.params = params - return msg - - def translate(self, desired_locale=None): - """Translate this message to the desired locale. - - :param desired_locale: The desired locale to translate the message to, - if no locale is provided the message will be - translated to the system's default locale. - - :returns: the translated message in unicode - """ - - translated_message = Message._translate_msgid(self.msgid, - self.domain, - desired_locale) - if self.params is None: - # No need for more translation - return translated_message - - # This Message object may have been formatted with one or more - # Message objects as substitution arguments, given either as a single - # argument, part of a tuple, or as one or more values in a dictionary. - # When translating this Message we need to translate those Messages too - translated_params = _translate_args(self.params, desired_locale) - - translated_message = translated_message % translated_params - - return translated_message - - @staticmethod - def _translate_msgid(msgid, domain, desired_locale=None): - if not desired_locale: - system_locale = locale.getdefaultlocale() - # If the system locale is not available to the runtime use English - if not system_locale[0]: - desired_locale = 'en_US' - else: - desired_locale = system_locale[0] - - locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') - lang = gettext.translation(domain, - localedir=locale_dir, - languages=[desired_locale], - fallback=True) - if six.PY3: - translator = lang.gettext - else: - translator = lang.ugettext - - translated_message = translator(msgid) - return translated_message - - def __mod__(self, other): - # When we mod a Message we want the actual operation to be performed - # by the parent class (i.e. unicode()), the only thing we do here is - # save the original msgid and the parameters in case of a translation - params = self._sanitize_mod_params(other) - unicode_mod = super(Message, self).__mod__(params) - modded = Message(self.msgid, - msgtext=unicode_mod, - params=params, - domain=self.domain) - return modded - - def _sanitize_mod_params(self, other): - """Sanitize the object being modded with this Message. - - - Add support for modding 'None' so translation supports it - - Trim the modded object, which can be a large dictionary, to only - those keys that would actually be used in a translation - - Snapshot the object being modded, in case the message is - translated, it will be used as it was when the Message was created - """ - if other is None: - params = (other,) - elif isinstance(other, dict): - # Merge the dictionaries - # Copy each item in case one does not support deep copy. - params = {} - if isinstance(self.params, dict): - for key, val in self.params.items(): - params[key] = self._copy_param(val) - for key, val in other.items(): - params[key] = self._copy_param(val) - else: - params = self._copy_param(other) - return params - - def _copy_param(self, param): - try: - return copy.deepcopy(param) - except Exception: - # Fallback to casting to unicode this will handle the - # python code-like objects that can't be deep-copied - return six.text_type(param) - - def __add__(self, other): - msg = _('Message objects do not support addition.') - raise TypeError(msg) - - def __radd__(self, other): - return self.__add__(other) - - if six.PY2: - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) +_AVAILABLE_LANGUAGES = {} def get_available_languages(domain): @@ -370,120 +102,3 @@ def get_available_languages(domain): _AVAILABLE_LANGUAGES[domain] = language_list return copy.copy(language_list) - - -def translate(obj, desired_locale=None): - """Gets the translated unicode representation of the given object. - - If the object is not translatable it is returned as-is. - If the locale is None the object is translated to the system locale. - - :param obj: the object to translate - :param desired_locale: the locale to translate the message to, if None the - default system locale will be used - :returns: the translated object in unicode, or the original object if - it could not be translated - """ - message = obj - if not isinstance(message, Message): - # If the object to translate is not already translatable, - # let's first get its unicode representation - message = six.text_type(obj) - if isinstance(message, Message): - # Even after unicoding() we still need to check if we are - # running with translatable unicode before translating - return message.translate(desired_locale) - return obj - - -def _translate_args(args, desired_locale=None): - """Translates all the translatable elements of the given arguments object. - - This method is used for translating the translatable values in method - arguments which include values of tuples or dictionaries. - If the object is not a tuple or a dictionary the object itself is - translated if it is translatable. - - If the locale is None the object is translated to the system locale. - - :param args: the args to translate - :param desired_locale: the locale to translate the args to, if None the - default system locale will be used - :returns: a new args object with the translated contents of the original - """ - if isinstance(args, tuple): - return tuple(translate(v, desired_locale) for v in args) - if isinstance(args, dict): - translated_dict = {} - for (k, v) in six.iteritems(args): - translated_v = translate(v, desired_locale) - translated_dict[k] = translated_v - return translated_dict - return translate(args, desired_locale) - - -class TranslationHandler(handlers.MemoryHandler): - """Handler that translates records before logging them. - - The TranslationHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating them. This handler - depends on Message objects being logged, instead of regular strings. - - The handler can be configured declaratively in the logging.conf as follows: - - [handlers] - keys = translatedlog, translator - - [handler_translatedlog] - class = handlers.WatchedFileHandler - args = ('/var/log/api-localized.log',) - formatter = context - - [handler_translator] - class = openstack.common.log.TranslationHandler - target = translatedlog - args = ('zh_CN',) - - If the specified locale is not available in the system, the handler will - log in the default locale. - """ - - def __init__(self, locale=None, target=None): - """Initialize a TranslationHandler - - :param locale: locale to use for translating messages - :param target: logging.Handler object to forward - LogRecord objects to after translation - """ - # NOTE(luisg): In order to allow this handler to be a wrapper for - # other handlers, such as a FileHandler, and still be able to - # configure it using logging.conf, this handler has to extend - # MemoryHandler because only the MemoryHandlers' logging.conf - # parsing is implemented such that it accepts a target handler. - handlers.MemoryHandler.__init__(self, capacity=0, target=target) - self.locale = locale - - def setFormatter(self, fmt): - self.target.setFormatter(fmt) - - def emit(self, record): - # We save the message from the original record to restore it - # after translation, so other handlers are not affected by this - original_msg = record.msg - original_args = record.args - - try: - self._translate_and_log_record(record) - finally: - record.msg = original_msg - record.args = original_args - - def _translate_and_log_record(self, record): - record.msg = translate(record.msg, self.locale) - - # In addition to translating the message, we also need to translate - # arguments that were passed to the log method that were not part - # of the main message e.g., log.info(_('Some message %s'), this_one)) - record.args = _translate_args(record.args, self.locale) - - self.target.emit(record) diff --git a/oslo/i18n/log.py b/oslo/i18n/log.py new file mode 100644 index 0000000..46d6c1e --- /dev/null +++ b/oslo/i18n/log.py @@ -0,0 +1,89 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""logging utilities for translation +""" + +from logging import handlers + +from oslo.i18n import _translate + + +class TranslationHandler(handlers.MemoryHandler): + """Handler that translates records before logging them. + + The TranslationHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating them. This handler + depends on Message objects being logged, instead of regular strings. + + The handler can be configured declaratively in the logging.conf as follows: + + [handlers] + keys = translatedlog, translator + + [handler_translatedlog] + class = handlers.WatchedFileHandler + args = ('/var/log/api-localized.log',) + formatter = context + + [handler_translator] + class = openstack.common.log.TranslationHandler + target = translatedlog + args = ('zh_CN',) + + If the specified locale is not available in the system, the handler will + log in the default locale. + """ + + def __init__(self, locale=None, target=None): + """Initialize a TranslationHandler + + :param locale: locale to use for translating messages + :param target: logging.Handler object to forward + LogRecord objects to after translation + """ + # NOTE(luisg): In order to allow this handler to be a wrapper for + # other handlers, such as a FileHandler, and still be able to + # configure it using logging.conf, this handler has to extend + # MemoryHandler because only the MemoryHandlers' logging.conf + # parsing is implemented such that it accepts a target handler. + handlers.MemoryHandler.__init__(self, capacity=0, target=target) + self.locale = locale + + def setFormatter(self, fmt): + self.target.setFormatter(fmt) + + def emit(self, record): + # We save the message from the original record to restore it + # after translation, so other handlers are not affected by this + original_msg = record.msg + original_args = record.args + + try: + self._translate_and_log_record(record) + finally: + record.msg = original_msg + record.args = original_args + + def _translate_and_log_record(self, record): + record.msg = _translate.translate(record.msg, self.locale) + + # In addition to translating the message, we also need to translate + # arguments that were passed to the log method that were not part + # of the main message e.g., log.info(_('Some message %s'), this_one)) + record.args = _translate.translate_args(record.args, self.locale) + + self.target.emit(record) diff --git a/tests/test_factory.py b/tests/test_factory.py index 58af03d..112960f 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -19,6 +19,8 @@ import six from oslotest import base as test_base +from oslo.i18n import _lazy +from oslo.i18n import _message from oslo.i18n import gettextutils @@ -27,23 +29,23 @@ class TranslatorFactoryTest(test_base.BaseTestCase): def setUp(self): super(TranslatorFactoryTest, self).setUp() # remember so we can reset to it later in case it changes - self._USE_LAZY = gettextutils._USE_LAZY + self._USE_LAZY = _lazy.USE_LAZY def tearDown(self): # reset to value before test - gettextutils._USE_LAZY = self._USE_LAZY + _lazy.USE_LAZY = self._USE_LAZY super(TranslatorFactoryTest, self).tearDown() def test_lazy(self): gettextutils.enable_lazy(True) - with mock.patch.object(gettextutils, 'Message') as msg: + with mock.patch.object(_message, 'Message') as msg: tf = gettextutils.TranslatorFactory('domain') tf.primary('some text') msg.assert_called_with('some text', domain='domain') def test_not_lazy(self): gettextutils.enable_lazy(False) - with mock.patch.object(gettextutils, 'Message') as msg: + with mock.patch.object(_message, 'Message') as msg: msg.side_effect = AssertionError('should not use Message') tf = gettextutils.TranslatorFactory('domain') tf.primary('some text') @@ -52,11 +54,11 @@ class TranslatorFactoryTest(test_base.BaseTestCase): gettextutils.enable_lazy(True) tf = gettextutils.TranslatorFactory('domain') r = tf.primary('some text') - self.assertIsInstance(r, gettextutils.Message) + self.assertIsInstance(r, _message.Message) gettextutils.enable_lazy(False) r = tf.primary('some text') # Python 2.6 doesn't have assertNotIsInstance(). - self.assertFalse(isinstance(r, gettextutils.Message)) + self.assertFalse(isinstance(r, _message.Message)) def test_py2(self): gettextutils.enable_lazy(False) diff --git a/tests/test_gettextutils.py b/tests/test_gettextutils.py index 543e897..8797e1f 100644 --- a/tests/test_gettextutils.py +++ b/tests/test_gettextutils.py @@ -24,9 +24,8 @@ import six from oslotest import base as test_base from oslotest import moxstubout -from tests import fakes -from tests import utils - +from oslo.i18n import _lazy +from oslo.i18n import _message from oslo.i18n import gettextutils @@ -41,35 +40,27 @@ class GettextTest(test_base.BaseTestCase): self.stubs = moxfixture.stubs self.mox = moxfixture.mox # remember so we can reset to it later in case it changes - self._USE_LAZY = gettextutils._USE_LAZY + self._USE_LAZY = _lazy.USE_LAZY + self.t = gettextutils.TranslatorFactory('oslo.i18n.test') def tearDown(self): # reset to value before test - gettextutils._USE_LAZY = self._USE_LAZY + _lazy.USE_LAZY = self._USE_LAZY super(GettextTest, self).tearDown() - def test_enable_lazy(self): - gettextutils._USE_LAZY = False - gettextutils.enable_lazy() - self.assertTrue(gettextutils._USE_LAZY) - - def test_disable_lazy(self): - gettextutils._USE_LAZY = True - gettextutils.enable_lazy(False) - self.assertFalse(gettextutils._USE_LAZY) - def test_gettext_does_not_blow_up(self): - LOG.info(gettextutils._('test')) + LOG.info(self.t.primary('test')) def test_gettextutils_install(self): gettextutils.install('blaa') gettextutils.enable_lazy(False) - self.assertTrue(isinstance(_('A String'), six.text_type)) # noqa + self.assertTrue(isinstance(self.t.primary('A String'), + six.text_type)) gettextutils.install('blaa') gettextutils.enable_lazy(True) - self.assertTrue(isinstance(_('A Message'), # noqa - gettextutils.Message)) + self.assertTrue(isinstance(self.t.primary('A Message'), + _message.Message)) def test_gettext_install_looks_up_localedir(self): with mock.patch('os.environ.get') as environ_get: @@ -133,19 +124,3 @@ class GettextTest(test_base.BaseTestCase): unknown_domain_languages = gettextutils.get_available_languages('huh') self.assertEqual(1, len(unknown_domain_languages)) self.assertIn('en_US', unknown_domain_languages) - - @mock.patch('gettext.translation') - def test_translate(self, mock_translation): - en_message = 'A message in the default locale' - es_translation = 'A message in Spanish' - message = gettextutils.Message(en_message) - - es_translations = {en_message: es_translation} - translations_map = {'es': es_translations} - translator = fakes.FakeTranslations.translator(translations_map) - mock_translation.side_effect = translator - - # translate() works on msgs and on objects whose unicode reps are msgs - obj = utils.SomeObject(message) - self.assertEqual(es_translation, gettextutils.translate(message, 'es')) - self.assertEqual(es_translation, gettextutils.translate(obj, 'es')) diff --git a/tests/test_handler.py b/tests/test_handler.py index 6b634da..9814f38 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -23,7 +23,8 @@ from oslotest import base as test_base from tests import fakes -from oslo.i18n import gettextutils +from oslo.i18n import _message +from oslo.i18n import log as i18n_log LOG = logging.getLogger(__name__) @@ -35,7 +36,7 @@ class TranslationHandlerTestCase(test_base.BaseTestCase): self.stream = six.StringIO() self.destination_handler = logging.StreamHandler(self.stream) - self.translation_handler = gettextutils.TranslationHandler('zh_CN') + self.translation_handler = i18n_log.TranslationHandler('zh_CN') self.translation_handler.setTarget(self.destination_handler) self.logger = logging.getLogger('localehander_logger') @@ -56,7 +57,7 @@ class TranslationHandlerTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator(translations_map) mock_translation.side_effect = translator - msg = gettextutils.Message(log_message) + msg = _message.Message(log_message) self.logger.info(msg) self.assertIn(log_message_translation, self.stream.getvalue()) @@ -74,8 +75,8 @@ class TranslationHandlerTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator(translations_map) mock_translation.side_effect = translator - msg = gettextutils.Message(log_message) - arg = gettextutils.Message(log_arg) + msg = _message.Message(log_message) + arg = _message.Message(log_arg) self.logger.info(msg, arg) self.assertIn(log_message_translation % log_arg_translation, @@ -97,9 +98,9 @@ class TranslationHandlerTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator(translations_map) mock_translation.side_effect = translator - msg = gettextutils.Message(log_message) - arg_1 = gettextutils.Message(log_arg_1) - arg_2 = gettextutils.Message(log_arg_2) + msg = _message.Message(log_message) + arg_1 = _message.Message(log_arg_1) + arg_2 = _message.Message(log_arg_2) self.logger.info(msg, {'arg1': arg_1, 'arg2': arg_2}) translation = log_message_translation % {'arg1': log_arg_1_translation, diff --git a/tests/test_lazy.py b/tests/test_lazy.py new file mode 100644 index 0000000..1183b8f --- /dev/null +++ b/tests/test_lazy.py @@ -0,0 +1,41 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base as test_base + +from oslo.i18n import _lazy +from oslo.i18n import gettextutils + + +class LazyTest(test_base.BaseTestCase): + + def setUp(self): + super(LazyTest, self).setUp() + self._USE_LAZY = _lazy.USE_LAZY + + def tearDown(self): + _lazy.USE_LAZY = self._USE_LAZY + super(LazyTest, self).tearDown() + + def test_enable_lazy(self): + _lazy.USE_LAZY = False + gettextutils.enable_lazy() + self.assertTrue(_lazy.USE_LAZY) + + def test_disable_lazy(self): + _lazy.USE_LAZY = True + gettextutils.enable_lazy(False) + self.assertFalse(_lazy.USE_LAZY) diff --git a/tests/test_locale_dir_variable.py b/tests/test_locale_dir_variable.py index 59daab9..ca87482 100644 --- a/tests/test_locale_dir_variable.py +++ b/tests/test_locale_dir_variable.py @@ -15,7 +15,7 @@ from oslotest import base as test_base import testscenarios.testcase -from oslo.i18n import gettextutils +from oslo.i18n import _locale class LocaleDirVariableTest(testscenarios.testcase.WithScenarios, @@ -28,5 +28,5 @@ class LocaleDirVariableTest(testscenarios.testcase.WithScenarios, ] def test_make_variable_name(self): - var = gettextutils._get_locale_dir_variable_name(self.domain) + var = _locale.get_locale_dir_variable_name(self.domain) self.assertEqual(self.expected, var) diff --git a/tests/test_message.py b/tests/test_message.py index f324472..0e96ae1 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +from __future__ import unicode_literals + import logging import mock @@ -25,7 +27,8 @@ from oslotest import base as test_base from tests import fakes from tests import utils -from oslo.i18n import gettextutils +from oslo.i18n import _message + LOG = logging.getLogger(__name__) @@ -33,20 +36,16 @@ LOG = logging.getLogger(__name__) class MessageTestCase(test_base.BaseTestCase): """Unit tests for locale Message class.""" - @staticmethod - def message(msg): - return gettextutils.Message(msg) - def test_message_id_and_message_text(self): - message = gettextutils.Message('1') + message = _message.Message('1') self.assertEqual('1', message.msgid) self.assertEqual('1', message) - message = gettextutils.Message('1', msgtext='A') + message = _message.Message('1', msgtext='A') self.assertEqual('1', message.msgid) self.assertEqual('A', message) def test_message_is_unicode(self): - message = self.message('some %s') % 'message' + message = _message.Message('some %s') % 'message' self.assertIsInstance(message, six.text_type) @mock.patch('locale.getdefaultlocale') @@ -63,7 +62,7 @@ class MessageTestCase(test_base.BaseTestCase): mock_translation.side_effect = translator mock_getdefaultlocale.return_value = ('es',) - message = gettextutils.Message(msgid) + message = _message.Message(msgid) # The base representation of the message is in Spanish, as well as # the default translation, since the default locale was Spanish. @@ -71,7 +70,7 @@ class MessageTestCase(test_base.BaseTestCase): self.assertEqual(es_translation, message.translate()) def test_translate_returns_unicode(self): - message = self.message('some %s') % 'message' + message = _message.Message('some %s') % 'message' self.assertIsInstance(message.translate(), six.text_type) def test_mod_with_named_parameters(self): @@ -85,7 +84,7 @@ class MessageTestCase(test_base.BaseTestCase): 'stderr': 'test5', 'something': 'trimmed'} - result = self.message(msgid) % params + result = _message.Message(msgid) % params expected = msgid % params self.assertEqual(result, expected) @@ -102,7 +101,7 @@ class MessageTestCase(test_base.BaseTestCase): 'stderr': 'test5'} # Run string interpolation the first time to make a new Message - first = self.message(msgid) % params + first = _message.Message(msgid) % params # Run string interpolation on the new Message, to replicate # one of the error paths with some Exception classes we've @@ -145,7 +144,7 @@ class MessageTestCase(test_base.BaseTestCase): 'url': 'test2', 'headers': {'h1': 'val1'}} - result = self.message(msgid) % params + result = _message.Message(msgid) % params expected = msgid % params self.assertEqual(result, expected) @@ -155,11 +154,11 @@ class MessageTestCase(test_base.BaseTestCase): msgid = "Test that we can inject a dictionary %s" params = {'description': 'test1'} - result = self.message(msgid) % params + result = _message.Message(msgid) % params expected = msgid % params - self.assertEqual(result, expected) - self.assertEqual(result.translate(), expected) + self.assertEqual(expected, result) + self.assertEqual(expected, result.translate()) def test_mod_with_integer_parameters(self): msgid = "Some string with params: %d" @@ -169,10 +168,10 @@ class MessageTestCase(test_base.BaseTestCase): results = [] for param in params: messages.append(msgid % param) - results.append(self.message(msgid) % param) + results.append(_message.Message(msgid) % param) for message, result in zip(messages, results): - self.assertEqual(type(result), gettextutils.Message) + self.assertEqual(type(result), _message.Message) self.assertEqual(result.translate(), message) # simulate writing out as string @@ -184,7 +183,7 @@ class MessageTestCase(test_base.BaseTestCase): msgid = "Found object: %(current_value)s" changing_dict = {'current_value': 1} # A message created with some params - result = self.message(msgid) % changing_dict + result = _message.Message(msgid) % changing_dict # The parameters may change changing_dict['current_value'] = 2 # Even if the param changes when the message is @@ -196,7 +195,7 @@ class MessageTestCase(test_base.BaseTestCase): changing_list = list([1, 2, 3]) params = {'current_list': changing_list} # Apply the params - result = self.message(msgid) % params + result = _message.Message(msgid) % params # Change the list changing_list.append(4) # Even though the list changed the message @@ -207,24 +206,24 @@ class MessageTestCase(test_base.BaseTestCase): msgid = "Value: %s" params = utils.NoDeepCopyObject(5) # Apply the params - result = self.message(msgid) % params + result = _message.Message(msgid) % params self.assertEqual(result.translate(), "Value: 5") def test_mod_deep_copies_param_nodeep_dict(self): msgid = "Values: %(val1)s %(val2)s" params = {'val1': 1, 'val2': utils.NoDeepCopyObject(2)} # Apply the params - result = self.message(msgid) % params + result = _message.Message(msgid) % params self.assertEqual(result.translate(), "Values: 1 2") # Apply again to make sure other path works as well params = {'val1': 3, 'val2': utils.NoDeepCopyObject(4)} - result = self.message(msgid) % params + result = _message.Message(msgid) % params self.assertEqual(result.translate(), "Values: 3 4") def test_mod_returns_a_copy(self): msgid = "Some msgid string: %(test1)s %(test2)s" - message = self.message(msgid) + message = _message.Message(msgid) m1 = message % {'test1': 'foo', 'test2': 'bar'} m2 = message % {'test1': 'foo2', 'test2': 'bar2'} @@ -237,13 +236,13 @@ class MessageTestCase(test_base.BaseTestCase): def test_mod_with_none_parameter(self): msgid = "Some string with params: %s" - message = self.message(msgid) % None + message = _message.Message(msgid) % None self.assertEqual(msgid % None, message) self.assertEqual(msgid % None, message.translate()) def test_mod_with_missing_parameters(self): msgid = "Some string with params: %s %s" - test_me = lambda: self.message(msgid) % 'just one' + test_me = lambda: _message.Message(msgid) % 'just one' # Just like with strings missing parameters raise TypeError self.assertRaises(TypeError, test_me) @@ -253,7 +252,7 @@ class MessageTestCase(test_base.BaseTestCase): 'param2': 'test2', 'param3': 'notinstring'} - result = self.message(msgid) % params + result = _message.Message(msgid) % params expected = msgid % params self.assertEqual(result, expected) @@ -268,31 +267,31 @@ class MessageTestCase(test_base.BaseTestCase): params = {'param1': 'test', 'param2': 'test2'} - test_me = lambda: self.message(msgid) % params + test_me = lambda: _message.Message(msgid) % params # Just like with strings missing named parameters raise KeyError self.assertRaises(KeyError, test_me) def test_add_disabled(self): msgid = "A message" - test_me = lambda: self.message(msgid) + ' some string' + test_me = lambda: _message.Message(msgid) + ' some string' self.assertRaises(TypeError, test_me) def test_radd_disabled(self): msgid = "A message" - test_me = lambda: utils.SomeObject('test') + self.message(msgid) + test_me = lambda: utils.SomeObject('test') + _message.Message(msgid) self.assertRaises(TypeError, test_me) @testtools.skipIf(six.PY3, 'test specific to Python 2') def test_str_disabled(self): msgid = "A message" - test_me = lambda: str(self.message(msgid)) + test_me = lambda: str(_message.Message(msgid)) self.assertRaises(UnicodeError, test_me) @mock.patch('gettext.translation') def test_translate(self, mock_translation): en_message = 'A message in the default locale' es_translation = 'A message in Spanish' - message = gettextutils.Message(en_message) + message = _message.Message(en_message) es_translations = {en_message: es_translation} translations_map = {'es': es_translations} @@ -305,7 +304,7 @@ class MessageTestCase(test_base.BaseTestCase): def test_translate_message_from_unicoded_object(self, mock_translation): en_message = 'A message in the default locale' es_translation = 'A message in Spanish' - message = gettextutils.Message(en_message) + message = _message.Message(en_message) es_translations = {en_message: es_translation} translations_map = {'es': es_translations} translator = fakes.FakeTranslations.translator(translations_map) @@ -323,7 +322,7 @@ class MessageTestCase(test_base.BaseTestCase): en_message = 'A message in the default locale' es_translation = 'A message in Spanish' zh_translation = 'A message in Chinese' - message = gettextutils.Message(en_message) + message = _message.Message(en_message) es_translations = {en_message: es_translation} zh_translations = {en_message: zh_translation} @@ -348,7 +347,7 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) + msg = _message.Message(message_with_params) msg = msg % param default_translation = message_with_params % param @@ -368,8 +367,8 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) - param_msg = gettextutils.Message(param) + msg = _message.Message(message_with_params) + param_msg = _message.Message(param) # Here we are testing translation of a Message with another object # that can be translated via its unicode() representation, this is @@ -394,7 +393,7 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) + msg = _message.Message(message_with_params) msg = msg % param default_translation = message_with_params % param @@ -418,8 +417,8 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) - msg_param = gettextutils.Message(message_param) + msg = _message.Message(message_with_params) + msg_param = _message.Message(message_param) msg = msg % msg_param default_translation = message_with_params % message_param @@ -442,9 +441,9 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) - param_1 = gettextutils.Message(message_param) - param_2 = gettextutils.Message(another_message_param) + msg = _message.Message(message_with_params) + param_1 = _message.Message(message_param) + param_2 = _message.Message(another_message_param) msg = msg % (param_1, param_2) default_translation = message_with_params % (message_param, @@ -466,8 +465,8 @@ class MessageTestCase(test_base.BaseTestCase): translator = fakes.FakeTranslations.translator({'es': translations}) mock_translation.side_effect = translator - msg = gettextutils.Message(message_with_params) - msg_param = gettextutils.Message(message_param) + msg = _message.Message(message_with_params) + msg_param = _message.Message(message_param) msg = msg % {'param': msg_param} default_translation = message_with_params % {'param': message_param} @@ -503,8 +502,8 @@ class MessageTestCase(test_base.BaseTestCase): mock_translation.side_effect = translator mock_getdefaultlocale.return_value = ('es',) - msg = gettextutils.Message(message_with_params) - msg_param = gettextutils.Message(message_param) + msg = _message.Message(message_with_params) + msg_param = _message.Message(message_param) msg = msg % {'param': msg_param} es_translation = es_translation % {'param': es_param_translation} diff --git a/tests/test_translate.py b/tests/test_translate.py new file mode 100644 index 0000000..2fad7c3 --- /dev/null +++ b/tests/test_translate.py @@ -0,0 +1,46 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals + +import mock + +from oslotest import base as test_base + +from tests import fakes +from tests import utils + +from oslo.i18n import _message +from oslo.i18n import _translate + + +class TranslateTest(test_base.BaseTestCase): + + @mock.patch('gettext.translation') + def test_translate(self, mock_translation): + en_message = 'A message in the default locale' + es_translation = 'A message in Spanish' + message = _message.Message(en_message) + + es_translations = {en_message: es_translation} + translations_map = {'es': es_translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + + # translate() works on msgs and on objects whose unicode reps are msgs + obj = utils.SomeObject(message) + self.assertEqual(es_translation, _translate.translate(message, 'es')) + self.assertEqual(es_translation, _translate.translate(obj, 'es')) @@ -32,4 +32,8 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' show-source = True ignore = E123,E125,H803 builtins = _ -exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build, + +[hacking] +import_exceptions = + oslo.i18n._i18n._
\ No newline at end of file |