1# Copyright 2012 Red Hat, Inc. 2# Copyright 2013 IBM Corp. 3# All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may 6# not use this file except in compliance with the License. You may obtain 7# a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations 15# under the License. 16"""Private Message class for lazy translation support. 17""" 18 19import copy 20import gettext 21import locale 22import logging 23import os 24import warnings 25 26from oslo_i18n import _locale 27from oslo_i18n import _translate 28 29# magic gettext number to separate context from message 30CONTEXT_SEPARATOR = "\x04" 31 32 33LOG = logging.getLogger(__name__) 34 35 36class Message(str): 37 """A Message object is a unicode object that can be translated. 38 39 Translation of Message is done explicitly using the translate() method. 40 For all non-translation intents and purposes, a Message is simply unicode, 41 and can be treated as such. 42 """ 43 44 def __new__(cls, msgid, msgtext=None, params=None, 45 domain='oslo', has_contextual_form=False, 46 has_plural_form=False, *args): 47 """Create a new Message object. 48 49 In order for translation to work gettext requires a message ID, this 50 msgid will be used as the base unicode text. It is also possible 51 for the msgid and the base unicode text to be different by passing 52 the msgtext parameter. 53 """ 54 # If the base msgtext is not given, we use the default translation 55 # of the msgid (which is in English) just in case the system locale is 56 # not English, so that the base text will be in that locale by default. 57 if not msgtext: 58 msgtext = Message._translate_msgid(msgid, domain) 59 # We want to initialize the parent unicode with the actual object that 60 # would have been plain unicode if 'Message' was not enabled. 61 msg = super(Message, cls).__new__(cls, msgtext) 62 msg.msgid = msgid 63 msg.domain = domain 64 msg.params = params 65 msg.has_contextual_form = has_contextual_form 66 msg.has_plural_form = has_plural_form 67 return msg 68 69 def translation(self, desired_locale=None): 70 """Translate this message to the desired locale. 71 72 :param desired_locale: The desired locale to translate the message to, 73 if no locale is provided the message will be 74 translated to the system's default locale. 75 76 :returns: the translated message in unicode 77 """ 78 translated_message = Message._translate_msgid(self.msgid, 79 self.domain, 80 desired_locale, 81 self.has_contextual_form, 82 self.has_plural_form) 83 84 if self.params is None: 85 # No need for more translation 86 return translated_message 87 88 # This Message object may have been formatted with one or more 89 # Message objects as substitution arguments, given either as a single 90 # argument, part of a tuple, or as one or more values in a dictionary. 91 # When translating this Message we need to translate those Messages too 92 translated_params = _translate.translate_args(self.params, 93 desired_locale) 94 95 return self._safe_translate(translated_message, translated_params) 96 97 @staticmethod 98 def _translate_msgid(msgid, domain, desired_locale=None, 99 has_contextual_form=False, has_plural_form=False): 100 if not desired_locale: 101 system_locale = locale.getdefaultlocale() 102 # If the system locale is not available to the runtime use English 103 if not system_locale or not system_locale[0]: 104 desired_locale = 'en_US' 105 else: 106 desired_locale = system_locale[0] 107 108 locale_dir = os.environ.get( 109 _locale.get_locale_dir_variable_name(domain) 110 ) 111 lang = gettext.translation(domain, 112 localedir=locale_dir, 113 languages=[desired_locale], 114 fallback=True) 115 116 if not has_contextual_form and not has_plural_form: 117 # This is the most common case, so check it first. 118 translator = lang.gettext 119 translated_message = translator(msgid) 120 121 elif has_contextual_form and has_plural_form: 122 # Reserved for contextual and plural translation function, 123 # which is not yet implemented. 124 raise ValueError("Unimplemented.") 125 126 elif has_contextual_form: 127 (msgctx, msgtxt) = msgid 128 translator = lang.gettext 129 130 msg_with_ctx = "%s%s%s" % (msgctx, CONTEXT_SEPARATOR, msgtxt) 131 translated_message = translator(msg_with_ctx) 132 133 if CONTEXT_SEPARATOR in translated_message: 134 # Translation not found, use the original text 135 translated_message = msgtxt 136 137 elif has_plural_form: 138 (msgsingle, msgplural, msgcount) = msgid 139 translator = lang.ngettext 140 translated_message = translator(msgsingle, msgplural, msgcount) 141 142 return translated_message 143 144 def _safe_translate(self, translated_message, translated_params): 145 """Trap translation errors and fall back to default translation. 146 147 :param translated_message: the requested translation 148 149 :param translated_params: the params to be inserted 150 151 :return: if parameter insertion is successful then it is the 152 translated_message with the translated_params inserted, if the 153 requested translation fails then it is the default translation 154 with the params 155 """ 156 157 try: 158 translated_message = translated_message % translated_params 159 except (KeyError, TypeError) as err: 160 # KeyError for parameters named in the translated_message 161 # but not found in translated_params and TypeError for 162 # type strings that do not match the type of the 163 # parameter. 164 # 165 # Log the error translating the message and use the 166 # original message string so the translator's bad message 167 # catalog doesn't break the caller. 168 # Do not translate this log message even if it is used as a 169 # warning message as a wrong translation of this message could 170 # cause infinite recursion 171 msg = (u'Failed to insert replacement values into translated ' 172 u'message %s (Original: %r): %s') 173 warnings.warn(msg % (translated_message, self.msgid, err)) 174 LOG.debug(msg, translated_message, self.msgid, err) 175 176 translated_message = self.msgid % translated_params 177 178 return translated_message 179 180 def __mod__(self, other): 181 # When we mod a Message we want the actual operation to be performed 182 # by the base class (i.e. unicode()), the only thing we do here is 183 # save the original msgid and the parameters in case of a translation 184 params = self._sanitize_mod_params(other) 185 unicode_mod = self._safe_translate(str(self), params) 186 modded = Message(self.msgid, 187 msgtext=unicode_mod, 188 params=params, 189 domain=self.domain) 190 return modded 191 192 def _sanitize_mod_params(self, other): 193 """Sanitize the object being modded with this Message. 194 195 - Add support for modding 'None' so translation supports it 196 - Trim the modded object, which can be a large dictionary, to only 197 those keys that would actually be used in a translation 198 - Snapshot the object being modded, in case the message is 199 translated, it will be used as it was when the Message was created 200 """ 201 if other is None: 202 params = (other,) 203 elif isinstance(other, dict): 204 # Merge the dictionaries 205 # Copy each item in case one does not support deep copy. 206 params = {} 207 if isinstance(self.params, dict): 208 params.update((key, self._copy_param(val)) 209 for key, val in self.params.items()) 210 params.update((key, self._copy_param(val)) 211 for key, val in other.items()) 212 else: 213 params = self._copy_param(other) 214 return params 215 216 def _copy_param(self, param): 217 try: 218 return copy.deepcopy(param) 219 except Exception: 220 # Fallback to casting to unicode this will handle the 221 # python code-like objects that can't be deep-copied 222 return str(param) 223 224 def __add__(self, other): 225 from oslo_i18n._i18n import _ 226 msg = _('Message objects do not support addition.') 227 raise TypeError(msg) 228 229 def __radd__(self, other): 230 return self.__add__(other) 231