From 32cabd6d275f7bba55e7d85b164fa507e16a677f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Oct 2010 13:05:52 +0200 Subject: [PATCH 1/4] Status: fix two translations --- plugins/Status/locale/fr.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Status/locale/fr.po b/plugins/Status/locale/fr.po index 4d1d797cc..6d79d98b0 100644 --- a/plugins/Status/locale/fr.po +++ b/plugins/Status/locale/fr.po @@ -67,7 +67,7 @@ msgstr "" #: plugin.py:95 msgid "I have spawned %n; %n %b still currently active: %L." -msgstr "J'ai lancé %n ; %n %b sont encore actuellement actifs : %L." +msgstr "J'ai lancé %n ; %n %b encore actuellement en vie : %L." #: plugin.py:103 msgid "" @@ -105,7 +105,7 @@ msgstr "Mes enfants ont pris %.2f secondes du temps utilisateur et %.2f secondes #: plugin.py:138 msgid "I have taken %.2f seconds of user time and %.2f seconds of system time, for a total of %.2f seconds of CPU time. %s" -msgstr "J'ai pris %.2f secondes du temps utilisateur et %.2f secondes du temps système, pour un total de %.2f secondes de temps CPU." +msgstr "J'ai pris %.2f secondes du temps utilisateur et %.2f secondes du temps système, pour un total de %.2f secondes de temps CPU. %s" #: plugin.py:160 msgid "Unable to run ps command." From 3ea8115095ea429b3a3ed7106dbb7e6d903ebfe7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Oct 2010 21:10:49 +0200 Subject: [PATCH 2/4] Internationalize src/utils/str.py and modify src/i18n.py to fit this changes --- src/i18n.py | 88 ++++++++++++++++++++++++++++++++++-------------- src/utils/str.py | 13 ++++--- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/i18n.py b/src/i18n.py index 7f6f4d1d1..274df8f70 100644 --- a/src/i18n.py +++ b/src/i18n.py @@ -75,16 +75,29 @@ def getLocalePath(name, localeName, extension): directory = ansi.__file__[0:-len('ansi.pyc')] + 'locale' return '%s/%s.%s' % (directory, localeName, extension) +i18nSupybot = None i18nClasses = {} internationalizedCommands = {} +internationalizedFunctions = [] def reloadLocals(): for pluginName in i18nClasses: i18nClasses[pluginName].loadLocale() for commandHash in internationalizedCommands: internationalizeDocstring(internationalizedCommands[commandHash]) + for function in internationalizedFunctions: + function.loadLocale() + -class PluginInternationalization: +def PluginInternationalization(name='supybot'): + global i18nSupybot + if name != 'supybot': + return _PluginInternationalization(name) + elif i18nSupybot == None: + i18nSupybot = _PluginInternationalization('supybot') + return i18nSupybot + +class _PluginInternationalization: """Internationalization managment for a plugin.""" def __init__(self, name='supybot'): self.name = name @@ -101,6 +114,8 @@ class PluginInternationalization: elif localeName is None: localeName = 'en' self.currentLocaleName = localeName + + self._loadL10nCode() try: translationFile = open(getLocalePath(self.name, localeName, 'po'), @@ -158,7 +173,7 @@ class PluginInternationalization: self._parse(translated)}) def _parse(self, string): - return str.replace(string, '\\n', '\n') # Replace \\n by \n + return str.replace(string, '\\n', '\n') def __call__(self, untranslated, *args): if not 'conf' in globals(): @@ -181,32 +196,55 @@ class PluginInternationalization: except KeyError: return untranslated % args - def _getL10nCode(self): - return getLocalePath('supybot', self.currentLocaleName, 'py') - - def getPluralizers(self, pluralize, depluralize): - # This should be used only by src/utils/str.py + def _loadL10nCode(self): + if self.name != 'supybot': + return try: - execfile(self._getL10nCode()) - except IOError: + execfile(self._getL10nCodePath()) + except IOError: # File doesn't exist pass - return (pluralize, depluralize) + + functions = locals() + functions.pop('self') + self._l10nFunctions = functions + + def _getL10nCodePath(self): + if self.name != 'supybot': + return + return getLocalePath('supybot', self.currentLocaleName, 'py') + + def localizeFunction(self, name): + if self.name != 'supybot': + return + if hasattr(self, '_l10nFunctions') and self._l10nFunctions.has_key(name): + return self._l10nFunctions[name] + + def internationalizeFunction(self, name): + if self.name != 'supybot': + return + class FunctionInternationalizer: + def __init__(self, parent, name): + self._parent = parent + self._name = name + def __call__(self, obj): + obj = internationalizedFunction(self._parent, self._name, obj) + obj.loadLocale() + return obj + return FunctionInternationalizer(self, name) - def getOrdinal(self, ordinal): - # This should be used only by src/utils/str.py - try: - execfile(self._getL10nCode()) - except IOError: - pass - return ordinal - - def getBeAndHas(self, be, has): - # This should be used only by src/utils/str.py - try: - execfile(self._getL10nCode()) - except IOError: - pass - return (be, has) +class internationalizedFunction: + def __init__(self, internationalizer, name, function): + self._internationalizer = internationalizer + self._name = name + self.__call__ = function + self._origin = function + internationalizedFunctions.append(self) + def loadLocale(self): + self.__call__ = self._internationalizer.localizeFunction(self._name) + if self.__call__ == None: + self.restore() + def restore(self): + self.__call__ = self._origin def internationalizeDocstring(obj): if sys.modules[obj.__module__].__dict__.has_key('_'): diff --git a/src/utils/str.py b/src/utils/str.py index 91f71f3e6..32d24e418 100644 --- a/src/utils/str.py +++ b/src/utils/str.py @@ -43,7 +43,7 @@ from iter import all, any from structures import TwoWayDictionary from supybot.i18n import PluginInternationalization -_ = PluginInternationalization() +internationalizeFunction=PluginInternationalization().internationalizeFunction curry = new.instancemethod chars = string.maketrans('', '') @@ -256,6 +256,7 @@ def matchCase(s1, s2): L[i] = L[i].upper() return ''.join(L) +@internationalizeFunction('pluralize') def pluralize(s): """Returns the plural of s. Put any exceptions to the general English rule of appending 's' in the plurals dictionary. @@ -278,6 +279,7 @@ def pluralize(s): else: return matchCase(s, s+'s') +@internationalizeFunction('depluralize') def depluralize(s): """Returns the singular of s.""" _depluralizeRegex = re.compile('[%s]ies' % consonants) @@ -294,8 +296,6 @@ def depluralize(s): else: return s # Don't know what to do. -pluralize, depluralize = _.getPluralizers(pluralize, depluralize) - def nItems(n, item, between=None): """Works like this: @@ -332,6 +332,7 @@ def nItems(n, item, between=None): else: return format('%s %s %s', n, between, item) +@internationalizeFunction('ordinal') def ordinal(i): """Returns i + the ordinal indicator for the number. @@ -350,8 +351,7 @@ def ordinal(i): ord = 'rd' return '%s%s' % (i, ord) -ordinal = _.getOrdinal(ordinal) - +@internationalizeFunction('be') def be(i): """Returns the form of the verb 'to be' based on the number i.""" if i == 1: @@ -359,6 +359,7 @@ def be(i): else: return 'are' +@internationalizeFunction('has') def has(i): """Returns the form of the verb 'to have' based on the number i.""" if i == 1: @@ -366,8 +367,6 @@ def has(i): else: return 'have' -be, has = _.getBeAndHas(be, has) - def toBool(s): s = s.strip().lower() if s in ('true', 'on', 'enable', 'enabled', '1'): From 7dc01670eb9a82b6ba8a1b5a9b1bfade5ff94905 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Oct 2010 21:12:44 +0200 Subject: [PATCH 3/4] Localize the utility functions into French --- locale/fr.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 locale/fr.py diff --git a/locale/fr.py b/locale/fr.py new file mode 100644 index 000000000..23bb731d2 --- /dev/null +++ b/locale/fr.py @@ -0,0 +1,100 @@ +# -*- encoding: utf8 -*- +### +# Copyright (c) 2010, Valentin Lorentz +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Supybot utility functions localization in French. +""" + +def pluralize(s): + """Returns the plural of s. + """ + lowered = s.lower() + if lowered.endswith('ou') and \ + lowered in ['bijou', 'caillou', 'chou', 'genou', 'hibou', 'joujou', + 'pou']: + return s + 'x' + elif lowered.endwith('al') and \ + lowered not in ['bal', 'carnaval', 'chacal', 'festival', 'récital', + 'régal', 'cal', 'étal', 'aval', 'caracal', 'val', 'choral', + 'corral', 'galgal', 'gayal']: + return s[0:-2] + 'aux' + elif lowered.endswith('ail') and \ + lowered not in ['bail', 'corail', 'émail', 'soupirail', 'travail', + 'ventail', 'vitrail', 'aspirail', 'fermail']: + return s[0:-3] + 'aux' + elif lowered.endswith('eau'): + return s + 'x' + elif lowered == 'pare-feu': + return s + elif lowered.endwith('eu') and \ + lowered not in ['bleu', 'pneu', 'émeu', 'enfeu']: + # Note: when 'lieu' is a fish, it has a 's' ; else, it has a 'x' + return s + 'x' + else: + return s + 's' + +def depluralize(s): + """Returns the singular of s.""" + lowered = s.lower() + if lowered.endswith('aux') and \ + lowered in ['baux', 'coraux', 'émaux', 'soupiraux', 'travaux', + 'ventaux', 'vitraux', 'aspiraux', 'fermaux']: + return s[0:-3] + 'ail' + elif lowered.endswith('aux'): + return s[0:-3] + 'al' + else: + return s[0:-1] + +def ordinal(i): + """Returns i + the ordinal indicator for the number. + + Example: ordinal(3) => '3ème' + """ + i = int(i) + if i == 1: + return '1er' + else: + return '%sème' % i + +def be(i): + """Returns the form of the verb 'être' based on the number i.""" + # Note: this function is used only for the third person + if i == 1: + return 'est' + else: + return 'sont' + +def has(i): + """Returns the form of the verb 'avoir' based on the number i.""" + # Note: this function is used only for the third person + if i == 1: + return 'a' + else: + return 'ont' From 4a516c5f46e67261b9d60927940300ff14c74952 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 30 Oct 2010 21:41:25 +0200 Subject: [PATCH 4/4] Makes the code of i18n.py cleaner More comments; docstrings; remove the % auto-handling (useless). --- src/i18n.py | 63 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/i18n.py b/src/i18n.py index 274df8f70..2d0c6d45e 100644 --- a/src/i18n.py +++ b/src/i18n.py @@ -31,7 +31,7 @@ Supybot internationalisation and localisation managment. """ -__all__ = ['PluginInternationalization'] +__all__ = ['PluginInternationalization', 'internationalizeDocstring'] import re import sys @@ -75,10 +75,9 @@ def getLocalePath(name, localeName, extension): directory = ansi.__file__[0:-len('ansi.pyc')] + 'locale' return '%s/%s.%s' % (directory, localeName, extension) -i18nSupybot = None i18nClasses = {} internationalizedCommands = {} -internationalizedFunctions = [] +internationalizedFunctions = [] # No need to know there name def reloadLocals(): for pluginName in i18nClasses: @@ -89,7 +88,10 @@ def reloadLocals(): function.loadLocale() +i18nSupybot = None def PluginInternationalization(name='supybot'): + # This is a proxy, that prevent Supybot against having more than one + # internationalizer global i18nSupybot if name != 'supybot': return _PluginInternationalization(name) @@ -109,6 +111,7 @@ class _PluginInternationalization: self.loadLocale() def loadLocale(self, localeName=None): + """(Re)loads the locale used by this class.""" if localeName is None and 'conf' in globals(): localeName = conf.supybot.language() elif localeName is None: @@ -121,9 +124,15 @@ class _PluginInternationalization: translationFile = open(getLocalePath(self.name, localeName, 'po'), 'ru') # ru is the mode, not the beginning # of 'russian' ;) + self._parse(translationFile) except IOError: # The translation is unavailable self.translations = {} return + + def _parse(self, translationFile): + """A .po files parser. + + Give it a file object.""" step = WAITING_FOR_MSGID self.translations = {} for line in translationFile: @@ -169,32 +178,22 @@ class _PluginInternationalization: self._translate(untranslated, translated) def _translate(self, untranslated, translated): - self.translations.update({self._parse(untranslated): - self._parse(translated)}) + self.translations.update({self._unescape(untranslated): + self._unescape(translated)}) - def _parse(self, string): + def _unescape(self, string): return str.replace(string, '\\n', '\n') - def __call__(self, untranslated, *args): + def __call__(self, untranslated): if not 'conf' in globals(): - if len(args) == 0: - return untranslated - else: - translation = self(untranslated) - return translation % args + return untranslated if self.currentLocaleName != conf.supybot.language(): # If the locale has been changed reloadLocals() - if len(args) == 0: - try: - return self.translations[untranslated] - except KeyError: - return untranslated - else: - try: - return self.translations[untranslated] % args - except KeyError: - return untranslated % args + try: + return self.translations[untranslated] + except KeyError: + return untranslated def _loadL10nCode(self): if self.name != 'supybot': @@ -207,19 +206,31 @@ class _PluginInternationalization: functions = locals() functions.pop('self') self._l10nFunctions = functions + # Remove old functions and come back to the native language def _getL10nCodePath(self): + """Returns the path to the code localization file. + + It contains functions that needs to by fully (code + strings) + localized""" if self.name != 'supybot': return return getLocalePath('supybot', self.currentLocaleName, 'py') def localizeFunction(self, name): + """Returns the localized version of the function. + + Should be used only by the internationalizedFunction class""" if self.name != 'supybot': return - if hasattr(self, '_l10nFunctions') and self._l10nFunctions.has_key(name): + if hasattr(self, '_l10nFunctions') and \ + self._l10nFunctions.has_key(name): return self._l10nFunctions[name] def internationalizeFunction(self, name): + """Decorates functions and internationalize their code. + + Only useful for Supybot core functions""" if self.name != 'supybot': return class FunctionInternationalizer: @@ -233,6 +244,9 @@ class _PluginInternationalization: return FunctionInternationalizer(self, name) class internationalizedFunction: + """Proxy for functions that need to be fully localized. + + The localization code is in locale/LOCALE.py""" def __init__(self, internationalizer, name, function): self._internationalizer = internationalizer self._name = name @@ -247,6 +261,9 @@ class internationalizedFunction: self.__call__ = self._origin def internationalizeDocstring(obj): + """Decorates functions and internationalize their docstring. + + Only useful for commands (commands' docstring is displayed on IRC)""" if sys.modules[obj.__module__].__dict__.has_key('_'): internationalizedCommands.update({hash(obj): obj}) obj.__doc__=sys.modules[obj.__module__]._.__call__(obj.__doc__)