diff options
author | Doug Hellmann <doug@doughellmann.com> | 2014-10-09 15:30:41 -0400 |
---|---|---|
committer | Doug Hellmann <doug@doughellmann.com> | 2014-12-18 16:35:03 -0500 |
commit | ba05e9a9b919e844121164fd23c560056da8a7bb (patch) | |
tree | 8c3d5545161823d1883d292eff7233145092aee4 /oslo_i18n | |
parent | 53635eae0f7db09fb9618dd71ed632e83a6ac8b6 (diff) | |
download | oslo-i18n-ba05e9a9b919e844121164fd23c560056da8a7bb.tar.gz |
Move out of the oslo namespace package
Move the public API out of oslo.i18n to oslo_i18n. Retain the ability to
import from the old namespace package for backwards compatibility for
this release cycle.
bp/drop-namespace-packages
Change-Id: I800f121c271d8e69f6e776c4aef509bbb8008170
Diffstat (limited to 'oslo_i18n')
-rw-r--r-- | oslo_i18n/__init__.py | 16 | ||||
-rw-r--r-- | oslo_i18n/_factory.py | 110 | ||||
-rw-r--r-- | oslo_i18n/_gettextutils.py | 90 | ||||
-rw-r--r-- | oslo_i18n/_i18n.py | 25 | ||||
-rw-r--r-- | oslo_i18n/_lazy.py | 38 | ||||
-rw-r--r-- | oslo_i18n/_locale.py | 25 | ||||
-rw-r--r-- | oslo_i18n/_message.py | 167 | ||||
-rw-r--r-- | oslo_i18n/_translate.py | 73 | ||||
-rw-r--r-- | oslo_i18n/fixture.py | 65 | ||||
-rw-r--r-- | oslo_i18n/log.py | 97 | ||||
-rw-r--r-- | oslo_i18n/tests/__init__.py | 0 | ||||
-rw-r--r-- | oslo_i18n/tests/fakes.py | 59 | ||||
-rw-r--r-- | oslo_i18n/tests/test_factory.py | 91 | ||||
-rw-r--r-- | oslo_i18n/tests/test_fixture.py | 38 | ||||
-rw-r--r-- | oslo_i18n/tests/test_gettextutils.py | 128 | ||||
-rw-r--r-- | oslo_i18n/tests/test_handler.py | 106 | ||||
-rw-r--r-- | oslo_i18n/tests/test_lazy.py | 40 | ||||
-rw-r--r-- | oslo_i18n/tests/test_locale_dir_variable.py | 32 | ||||
-rw-r--r-- | oslo_i18n/tests/test_logging.py | 42 | ||||
-rw-r--r-- | oslo_i18n/tests/test_message.py | 518 | ||||
-rw-r--r-- | oslo_i18n/tests/test_public_api.py | 44 | ||||
-rw-r--r-- | oslo_i18n/tests/test_translate.py | 44 | ||||
-rw-r--r-- | oslo_i18n/tests/utils.py | 42 |
23 files changed, 1890 insertions, 0 deletions
diff --git a/oslo_i18n/__init__.py b/oslo_i18n/__init__.py new file mode 100644 index 0000000..4602749 --- /dev/null +++ b/oslo_i18n/__init__.py @@ -0,0 +1,16 @@ +# 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 ._factory import * +from ._gettextutils import * +from ._lazy import * +from ._translate import * diff --git a/oslo_i18n/_factory.py b/oslo_i18n/_factory.py new file mode 100644 index 0000000..73ab217 --- /dev/null +++ b/oslo_i18n/_factory.py @@ -0,0 +1,110 @@ +# 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 + + +__all__ = [ + 'TranslatorFactory', +] + + +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: + variable_name = _locale.get_locale_dir_variable_name(domain) + localedir = os.environ.get(variable_name) + 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/_gettextutils.py b/oslo_i18n/_gettextutils.py new file mode 100644 index 0000000..75a8313 --- /dev/null +++ b/oslo_i18n/_gettextutils.py @@ -0,0 +1,90 @@ +# 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. + +"""gettextutils provides a wrapper around gettext for OpenStack projects +""" + +import copy +import gettext +import os + +from babel import localedata +import six + +from oslo_i18n import _factory +from oslo_i18n import _locale + +__all__ = [ + 'install', + 'get_available_languages', +] + + +def install(domain): + """Install a _() function using the given translation domain. + + Given a translation domain, install a _() function using gettext's + install() function. + + The main difference from gettext.install() is that we allow + overriding the default localedir (e.g. /usr/share/locale) using + a translation-domain-specific environment variable (e.g. + NOVA_LOCALEDIR). + + :param domain: the translation domain + """ + from six import moves + tf = _factory.TranslatorFactory(domain) + moves.builtins.__dict__['_'] = tf.primary + + +_AVAILABLE_LANGUAGES = {} + + +def get_available_languages(domain): + """Lists the available languages for the given translation domain. + + :param domain: the domain to get languages for + """ + if domain in _AVAILABLE_LANGUAGES: + return copy.copy(_AVAILABLE_LANGUAGES[domain]) + + localedir = os.environ.get(_locale.get_locale_dir_variable_name(domain)) + find = lambda x: gettext.find(domain, + localedir=localedir, + languages=[x]) + + # NOTE(mrodden): en_US should always be available (and first in case + # order matters) since our in-line message strings are en_US + language_list = ['en_US'] + locale_identifiers = localedata.locale_identifiers() + language_list.extend(language for language in locale_identifiers + if find(language)) + + # In Babel 1.3, locale_identifiers() doesn't list some OpenStack supported + # locales (e.g. 'zh_CN', and 'zh_TW') so we add the locales explicitly if + # necessary so that they are listed as supported. + aliases = {'zh': 'zh_CN', + 'zh_Hant_HK': 'zh_HK', + 'zh_Hant': 'zh_TW', + 'fil': 'tl_PH'} + + language_list.extend(alias for locale, alias in six.iteritems(aliases) + if (locale in language_list and + alias not in language_list)) + + _AVAILABLE_LANGUAGES[domain] = language_list + return copy.copy(language_list) diff --git a/oslo_i18n/_i18n.py b/oslo_i18n/_i18n.py new file mode 100644 index 0000000..7755571 --- /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..82de17d --- /dev/null +++ b/oslo_i18n/_lazy.py @@ -0,0 +1,38 @@ +# 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. + +__all__ = [ + 'enable_lazy', +] + +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..51908db --- /dev/null +++ b/oslo_i18n/_locale.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. + + +def get_locale_dir_variable_name(domain): + """Build environment variable name for local dir. + + 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..ceb39e7 --- /dev/null +++ b/oslo_i18n/_message.py @@ -0,0 +1,167 @@ +# 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 _locale +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 + desired_locale = system_locale[0] or 'en_US' + + locale_dir = os.environ.get( + _locale.get_locale_dir_variable_name(domain) + ) + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) + translator = lang.gettext if six.PY3 else 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): + params.update((key, self._copy_param(val)) + for key, val in self.params.items()) + params.update((key, self._copy_param(val)) + for key, val in other.items()) + 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..1809e1e --- /dev/null +++ b/oslo_i18n/_translate.py @@ -0,0 +1,73 @@ +# 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 + +__all__ = [ + 'translate', +] + + +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 = dict((key, translate(value, desired_locale)) + for key, value in six.iteritems(args)) + return translated_dict + return translate(args, desired_locale) diff --git a/oslo_i18n/fixture.py b/oslo_i18n/fixture.py new file mode 100644 index 0000000..076c708 --- /dev/null +++ b/oslo_i18n/fixture.py @@ -0,0 +1,65 @@ +# 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. +"""Test fixtures for working with oslo_i18n. + +""" + +import fixtures +import six + +from oslo_i18n import _message + + +class Translation(fixtures.Fixture): + """Fixture for managing translatable strings. + + This class provides methods for creating translatable strings + using both lazy translation and immediate translation. It can be + used to generate the different types of messages returned from + oslo_i18n to test code that may need to know about the type to + handle them differently (for example, error handling in WSGI apps, + or logging). + + Use this class to generate messages instead of toggling the global + lazy flag and using the regular translation factory. + + """ + + def __init__(self, domain='test-domain'): + """Initialize the fixture. + + :param domain: The translation domain. This is not expected to + coincide with an actual set of message + catalogs, but it can. + :type domain: str + """ + self.domain = domain + + def lazy(self, msg): + """Return a lazily translated message. + + :param msg: Input message string. May optionally include + positional or named string interpolation markers. + :type msg: str or unicode + + """ + return _message.Message(msg, domain=self.domain) + + def immediate(self, msg): + """Return a string as though it had been translated immediately. + + :param msg: Input message string. May optionally include + positional or named string interpolation markers. + :type msg: str or unicode + + """ + return six.text_type(msg) diff --git a/oslo_i18n/log.py b/oslo_i18n/log.py new file mode 100644 index 0000000..563c1b3 --- /dev/null +++ b/oslo_i18n/log.py @@ -0,0 +1,97 @@ +# 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. + + When lazy translation is enabled in the application (see + :func:`~oslo_i18n.enable_lazy`), the :class:`TranslationHandler` + uses its locale configuration setting to determine how to + translate LogRecord objects before forwarding them to the + logging.Handler. + + When lazy translation is disabled, the message in the LogRecord is + converted to unicode without any changes and then forwarded to the + logging.Handler. + + 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 = oslo_i18n.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/oslo_i18n/tests/__init__.py b/oslo_i18n/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/oslo_i18n/tests/__init__.py diff --git a/oslo_i18n/tests/fakes.py b/oslo_i18n/tests/fakes.py new file mode 100644 index 0000000..6bed973 --- /dev/null +++ b/oslo_i18n/tests/fakes.py @@ -0,0 +1,59 @@ +# Copyright 2012 Intel Inc, OpenStack Foundation. +# 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. + +""" +Fakes For translation tests. +""" + +import gettext + + +class FakeTranslations(gettext.GNUTranslations): + """A test GNUTranslations class that takes a map of msg -> translations.""" + + def __init__(self, translations): + self.translations = translations + + # used by Python 3 + def gettext(self, msgid): + return self.translations.get(msgid, msgid) + + # used by Python 2 + def ugettext(self, msgid): + return self.translations.get(msgid, msgid) + + @staticmethod + def translator(locales_map): + """Build mock translator for the given locales. + + Returns a mock gettext.translation function that uses + individual TestTranslations to translate in the given locales. + + :param locales_map: A map from locale name to a translations map. + { + 'es': {'Hi': 'Hola', 'Bye': 'Adios'}, + 'zh': {'Hi': 'Ni Hao', 'Bye': 'Zaijian'} + } + + + """ + def _translation(domain, localedir=None, + languages=None, fallback=None): + if languages: + language = languages[0] + if language in locales_map: + return FakeTranslations(locales_map[language]) + return gettext.NullTranslations() + return _translation diff --git a/oslo_i18n/tests/test_factory.py b/oslo_i18n/tests/test_factory.py new file mode 100644 index 0000000..771728e --- /dev/null +++ b/oslo_i18n/tests/test_factory.py @@ -0,0 +1,91 @@ +# 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 mock +from oslotest import base as test_base +import six + +from oslo_i18n import _factory +from oslo_i18n import _lazy +from oslo_i18n import _message + + +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 = _lazy.USE_LAZY + + def tearDown(self): + # reset to value before test + _lazy.USE_LAZY = self._USE_LAZY + super(TranslatorFactoryTest, self).tearDown() + + def test_lazy(self): + _lazy.enable_lazy(True) + with mock.patch.object(_message, 'Message') as msg: + tf = _factory.TranslatorFactory('domain') + tf.primary('some text') + msg.assert_called_with('some text', domain='domain') + + def test_not_lazy(self): + _lazy.enable_lazy(False) + with mock.patch.object(_message, 'Message') as msg: + msg.side_effect = AssertionError('should not use Message') + tf = _factory.TranslatorFactory('domain') + tf.primary('some text') + + def test_change_lazy(self): + _lazy.enable_lazy(True) + tf = _factory.TranslatorFactory('domain') + r = tf.primary('some text') + self.assertIsInstance(r, _message.Message) + _lazy.enable_lazy(False) + r = tf.primary('some text') + # Python 2.6 doesn't have assertNotIsInstance(). + self.assertFalse(isinstance(r, _message.Message)) + + def test_py2(self): + _lazy.enable_lazy(False) + with mock.patch.object(six, 'PY3', False): + with mock.patch('gettext.translation') as translation: + trans = mock.Mock() + translation.return_value = trans + trans.gettext.side_effect = AssertionError( + 'should have called ugettext') + tf = _factory.TranslatorFactory('domain') + tf.primary('some text') + trans.ugettext.assert_called_with('some text') + + def test_py3(self): + _lazy.enable_lazy(False) + with mock.patch.object(six, 'PY3', True): + with mock.patch('gettext.translation') as translation: + trans = mock.Mock() + translation.return_value = trans + trans.ugettext.side_effect = AssertionError( + 'should have called gettext') + tf = _factory.TranslatorFactory('domain') + tf.primary('some text') + trans.gettext.assert_called_with('some text') + + def test_log_level_domain_name(self): + with mock.patch.object(_factory.TranslatorFactory, + '_make_translation_func') as mtf: + tf = _factory.TranslatorFactory('domain') + tf._make_log_translation_func('mylevel') + mtf.assert_called_with('domain-log-mylevel') diff --git a/oslo_i18n/tests/test_fixture.py b/oslo_i18n/tests/test_fixture.py new file mode 100644 index 0000000..aca994f --- /dev/null +++ b/oslo_i18n/tests/test_fixture.py @@ -0,0 +1,38 @@ +# 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 +import six + +from oslo_i18n import _message +from oslo_i18n import fixture + + +class FixtureTest(test_base.BaseTestCase): + + def setUp(self): + super(FixtureTest, self).setUp() + self.trans_fixture = self.useFixture(fixture.Translation()) + + def test_lazy(self): + msg = self.trans_fixture.lazy('this is a lazy message') + self.assertIsInstance(msg, _message.Message) + self.assertEqual(msg.msgid, 'this is a lazy message') + + def test_immediate(self): + msg = self.trans_fixture.immediate('this is a lazy message') + # Python 2.6 does not have assertNotIsInstance + self.assertFalse(isinstance(msg, _message.Message)) + self.assertIsInstance(msg, six.text_type) + self.assertEqual(msg, u'this is a lazy message') diff --git a/oslo_i18n/tests/test_gettextutils.py b/oslo_i18n/tests/test_gettextutils.py new file mode 100644 index 0000000..f433700 --- /dev/null +++ b/oslo_i18n/tests/test_gettextutils.py @@ -0,0 +1,128 @@ +# 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 gettext +import logging + +from babel import localedata +import mock +from oslotest import base as test_base +from oslotest import moxstubout +import six + +from oslo_i18n import _factory +from oslo_i18n import _gettextutils +from oslo_i18n import _lazy +from oslo_i18n import _message + + +LOG = logging.getLogger(__name__) + + +class GettextTest(test_base.BaseTestCase): + + def setUp(self): + super(GettextTest, self).setUp() + moxfixture = self.useFixture(moxstubout.MoxStubout()) + self.stubs = moxfixture.stubs + self.mox = moxfixture.mox + # remember so we can reset to it later in case it changes + self._USE_LAZY = _lazy.USE_LAZY + self.t = _factory.TranslatorFactory('oslo_i18n.test') + + def tearDown(self): + # reset to value before test + _lazy.USE_LAZY = self._USE_LAZY + super(GettextTest, self).tearDown() + + def test_gettext_does_not_blow_up(self): + LOG.info(self.t.primary('test')) + + def test__gettextutils_install(self): + _gettextutils.install('blaa') + _lazy.enable_lazy(False) + self.assertTrue(isinstance(self.t.primary('A String'), + six.text_type)) + + _gettextutils.install('blaa') + _lazy.enable_lazy(True) + 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: + with mock.patch('gettext.install'): + environ_get.return_value = '/foo/bar' + _gettextutils.install('blaa') + environ_get.assert_calls([mock.call('BLAA_LOCALEDIR')]) + + def test_gettext_install_updates_builtins(self): + with mock.patch('os.environ.get') as environ_get: + with mock.patch('gettext.install'): + environ_get.return_value = '/foo/bar' + if '_' in six.moves.builtins.__dict__: + del six.moves.builtins.__dict__['_'] + _gettextutils.install('blaa') + self.assertIn('_', six.moves.builtins.__dict__) + + def test_get_available_languages(self): + # All the available languages for which locale data is available + def _mock_locale_identifiers(): + # 'zh', 'zh_Hant'. 'zh_Hant_HK', 'fil' all have aliases + # missing from babel but we add them in _gettextutils, we + # test that here too + return ['zh', 'es', 'nl', 'fr', 'zh_Hant', 'zh_Hant_HK', 'fil'] + + self.stubs.Set(localedata, + 'list' if hasattr(localedata, 'list') + else 'locale_identifiers', + _mock_locale_identifiers) + + # Only the languages available for a specific translation domain + def _mock_gettext_find(domain, localedir=None, languages=None, all=0): + languages = languages or [] + if domain == 'domain_1': + return 'translation-file' if any(x in ['zh', 'es', 'fil'] + for x in languages) else None + elif domain == 'domain_2': + return 'translation-file' if any(x in ['fr', 'zh_Hant'] + for x in languages) else None + return None + self.stubs.Set(gettext, 'find', _mock_gettext_find) + + # en_US should always be available no matter the domain + # and it should also always be the first element since order matters + domain_1_languages = _gettextutils.get_available_languages('domain_1') + domain_2_languages = _gettextutils.get_available_languages('domain_2') + self.assertEqual('en_US', domain_1_languages[0]) + self.assertEqual('en_US', domain_2_languages[0]) + # The domain languages should be included after en_US with + # with their respective aliases when it applies + self.assertEqual(6, len(domain_1_languages)) + self.assertIn('zh', domain_1_languages) + self.assertIn('zh_CN', domain_1_languages) + self.assertIn('es', domain_1_languages) + self.assertIn('fil', domain_1_languages) + self.assertIn('tl_PH', domain_1_languages) + self.assertEqual(4, len(domain_2_languages)) + self.assertIn('fr', domain_2_languages) + self.assertIn('zh_Hant', domain_2_languages) + self.assertIn('zh_TW', domain_2_languages) + self.assertEqual(2, len(_gettextutils._AVAILABLE_LANGUAGES)) + # Now test an unknown domain, only en_US should be included + unknown_domain_languages = _gettextutils.get_available_languages('huh') + self.assertEqual(1, len(unknown_domain_languages)) + self.assertIn('en_US', unknown_domain_languages) diff --git a/oslo_i18n/tests/test_handler.py b/oslo_i18n/tests/test_handler.py new file mode 100644 index 0000000..b0c678e --- /dev/null +++ b/oslo_i18n/tests/test_handler.py @@ -0,0 +1,106 @@ +# 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 logging + +import mock +from oslotest import base as test_base +import six + +from oslo_i18n import _message +from oslo_i18n import log as i18n_log +from oslo_i18n.tests import fakes + +LOG = logging.getLogger(__name__) + + +class TranslationHandlerTestCase(test_base.BaseTestCase): + + def setUp(self): + super(TranslationHandlerTestCase, self).setUp() + + self.stream = six.StringIO() + self.destination_handler = logging.StreamHandler(self.stream) + self.translation_handler = i18n_log.TranslationHandler('zh_CN') + self.translation_handler.setTarget(self.destination_handler) + + self.logger = logging.getLogger('localehander_logger') + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.translation_handler) + + def test_set_formatter(self): + formatter = 'some formatter' + self.translation_handler.setFormatter(formatter) + self.assertEqual(formatter, self.translation_handler.target.formatter) + + @mock.patch('gettext.translation') + def test_emit_translated_message(self, mock_translation): + log_message = 'A message to be logged' + log_message_translation = 'A message to be logged in Chinese' + translations = {log_message: log_message_translation} + translations_map = {'zh_CN': translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + + msg = _message.Message(log_message) + + self.logger.info(msg) + self.assertIn(log_message_translation, self.stream.getvalue()) + + @mock.patch('gettext.translation') + def test_emit_translated_message_with_args(self, mock_translation): + log_message = 'A message to be logged %s' + log_message_translation = 'A message to be logged in Chinese %s' + log_arg = 'Arg to be logged' + log_arg_translation = 'An arg to be logged in Chinese' + + translations = {log_message: log_message_translation, + log_arg: log_arg_translation} + translations_map = {'zh_CN': translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + + msg = _message.Message(log_message) + arg = _message.Message(log_arg) + + self.logger.info(msg, arg) + self.assertIn(log_message_translation % log_arg_translation, + self.stream.getvalue()) + + @mock.patch('gettext.translation') + def test_emit_translated_message_with_named_args(self, mock_translation): + log_message = 'A message to be logged %(arg1)s $(arg2)s' + log_message_translation = 'Chinese msg to be logged %(arg1)s $(arg2)s' + log_arg_1 = 'Arg1 to be logged' + log_arg_1_translation = 'Arg1 to be logged in Chinese' + log_arg_2 = 'Arg2 to be logged' + log_arg_2_translation = 'Arg2 to be logged in Chinese' + + translations = {log_message: log_message_translation, + log_arg_1: log_arg_1_translation, + log_arg_2: log_arg_2_translation} + translations_map = {'zh_CN': translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + + 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, + 'arg2': log_arg_2_translation} + self.assertIn(translation, self.stream.getvalue()) diff --git a/oslo_i18n/tests/test_lazy.py b/oslo_i18n/tests/test_lazy.py new file mode 100644 index 0000000..049d51a --- /dev/null +++ b/oslo_i18n/tests/test_lazy.py @@ -0,0 +1,40 @@ +# 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 + + +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 + _lazy.enable_lazy() + self.assertTrue(_lazy.USE_LAZY) + + def test_disable_lazy(self): + _lazy.USE_LAZY = True + _lazy.enable_lazy(False) + self.assertFalse(_lazy.USE_LAZY) diff --git a/oslo_i18n/tests/test_locale_dir_variable.py b/oslo_i18n/tests/test_locale_dir_variable.py new file mode 100644 index 0000000..26321c1 --- /dev/null +++ b/oslo_i18n/tests/test_locale_dir_variable.py @@ -0,0 +1,32 @@ +# 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 +import testscenarios.testcase + +from oslo_i18n import _locale + + +class LocaleDirVariableTest(testscenarios.testcase.WithScenarios, + test_base.BaseTestCase): + + scenarios = [ + ('simple', {'domain': 'simple', 'expected': 'SIMPLE_LOCALEDIR'}), + ('with_dot', {'domain': 'one.two', 'expected': 'ONE_TWO_LOCALEDIR'}), + ('with_dash', {'domain': 'one-two', 'expected': 'ONE_TWO_LOCALEDIR'}), + ] + + def test_make_variable_name(self): + var = _locale.get_locale_dir_variable_name(self.domain) + self.assertEqual(self.expected, var) diff --git a/oslo_i18n/tests/test_logging.py b/oslo_i18n/tests/test_logging.py new file mode 100644 index 0000000..07e5c71 --- /dev/null +++ b/oslo_i18n/tests/test_logging.py @@ -0,0 +1,42 @@ +# 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 mock +from oslotest import base as test_base + +from oslo_i18n import _factory + + +class LogLevelTranslationsTest(test_base.BaseTestCase): + + def test_info(self): + self._test('info') + + def test_warning(self): + self._test('warning') + + def test_error(self): + self._test('error') + + def test_critical(self): + self._test('critical') + + def _test(self, level): + with mock.patch.object(_factory.TranslatorFactory, + '_make_translation_func') as mtf: + tf = _factory.TranslatorFactory('domain') + getattr(tf, 'log_%s' % level) + mtf.assert_called_with('domain-log-%s' % level) diff --git a/oslo_i18n/tests/test_message.py b/oslo_i18n/tests/test_message.py new file mode 100644 index 0000000..0fab2fa --- /dev/null +++ b/oslo_i18n/tests/test_message.py @@ -0,0 +1,518 @@ +# 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 logging + +import mock +from oslotest import base as test_base +import six +import testtools + +from oslo_i18n import _message +from oslo_i18n.tests import fakes +from oslo_i18n.tests import utils + +LOG = logging.getLogger(__name__) + + +class MessageTestCase(test_base.BaseTestCase): + """Unit tests for locale Message class.""" + + def test_message_id_and_message_text(self): + message = _message.Message('1') + self.assertEqual('1', message.msgid) + self.assertEqual('1', message) + message = _message.Message('1', msgtext='A') + self.assertEqual('1', message.msgid) + self.assertEqual('A', message) + + def test_message_is_unicode(self): + message = _message.Message('some %s') % 'message' + self.assertIsInstance(message, six.text_type) + + @mock.patch('locale.getdefaultlocale') + @mock.patch('gettext.translation') + def test_create_message_non_english_default_locale(self, + mock_translation, + mock_getdefaultlocale): + msgid = 'A message in English' + es_translation = 'A message in Spanish' + + es_translations = {msgid: es_translation} + translations_map = {'es': es_translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + mock_getdefaultlocale.return_value = ('es',) + + 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. + self.assertEqual(es_translation, message) + self.assertEqual(es_translation, message.translate()) + + def test_translate_returns_unicode(self): + message = _message.Message('some %s') % 'message' + self.assertIsInstance(message.translate(), six.text_type) + + def test_mod_with_named_parameters(self): + msgid = ("%(description)s\nCommand: %(cmd)s\n" + "Exit code: %(exit_code)s\nStdout: %(stdout)r\n" + "Stderr: %(stderr)r %%(something)s") + params = {'description': 'test1', + 'cmd': 'test2', + 'exit_code': 'test3', + 'stdout': 'test4', + 'stderr': 'test5', + 'something': 'trimmed'} + + result = _message.Message(msgid) % params + + expected = msgid % params + self.assertEqual(result, expected) + self.assertEqual(result.translate(), expected) + + def test_multiple_mod_with_named_parameter(self): + msgid = ("%(description)s\nCommand: %(cmd)s\n" + "Exit code: %(exit_code)s\nStdout: %(stdout)r\n" + "Stderr: %(stderr)r") + params = {'description': 'test1', + 'cmd': 'test2', + 'exit_code': 'test3', + 'stdout': 'test4', + 'stderr': 'test5'} + + # Run string interpolation the first time to make a new Message + 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 + # implemented in OpenStack. We should receive a second Message + # object, but the translation results should be the same. + # + # The production code that triggers this problem does something + # like: + # + # msg = _('there was a problem %(name)s') % {'name': 'some value'} + # LOG.error(msg) + # raise BadExceptionClass(msg) + # + # where BadExceptionClass does something like: + # + # class BadExceptionClass(Exception): + # def __init__(self, msg, **kwds): + # super(BadExceptionClass, self).__init__(msg % kwds) + # + expected = first % {} + + # Base message id should be the same + self.assertEqual(first.msgid, expected.msgid) + + # Preserved arguments should be the same + self.assertEqual(first.params, expected.params) + + # Should have different objects + self.assertIsNot(expected, first) + + # Final translations should be the same + self.assertEqual(expected.translate(), first.translate()) + + def test_mod_with_named_parameters_no_space(self): + msgid = ("Request: %(method)s http://%(server)s:" + "%(port)s%(url)s with headers %(headers)s") + params = {'method': 'POST', + 'server': 'test1', + 'port': 1234, + 'url': 'test2', + 'headers': {'h1': 'val1'}} + + result = _message.Message(msgid) % params + + expected = msgid % params + self.assertEqual(result, expected) + self.assertEqual(result.translate(), expected) + + def test_mod_with_dict_parameter(self): + msgid = "Test that we can inject a dictionary %s" + params = {'description': 'test1'} + + result = _message.Message(msgid) % params + + expected = msgid % params + self.assertEqual(expected, result) + self.assertEqual(expected, result.translate()) + + def test_mod_with_integer_parameters(self): + msgid = "Some string with params: %d" + params = [0, 1, 10, 24124] + + messages = [] + results = [] + for param in params: + messages.append(msgid % param) + results.append(_message.Message(msgid) % param) + + for message, result in zip(messages, results): + self.assertEqual(type(result), _message.Message) + self.assertEqual(result.translate(), message) + + # simulate writing out as string + result_str = '%s' % result.translate() + self.assertEqual(result_str, message) + self.assertEqual(result, message) + + def test_mod_copies_parameters(self): + msgid = "Found object: %(current_value)s" + changing_dict = {'current_value': 1} + # A message created with some params + result = _message.Message(msgid) % changing_dict + # The parameters may change + changing_dict['current_value'] = 2 + # Even if the param changes when the message is + # translated it should use the original param + self.assertEqual(result.translate(), 'Found object: 1') + + def test_mod_deep_copies_parameters(self): + msgid = "Found list: %(current_list)s" + changing_list = list([1, 2, 3]) + params = {'current_list': changing_list} + # Apply the params + result = _message.Message(msgid) % params + # Change the list + changing_list.append(4) + # Even though the list changed the message + # translation should use the original list + self.assertEqual(result.translate(), "Found list: [1, 2, 3]") + + def test_mod_deep_copies_param_nodeep_param(self): + msgid = "Value: %s" + params = utils.NoDeepCopyObject(5) + # Apply the 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 = _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 = _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 = _message.Message(msgid) + m1 = message % {'test1': 'foo', 'test2': 'bar'} + m2 = message % {'test1': 'foo2', 'test2': 'bar2'} + + self.assertIsNot(message, m1) + self.assertIsNot(message, m2) + self.assertEqual(m1.translate(), + msgid % {'test1': 'foo', 'test2': 'bar'}) + self.assertEqual(m2.translate(), + msgid % {'test1': 'foo2', 'test2': 'bar2'}) + + def test_mod_with_none_parameter(self): + msgid = "Some string with params: %s" + 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: _message.Message(msgid) % 'just one' + # Just like with strings missing parameters raise TypeError + self.assertRaises(TypeError, test_me) + + def test_mod_with_extra_parameters(self): + msgid = "Some string with params: %(param1)s %(param2)s" + params = {'param1': 'test', + 'param2': 'test2', + 'param3': 'notinstring'} + + result = _message.Message(msgid) % params + + expected = msgid % params + self.assertEqual(result, expected) + self.assertEqual(result.translate(), expected) + + # Make sure unused params still there + self.assertEqual(result.params.keys(), params.keys()) + + def test_mod_with_missing_named_parameters(self): + msgid = ("Some string with params: %(param1)s %(param2)s" + " and a missing one %(missing)s") + params = {'param1': 'test', + 'param2': 'test2'} + + 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: _message.Message(msgid) + ' some string' + self.assertRaises(TypeError, test_me) + + def test_radd_disabled(self): + msgid = "A message" + 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(_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 = _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 + + self.assertEqual(es_translation, message.translate('es')) + + @mock.patch('gettext.translation') + 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 = _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 + + # Here we are not testing the Message object directly but the result + # of unicoding() an object whose unicode representation is a Message + obj = utils.SomeObject(message) + unicoded_obj = six.text_type(obj) + + self.assertEqual(es_translation, unicoded_obj.translate('es')) + + @mock.patch('gettext.translation') + def test_translate_multiple_languages(self, mock_translation): + en_message = 'A message in the default locale' + es_translation = 'A message in Spanish' + zh_translation = 'A message in Chinese' + message = _message.Message(en_message) + + es_translations = {en_message: es_translation} + zh_translations = {en_message: zh_translation} + translations_map = {'es': es_translations, + 'zh': zh_translations} + translator = fakes.FakeTranslations.translator(translations_map) + mock_translation.side_effect = translator + + self.assertEqual(es_translation, message.translate('es')) + self.assertEqual(zh_translation, message.translate('zh')) + self.assertEqual(en_message, message.translate(None)) + self.assertEqual(en_message, message.translate('en')) + self.assertEqual(en_message, message.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_param(self, mock_translation): + message_with_params = 'A message: %s' + es_translation = 'A message in Spanish: %s' + param = 'A Message param' + + translations = {message_with_params: es_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + msg = _message.Message(message_with_params) + msg = msg % param + + default_translation = message_with_params % param + expected_translation = es_translation % param + self.assertEqual(expected_translation, msg.translate('es')) + self.assertEqual(default_translation, msg.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_object_param(self, mock_translation): + message_with_params = 'A message: %s' + es_translation = 'A message in Spanish: %s' + param = 'A Message param' + param_translation = 'A Message param in Spanish' + + translations = {message_with_params: es_translation, + param: param_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + 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 + # very common for instance when modding an Exception with a Message + obj = utils.SomeObject(param_msg) + msg = msg % obj + + default_translation = message_with_params % param + expected_translation = es_translation % param_translation + + self.assertEqual(expected_translation, msg.translate('es')) + self.assertEqual(default_translation, msg.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_param_from_unicoded_obj(self, + mock_translation): + message_with_params = 'A message: %s' + es_translation = 'A message in Spanish: %s' + param = 'A Message param' + + translations = {message_with_params: es_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + msg = _message.Message(message_with_params) + msg = msg % param + + default_translation = message_with_params % param + expected_translation = es_translation % param + + obj = utils.SomeObject(msg) + unicoded_obj = six.text_type(obj) + + self.assertEqual(expected_translation, unicoded_obj.translate('es')) + self.assertEqual(default_translation, unicoded_obj.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_message_parameter(self, mock_translation): + message_with_params = 'A message with param: %s' + es_translation = 'A message with param in Spanish: %s' + message_param = 'A message param' + es_param_translation = 'A message param in Spanish' + + translations = {message_with_params: es_translation, + message_param: es_param_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + msg = _message.Message(message_with_params) + msg_param = _message.Message(message_param) + msg = msg % msg_param + + default_translation = message_with_params % message_param + expected_translation = es_translation % es_param_translation + self.assertEqual(expected_translation, msg.translate('es')) + self.assertEqual(default_translation, msg.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_message_parameters(self, mock_translation): + message_with_params = 'A message with params: %s %s' + es_translation = 'A message with params in Spanish: %s %s' + message_param = 'A message param' + es_param_translation = 'A message param in Spanish' + another_message_param = 'Another message param' + another_es_param_translation = 'Another message param in Spanish' + + translations = {message_with_params: es_translation, + message_param: es_param_translation, + another_message_param: another_es_param_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + 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, + another_message_param) + expected_translation = es_translation % (es_param_translation, + another_es_param_translation) + self.assertEqual(expected_translation, msg.translate('es')) + self.assertEqual(default_translation, msg.translate('XX')) + + @mock.patch('gettext.translation') + def test_translate_message_with_named_parameters(self, mock_translation): + message_with_params = 'A message with params: %(param)s' + es_translation = 'A message with params in Spanish: %(param)s' + message_param = 'A Message param' + es_param_translation = 'A message param in Spanish' + + translations = {message_with_params: es_translation, + message_param: es_param_translation} + translator = fakes.FakeTranslations.translator({'es': translations}) + mock_translation.side_effect = translator + + 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} + expected_translation = es_translation % {'param': es_param_translation} + self.assertEqual(expected_translation, msg.translate('es')) + self.assertEqual(default_translation, msg.translate('XX')) + + @mock.patch('locale.getdefaultlocale') + @mock.patch('gettext.translation') + def test_translate_message_non_default_locale(self, + mock_translation, + mock_getdefaultlocale): + message_with_params = 'A message with params: %(param)s' + es_translation = 'A message with params in Spanish: %(param)s' + zh_translation = 'A message with params in Chinese: %(param)s' + fr_translation = 'A message with params in French: %(param)s' + + message_param = 'A Message param' + es_param_translation = 'A message param in Spanish' + zh_param_translation = 'A message param in Chinese' + fr_param_translation = 'A message param in French' + + es_translations = {message_with_params: es_translation, + message_param: es_param_translation} + zh_translations = {message_with_params: zh_translation, + message_param: zh_param_translation} + fr_translations = {message_with_params: fr_translation, + message_param: fr_param_translation} + + translator = fakes.FakeTranslations.translator({'es': es_translations, + 'zh': zh_translations, + 'fr': fr_translations}) + mock_translation.side_effect = translator + mock_getdefaultlocale.return_value = ('es',) + + 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} + zh_translation = zh_translation % {'param': zh_param_translation} + fr_translation = fr_translation % {'param': fr_param_translation} + + # Because sys.getdefaultlocale() was Spanish, + # the default translation will be to Spanish + self.assertEqual(es_translation, msg) + self.assertEqual(es_translation, msg.translate()) + self.assertEqual(es_translation, msg.translate('es')) + + # Translation into other locales still works + self.assertEqual(zh_translation, msg.translate('zh')) + self.assertEqual(fr_translation, msg.translate('fr')) diff --git a/oslo_i18n/tests/test_public_api.py b/oslo_i18n/tests/test_public_api.py new file mode 100644 index 0000000..d419105 --- /dev/null +++ b/oslo_i18n/tests/test_public_api.py @@ -0,0 +1,44 @@ +# 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. +"""A few tests that use the public API to ensure the imports work. +""" + +import unittest + +import mock + +import oslo_i18n +from oslo_i18n import _lazy + + +class PublicAPITest(unittest.TestCase): + + def test_create_factory(self): + oslo_i18n.TranslatorFactory('domain') + + def test_install(self): + with mock.patch('six.moves.builtins'): + oslo_i18n.install('domain') + + def test_get_available_languages(self): + oslo_i18n.get_available_languages('domains') + + def test_toggle_lazy(self): + original = _lazy.USE_LAZY + try: + oslo_i18n.enable_lazy(True) + oslo_i18n.enable_lazy(False) + finally: + oslo_i18n.enable_lazy(original) + + def test_translate(self): + oslo_i18n.translate(u'string') diff --git a/oslo_i18n/tests/test_translate.py b/oslo_i18n/tests/test_translate.py new file mode 100644 index 0000000..335b28c --- /dev/null +++ b/oslo_i18n/tests/test_translate.py @@ -0,0 +1,44 @@ +# 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 oslo_i18n import _message +from oslo_i18n import _translate +from oslo_i18n.tests import fakes +from oslo_i18n.tests import utils + + +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')) diff --git a/oslo_i18n/tests/utils.py b/oslo_i18n/tests/utils.py new file mode 100644 index 0000000..a6ad6c3 --- /dev/null +++ b/oslo_i18n/tests/utils.py @@ -0,0 +1,42 @@ +# 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 + + +class SomeObject(object): + + def __init__(self, message): + self.message = message + + def __unicode__(self): + return self.message + # alias for Python 3 + __str__ = __unicode__ + + +class NoDeepCopyObject(object): + + def __init__(self, value): + self.value = value + + if six.PY3: + def __str__(self): + return str(self.value) + else: + def __unicode__(self): + return unicode(self.value) + + def __deepcopy__(self, memo): + raise TypeError('Deep Copy not supported') |