1### 2# Copyright (c) 2010, Valentin Lorentz 3# All rights reserved. 4# 5# Redistribution and use in source and binary forms, with or without 6# modification, are permitted provided that the following conditions are met: 7# 8# * Redistributions of source code must retain the above copyright notice, 9# this list of conditions, and the following disclaimer. 10# * Redistributions in binary form must reproduce the above copyright notice, 11# this list of conditions, and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# * Neither the name of the author of this software nor the name of 14# contributors to this software may be used to endorse or promote products 15# derived from this software without specific prior written consent. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28### 29 30""" 31Supybot internationalisation and localisation managment. 32""" 33 34__all__ = ['PluginInternationalization', 'internationalizeDocstring'] 35 36import os 37import sys 38import weakref 39conf = None 40# Don't import conf here ; because conf needs this module 41 42WAITING_FOR_MSGID = 1 43IN_MSGID = 2 44WAITING_FOR_MSGSTR = 3 45IN_MSGSTR = 4 46 47MSGID = 'msgid "' 48MSGSTR = 'msgstr "' 49 50currentLocale = 'en' 51 52class PluginNotFound(Exception): 53 pass 54 55def getLocaleFromRegistryFilename(filename): 56 """Called by the 'supybot' script. Gets the locale name before conf is 57 loaded.""" 58 global currentLocale 59 with open(filename, 'r') as fd: 60 for line in fd: 61 if line.startswith('supybot.language: '): 62 currentLocale = line[len('supybot.language: '):] 63 64def import_conf(): 65 """Imports the conf into this module""" 66 global conf 67 conf = __import__('supybot.conf').conf 68 conf.registerGlobalValue(conf.supybot, 'language', 69 conf.registry.String(currentLocale, """Determines the bot's default 70 language if translations exist. Currently supported are 'de', 'en', 71 'es', 'fi', 'fr' and 'it'.""")) 72 conf.supybot.language.addCallback(reloadLocalesIfRequired) 73 74def getPluginDir(plugin_name): 75 """Gets the directory of the given plugin""" 76 filename = None 77 try: 78 filename = sys.modules[plugin_name].__file__ 79 except KeyError: # It sometimes happens with Owner 80 pass 81 if filename == None: 82 try: 83 filename = sys.modules['supybot.plugins.' + plugin_name].__file__ 84 except: # In the case where the plugin is not loaded by Supybot 85 try: 86 filename = sys.modules['plugin'].__file__ 87 except: 88 filename = sys.modules['__main__'].__file__ 89 if filename.endswith(".pyc"): 90 filename = filename[0:-1] 91 92 allowed_files = ['__init__.py', 'config.py', 'plugin.py', 'test.py'] 93 for allowed_file in allowed_files: 94 if filename.endswith(allowed_file): 95 return filename[0:-len(allowed_file)] 96 raise PluginNotFound() 97 98def getLocalePath(name, localeName, extension): 99 """Gets the path of the locale file of the given plugin ('supybot' stands 100 for the core).""" 101 if name != 'supybot': 102 base = getPluginDir(name) 103 else: 104 from . import ansi # Any Supybot plugin could fit 105 base = ansi.__file__[0:-len('ansi.pyc')] 106 directory = os.path.join(base, 'locales') 107 return '%s/%s.%s' % (directory, localeName, extension) 108 109i18nClasses = weakref.WeakValueDictionary() 110internationalizedCommands = weakref.WeakValueDictionary() 111internationalizedFunctions = [] # No need to know their name 112 113def reloadLocalesIfRequired(): 114 global currentLocale 115 if conf is None: 116 return 117 if currentLocale != conf.supybot.language(): 118 currentLocale = conf.supybot.language() 119 reloadLocales() 120 121def reloadLocales(): 122 for pluginClass in i18nClasses.values(): 123 pluginClass.loadLocale() 124 for command in list(internationalizedCommands.values()): 125 internationalizeDocstring(command) 126 for function in internationalizedFunctions: 127 function.loadLocale() 128 129def normalize(string, removeNewline=False): 130 import supybot.utils as utils 131 string = string.replace('\\n\\n', '\n\n') 132 string = string.replace('\\n', ' ') 133 string = string.replace('\\"', '"') 134 string = string.replace("\'", "'") 135 string = utils.str.normalizeWhitespace(string, removeNewline) 136 string = string.strip('\n') 137 string = string.strip('\t') 138 return string 139 140 141def parse(translationFile): 142 step = WAITING_FOR_MSGID 143 translations = set() 144 for line in translationFile: 145 line = line[0:-1] # Remove the ending \n 146 line = line 147 148 if line.startswith(MSGID): 149 # Don't check if step is WAITING_FOR_MSGID 150 untranslated = '' 151 translated = '' 152 data = line[len(MSGID):-1] 153 if len(data) == 0: # Multiline mode 154 step = IN_MSGID 155 else: 156 untranslated += data 157 step = WAITING_FOR_MSGSTR 158 159 160 elif step is IN_MSGID and line.startswith('"') and \ 161 line.endswith('"'): 162 untranslated += line[1:-1] 163 elif step is IN_MSGID and untranslated == '': # Empty MSGID 164 step = WAITING_FOR_MSGID 165 elif step is IN_MSGID: # the MSGID is finished 166 step = WAITING_FOR_MSGSTR 167 168 169 if step is WAITING_FOR_MSGSTR and line.startswith(MSGSTR): 170 data = line[len(MSGSTR):-1] 171 if len(data) == 0: # Multiline mode 172 step = IN_MSGSTR 173 else: 174 translations |= set([(untranslated, data)]) 175 step = WAITING_FOR_MSGID 176 177 178 elif step is IN_MSGSTR and line.startswith('"') and \ 179 line.endswith('"'): 180 translated += line[1:-1] 181 elif step is IN_MSGSTR: # the MSGSTR is finished 182 step = WAITING_FOR_MSGID 183 if translated == '': 184 translated = untranslated 185 translations |= set([(untranslated, translated)]) 186 if step is IN_MSGSTR: 187 if translated == '': 188 translated = untranslated 189 translations |= set([(untranslated, translated)]) 190 return translations 191 192 193i18nSupybot = None 194def PluginInternationalization(name='supybot'): 195 # This is a proxy that prevents having several objects for the same plugin 196 if name in i18nClasses: 197 return i18nClasses[name] 198 else: 199 return _PluginInternationalization(name) 200 201class _PluginInternationalization: 202 """Internationalization managment for a plugin.""" 203 def __init__(self, name='supybot'): 204 self.name = name 205 self.translations = {} 206 self.currentLocaleName = None 207 i18nClasses.update({name: self}) 208 self.loadLocale() 209 210 def loadLocale(self, localeName=None): 211 """(Re)loads the locale used by this class.""" 212 self.translations = {} 213 if localeName is None: 214 localeName = currentLocale 215 self.currentLocaleName = localeName 216 217 self._loadL10nCode() 218 219 try: 220 try: 221 translationFile = open(getLocalePath(self.name, 222 localeName, 'po'), 'ru') 223 except ValueError: # We are using Windows 224 translationFile = open(getLocalePath(self.name, 225 localeName, 'po'), 'r') 226 self._parse(translationFile) 227 except (IOError, PluginNotFound): # The translation is unavailable 228 pass 229 finally: 230 if 'translationFile' in locals(): 231 translationFile.close() 232 233 def _parse(self, translationFile): 234 """A .po files parser. 235 236 Give it a file object.""" 237 self.translations = {} 238 for translation in parse(translationFile): 239 self._addToDatabase(*translation) 240 241 def _addToDatabase(self, untranslated, translated): 242 untranslated = normalize(untranslated, True) 243 translated = normalize(translated) 244 if translated: 245 self.translations.update({untranslated: translated}) 246 247 def __call__(self, untranslated): 248 """Main function. 249 250 This is the function which is called when a plugin runs _()""" 251 normalizedUntranslated = normalize(untranslated, True) 252 try: 253 string = self._translate(normalizedUntranslated) 254 return self._addTracker(string, untranslated) 255 except KeyError: 256 pass 257 if untranslated.__class__ is InternationalizedString: 258 return untranslated._original 259 else: 260 return untranslated 261 262 def _translate(self, string): 263 """Translate the string. 264 265 C the string internationalizer if any; else, use the local database""" 266 if string.__class__ == InternationalizedString: 267 return string._internationalizer(string.untranslated) 268 else: 269 return self.translations[string] 270 271 def _addTracker(self, string, untranslated): 272 """Add a kind of 'tracker' on the string, in order to keep the 273 untranslated string (used when changing the locale)""" 274 if string.__class__ == InternationalizedString: 275 return string 276 else: 277 string = InternationalizedString(string) 278 string._original = untranslated 279 string._internationalizer = self 280 return string 281 282 def _loadL10nCode(self): 283 """Open the file containing the code specific to this locale, and 284 load its functions.""" 285 if self.name != 'supybot': 286 return 287 path = self._getL10nCodePath() 288 try: 289 with open(path) as fd: 290 exec(compile(fd.read(), path, 'exec')) 291 except IOError: # File doesn't exist 292 pass 293 294 functions = locals() 295 functions.pop('self') 296 self._l10nFunctions = functions 297 # Remove old functions and come back to the native language 298 299 def _getL10nCodePath(self): 300 """Returns the path to the code localization file. 301 302 It contains functions that needs to by fully (code + strings) 303 localized""" 304 if self.name != 'supybot': 305 return 306 return getLocalePath('supybot', self.currentLocaleName, 'py') 307 308 def localizeFunction(self, name): 309 """Returns the localized version of the function. 310 311 Should be used only by the InternationalizedFunction class""" 312 if self.name != 'supybot': 313 return 314 if hasattr(self, '_l10nFunctions') and \ 315 name in self._l10nFunctions: 316 return self._l10nFunctions[name] 317 318 def internationalizeFunction(self, name): 319 """Decorates functions and internationalize their code. 320 321 Only useful for Supybot core functions""" 322 if self.name != 'supybot': 323 return 324 class FunctionInternationalizer: 325 def __init__(self, parent, name): 326 self._parent = parent 327 self._name = name 328 def __call__(self, obj): 329 obj = InternationalizedFunction(self._parent, self._name, obj) 330 obj.loadLocale() 331 return obj 332 return FunctionInternationalizer(self, name) 333 334class InternationalizedFunction: 335 """Proxy for functions that need to be fully localized. 336 337 The localization code is in locales/LOCALE.py""" 338 def __init__(self, internationalizer, name, function): 339 self._internationalizer = internationalizer 340 self._name = name 341 self._origin = function 342 internationalizedFunctions.append(self) 343 def loadLocale(self): 344 self.__call__ = self._internationalizer.localizeFunction(self._name) 345 if self.__call__ == None: 346 self.restore() 347 def restore(self): 348 self.__call__ = self._origin 349 350 def __call__(self, *args, **kwargs): 351 return self._origin(*args, **kwargs) 352 353try: 354 class InternationalizedString(str): 355 """Simple subclass to str, that allow to add attributes. Also used to 356 know if a string is already localized""" 357 __slots__ = ('_original', '_internationalizer') 358except TypeError: 359 # Fallback for CPython 2.x: 360 # TypeError: Error when calling the metaclass bases 361 # nonempty __slots__ not supported for subtype of 'str' 362 class InternationalizedString(str): 363 """Simple subclass to str, that allow to add attributes. Also used to 364 know if a string is already localized""" 365 pass 366 367def internationalizeDocstring(obj): 368 """Decorates functions and internationalize their docstring. 369 370 Only useful for commands (commands' docstring is displayed on IRC)""" 371 if obj.__doc__ == None: 372 return obj 373 plugin_module = sys.modules[obj.__module__] 374 if '_' in plugin_module.__dict__: 375 internationalizedCommands.update({hash(obj): obj}) 376 try: 377 obj.__doc__ = plugin_module._.__call__(obj.__doc__) 378 # We use _.__call__() instead of _() because of a pygettext warning. 379 except AttributeError: 380 # attribute '__doc__' of 'type' objects is not writable 381 pass 382 return obj 383