2249925846
It shadows the unicode function of the same name, and there's no way to entirely disambiguate the two based on the parameter passed in. This change deprecates Message.translate and makes it a wrapper around the new function, Message.translation. Note that we never documented calling Message.translate directly. The documented way to get the translated form of a Message is to call the _translate.translate function in this project, so chances are that this public API change will have little actual impact on users. Change-Id: I0c617937f5af7467d1454f72acd0474ae2ce0293 Closes-Bug: 1841796
268 lines
12 KiB
Python
268 lines
12 KiB
Python
# 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 logging
|
|
import os
|
|
import warnings
|
|
|
|
import six
|
|
|
|
from oslo_i18n import _locale
|
|
from oslo_i18n import _translate
|
|
|
|
# magic gettext number to separate context from message
|
|
CONTEXT_SEPARATOR = "\x04"
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
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', has_contextual_form=False,
|
|
has_plural_form=False, *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
|
|
msg.has_contextual_form = has_contextual_form
|
|
msg.has_plural_form = has_plural_form
|
|
return msg
|
|
|
|
def translate(self, desired_locale=None):
|
|
"""DEPRECATED: Use ``translation`` instead
|
|
|
|
This is a compatibility shim to allow callers a chance to move away
|
|
from using this function, which shadows a built-in function from our
|
|
parent class.
|
|
"""
|
|
# We did a bad thing here. We shadowed the unicode built-in translate,
|
|
# which means there are circumstances where this function may be called
|
|
# with a desired_locale that is a non-string sequence or mapping type.
|
|
# This will not only result in incorrect behavior, it also fails
|
|
# because things like lists are not hashable, and we use the value in
|
|
# desired_locale as part of a dict key. If we see a non-string
|
|
# desired_locale, we know that the caller did not intend to call this
|
|
# form of translate and we should instead pass that along to the
|
|
# unicode implementation of translate.
|
|
#
|
|
# Unfortunately this doesn't entirely solve the problem as it would be
|
|
# possible for a caller to use a string as the mapping type and in that
|
|
# case we can't tell which version of translate they intended to call.
|
|
# That doesn't seem to be a common thing to do though. str.maketrans
|
|
# returns a dict, and that is probably the way most callers will create
|
|
# their mapping.
|
|
if (desired_locale is not None and
|
|
not isinstance(desired_locale, six.string_types)):
|
|
return super(Message, self).translate(desired_locale)
|
|
warnings.warn('Message.translate called with a string argument. '
|
|
'If your intent was to translate the message into '
|
|
'another language, please call Message.translation '
|
|
'instead. If your intent was to call "translate" as '
|
|
'defined by the str/unicode type, please use a dict or '
|
|
'list mapping instead. String mappings will not work '
|
|
'until this compatibility shim is removed.')
|
|
return self.translation(desired_locale)
|
|
|
|
def translation(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,
|
|
self.has_contextual_form,
|
|
self.has_plural_form)
|
|
|
|
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)
|
|
|
|
return self._safe_translate(translated_message, translated_params)
|
|
|
|
@staticmethod
|
|
def _translate_msgid(msgid, domain, desired_locale=None,
|
|
has_contextual_form=False, has_plural_form=False):
|
|
if not desired_locale:
|
|
system_locale = locale.getdefaultlocale()
|
|
# If the system locale is not available to the runtime use English
|
|
if not system_locale or not system_locale[0]:
|
|
desired_locale = 'en_US'
|
|
else:
|
|
desired_locale = system_locale[0]
|
|
|
|
locale_dir = os.environ.get(
|
|
_locale.get_locale_dir_variable_name(domain)
|
|
)
|
|
lang = gettext.translation(domain,
|
|
localedir=locale_dir,
|
|
languages=[desired_locale],
|
|
fallback=True)
|
|
|
|
if not has_contextual_form and not has_plural_form:
|
|
# This is the most common case, so check it first.
|
|
translator = lang.gettext if six.PY3 else lang.ugettext
|
|
translated_message = translator(msgid)
|
|
|
|
elif has_contextual_form and has_plural_form:
|
|
# Reserved for contextual and plural translation function,
|
|
# which is not yet implemented.
|
|
raise ValueError("Unimplemented.")
|
|
|
|
elif has_contextual_form:
|
|
(msgctx, msgtxt) = msgid
|
|
translator = lang.gettext if six.PY3 else lang.ugettext
|
|
|
|
msg_with_ctx = "%s%s%s" % (msgctx, CONTEXT_SEPARATOR, msgtxt)
|
|
translated_message = translator(msg_with_ctx)
|
|
|
|
if CONTEXT_SEPARATOR in translated_message:
|
|
# Translation not found, use the original text
|
|
translated_message = msgtxt
|
|
|
|
elif has_plural_form:
|
|
(msgsingle, msgplural, msgcount) = msgid
|
|
translator = lang.ngettext if six.PY3 else lang.ungettext
|
|
translated_message = translator(msgsingle, msgplural, msgcount)
|
|
|
|
return translated_message
|
|
|
|
def _safe_translate(self, translated_message, translated_params):
|
|
"""Trap translation errors and fall back to default translation.
|
|
|
|
:param translated_message: the requested translation
|
|
|
|
:param translated_params: the params to be inserted
|
|
|
|
:return: if parameter insertion is successful then it is the
|
|
translated_message with the translated_params inserted, if the
|
|
requested translation fails then it is the default translation
|
|
with the params
|
|
"""
|
|
|
|
try:
|
|
translated_message = translated_message % translated_params
|
|
except (KeyError, TypeError) as err:
|
|
# KeyError for parameters named in the translated_message
|
|
# but not found in translated_params and TypeError for
|
|
# type strings that do not match the type of the
|
|
# parameter.
|
|
#
|
|
# Log the error translating the message and use the
|
|
# original message string so the translator's bad message
|
|
# catalog doesn't break the caller.
|
|
# Do not translate this log message even if it is used as a
|
|
# warning message as a wrong translation of this message could
|
|
# cause infinite recursion
|
|
msg = (u'Failed to insert replacement values into translated '
|
|
u'message %s (Original: %r): %s')
|
|
warnings.warn(msg % (translated_message, self.msgid, err))
|
|
LOG.debug(msg, translated_message, self.msgid, err)
|
|
|
|
translated_message = self.msgid % translated_params
|
|
|
|
return translated_message
|
|
|
|
def __mod__(self, other):
|
|
# When we mod a Message we want the actual operation to be performed
|
|
# by the base 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 = self._safe_translate(six.text_type(self), 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)
|