summaryrefslogtreecommitdiff
path: root/oslo_i18n
diff options
context:
space:
mode:
authorDoug Hellmann <doug@doughellmann.com>2014-10-09 15:30:41 -0400
committerDoug Hellmann <doug@doughellmann.com>2014-12-18 16:35:03 -0500
commitba05e9a9b919e844121164fd23c560056da8a7bb (patch)
tree8c3d5545161823d1883d292eff7233145092aee4 /oslo_i18n
parent53635eae0f7db09fb9618dd71ed632e83a6ac8b6 (diff)
downloadoslo-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__.py16
-rw-r--r--oslo_i18n/_factory.py110
-rw-r--r--oslo_i18n/_gettextutils.py90
-rw-r--r--oslo_i18n/_i18n.py25
-rw-r--r--oslo_i18n/_lazy.py38
-rw-r--r--oslo_i18n/_locale.py25
-rw-r--r--oslo_i18n/_message.py167
-rw-r--r--oslo_i18n/_translate.py73
-rw-r--r--oslo_i18n/fixture.py65
-rw-r--r--oslo_i18n/log.py97
-rw-r--r--oslo_i18n/tests/__init__.py0
-rw-r--r--oslo_i18n/tests/fakes.py59
-rw-r--r--oslo_i18n/tests/test_factory.py91
-rw-r--r--oslo_i18n/tests/test_fixture.py38
-rw-r--r--oslo_i18n/tests/test_gettextutils.py128
-rw-r--r--oslo_i18n/tests/test_handler.py106
-rw-r--r--oslo_i18n/tests/test_lazy.py40
-rw-r--r--oslo_i18n/tests/test_locale_dir_variable.py32
-rw-r--r--oslo_i18n/tests/test_logging.py42
-rw-r--r--oslo_i18n/tests/test_message.py518
-rw-r--r--oslo_i18n/tests/test_public_api.py44
-rw-r--r--oslo_i18n/tests/test_translate.py44
-rw-r--r--oslo_i18n/tests/utils.py42
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')