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' 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." diff --git a/src/i18n.py b/src/i18n.py index 7f6f4d1d1..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 @@ -77,14 +77,29 @@ def getLocalePath(name, localeName, extension): i18nClasses = {} internationalizedCommands = {} +internationalizedFunctions = [] # No need to know there name def reloadLocals(): for pluginName in i18nClasses: i18nClasses[pluginName].loadLocale() for commandHash in internationalizedCommands: internationalizeDocstring(internationalizedCommands[commandHash]) + for function in internationalizedFunctions: + function.loadLocale() + -class PluginInternationalization: +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) + elif i18nSupybot == None: + i18nSupybot = _PluginInternationalization('supybot') + return i18nSupybot + +class _PluginInternationalization: """Internationalization managment for a plugin.""" def __init__(self, name='supybot'): self.name = name @@ -96,19 +111,28 @@ 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: localeName = 'en' self.currentLocaleName = localeName + + self._loadL10nCode() try: 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: @@ -154,61 +178,92 @@ 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): - return str.replace(string, '\\n', '\n') # Replace \\n by \n + 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 - - def _getL10nCode(self): - return getLocalePath('supybot', self.currentLocaleName, 'py') - - def getPluralizers(self, pluralize, depluralize): - # This should be used only by src/utils/str.py try: - execfile(self._getL10nCode()) - except IOError: + return self.translations[untranslated] + except KeyError: + return untranslated + + def _loadL10nCode(self): + if self.name != 'supybot': + return + try: + execfile(self._getL10nCodePath()) + except IOError: # File doesn't exist pass - return (pluralize, depluralize) + + 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. - def getOrdinal(self, ordinal): - # This should be used only by src/utils/str.py - try: - execfile(self._getL10nCode()) - except IOError: - pass - return ordinal + 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. - 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) + Should be used only by the internationalizedFunction class""" + if self.name != 'supybot': + return + 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: + 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) + +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 + 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): + """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__) diff --git a/src/utils/str.py b/src/utils/str.py index 717a0c9f7..d833a4c30 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'):