utils: Rewrite i18n initialization

The previous implementation was messy and needlessly complicated

This simplifies the logic and removes hackiness by making utils/str.py
handle internationalization logic itself, instead of bending over
backwards to load logic from the parent package at import time.
This commit is contained in:
Valentin Lorentz 2022-06-16 23:43:34 +02:00 committed by Val Lorentz
parent 4a620bf7f0
commit 93370b6f0e
5 changed files with 52 additions and 52 deletions

View File

@ -33,10 +33,7 @@ from . import dynamicScope
from . import i18n from . import i18n
builtins = (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)
builtins['supybotInternationalization'] = i18n.PluginInternationalization()
from . import utils from . import utils
del builtins['supybotInternationalization']
(__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['format'] = utils.str.format (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)['format'] = utils.str.format

View File

@ -113,7 +113,6 @@ def getLocalePath(name, localeName, extension):
i18nClasses = weakref.WeakValueDictionary() i18nClasses = weakref.WeakValueDictionary()
internationalizedCommands = weakref.WeakValueDictionary() internationalizedCommands = weakref.WeakValueDictionary()
internationalizedFunctions = [] # No need to know their name
def reloadLocalesIfRequired(): def reloadLocalesIfRequired():
global currentLocale global currentLocale
@ -124,12 +123,13 @@ def reloadLocalesIfRequired():
reloadLocales() reloadLocales()
def reloadLocales(): def reloadLocales():
import supybot.utils as utils
for pluginClass in i18nClasses.values(): for pluginClass in i18nClasses.values():
pluginClass.loadLocale() pluginClass.loadLocale()
for command in list(internationalizedCommands.values()): for command in list(internationalizedCommands.values()):
internationalizeDocstring(command) internationalizeDocstring(command)
for function in internationalizedFunctions: utils.str._relocalizeFunctions(PluginInternationalization())
function.loadLocale()
def normalize(string, removeNewline=False): def normalize(string, removeNewline=False):
import supybot.utils as utils import supybot.utils as utils
@ -201,7 +201,6 @@ def parse(translationFile):
return translations return translations
i18nSupybot = None
def PluginInternationalization(name='supybot'): def PluginInternationalization(name='supybot'):
# This is a proxy that prevents having several objects for the same plugin # This is a proxy that prevents having several objects for the same plugin
if name in i18nClasses: if name in i18nClasses:
@ -326,40 +325,6 @@ class _PluginInternationalization:
name in self._l10nFunctions: name in self._l10nFunctions:
return self._l10nFunctions[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 locales/LOCALE.py"""
def __init__(self, internationalizer, name, function):
self._internationalizer = internationalizer
self._name = name
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 __call__(self, *args, **kwargs):
return self._origin(*args, **kwargs)
try: try:
class InternationalizedString(str): class InternationalizedString(str):
@ -375,6 +340,7 @@ except TypeError:
know if a string is already localized""" know if a string is already localized"""
pass pass
def internationalizeDocstring(obj): def internationalizeDocstring(obj):
"""Decorates functions and internationalize their docstring. """Decorates functions and internationalize their docstring.
@ -391,3 +357,13 @@ def internationalizeDocstring(obj):
# attribute '__doc__' of 'type' objects is not writable # attribute '__doc__' of 'type' objects is not writable
pass pass
return obj return obj
def _install():
from . import utils
_ = PluginInternationalization()
utils.gen._ = _
utils.str._ = _
utils.str._relocalizeFunctions(_)
_install()

View File

@ -50,6 +50,7 @@ csv.split = split
builtins = (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__) builtins = (__builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__)
# We use this often enough that we're going to stick it in builtins. # We use this often enough that we're going to stick it in builtins.
def force(x): def force(x):
if callable(x): if callable(x):
@ -58,8 +59,6 @@ def force(x):
return x return x
builtins['force'] = force builtins['force'] = force
internationalization = builtins.get('supybotInternationalization', None)
# These imports need to happen below the block above, so things get put into # These imports need to happen below the block above, so things get put into
# __builtins__ appropriately. # __builtins__ appropriately.
from .gen import * from .gen import *

View File

@ -45,7 +45,9 @@ from . import crypt
from .str import format from .str import format
from .file import mktemp from .file import mktemp
from . import minisix from . import minisix
from . import internationalization as _
# will be replaced by supybot.i18n.install()
_ = lambda x: x
def warn_non_constant_time(f): def warn_non_constant_time(f):
@functools.wraps(f) @functools.wraps(f)

View File

@ -38,20 +38,46 @@ import sys
import time import time
import string import string
import textwrap import textwrap
import functools
from . import minisix from . import minisix
from .iter import any from .iter import any
from .structures import TwoWayDictionary from .structures import TwoWayDictionary
from . import internationalization as _
internationalizeFunction = _.internationalizeFunction
try: try:
from charade.universaldetector import UniversalDetector from charade.universaldetector import UniversalDetector
charadeLoaded = True charadeLoaded = True
except ImportError: except ImportError:
charadeLoaded = False charadeLoaded = False
# will be replaced by supybot.i18n.install()
_ = lambda x: x
# used by supybot.i18n.reloadLocales() to (re)load the localized function of
# these functions
_localizedFunctions = {}
_defaultFunctions = {}
def internationalizeFunction(f):
f_name = f.__name__
_localizedFunctions[f_name] = f
_defaultFunctions[f_name] = f
@functools.wraps(f)
def newf(*args, **kwargs):
f = _localizedFunctions[f_name]
assert f is not None, "_localizedFunctions[%s] is None" % f_name
return f(*args, **kwargs)
return newf
def _relocalizeFunctions(localizer):
for f_name in list(_localizedFunctions):
f = localizer.localizeFunction(f_name) or _defaultFunctions[f_name]
_localizedFunctions[f_name] = f
if minisix.PY3: if minisix.PY3:
def decode_raw_line(line): def decode_raw_line(line):
#first, try to decode using utf-8 #first, try to decode using utf-8
@ -390,7 +416,7 @@ def matchCase(s1, s2):
L[i] = L[i].upper() L[i] = L[i].upper()
return ''.join(L) return ''.join(L)
@internationalizeFunction('pluralize') @internationalizeFunction
def pluralize(s): def pluralize(s):
"""Returns the plural of s. Put any exceptions to the general English """Returns the plural of s. Put any exceptions to the general English
rule of appending 's' in the plurals dictionary. rule of appending 's' in the plurals dictionary.
@ -413,7 +439,7 @@ def pluralize(s):
else: else:
return matchCase(s, s+'s') return matchCase(s, s+'s')
@internationalizeFunction('depluralize') @internationalizeFunction
def depluralize(s): def depluralize(s):
"""Returns the singular of s.""" """Returns the singular of s."""
consonants = 'bcdfghjklmnpqrstvwxz' consonants = 'bcdfghjklmnpqrstvwxz'
@ -467,7 +493,7 @@ def nItems(n, item, between=None):
else: else:
return format('%s %s %s', n, between, item) return format('%s %s %s', n, between, item)
@internationalizeFunction('ordinal') @internationalizeFunction
def ordinal(i): def ordinal(i):
"""Returns i + the ordinal indicator for the number. """Returns i + the ordinal indicator for the number.
@ -486,7 +512,7 @@ def ordinal(i):
ord = 'rd' ord = 'rd'
return '%s%s' % (i, ord) return '%s%s' % (i, ord)
@internationalizeFunction('be') @internationalizeFunction
def be(i): def be(i):
"""Returns the form of the verb 'to be' based on the number i.""" """Returns the form of the verb 'to be' based on the number i."""
if i == 1: if i == 1:
@ -494,7 +520,7 @@ def be(i):
else: else:
return 'are' return 'are'
@internationalizeFunction('has') @internationalizeFunction
def has(i): def has(i):
"""Returns the form of the verb 'to have' based on the number i.""" """Returns the form of the verb 'to have' based on the number i."""
if i == 1: if i == 1: