Skip to content

Commit

Permalink
Prevent gettext() from installing to global _ namespace (#821)
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc authored Aug 22, 2023
1 parent 31caff1 commit f82934a
Show file tree
Hide file tree
Showing 10 changed files with 707 additions and 492 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ sdist/
*.egg-info/
.installed.cfg
*.egg
.local

# Generated from Docker Instance
.bash_history
Expand Down
187 changes: 108 additions & 79 deletions apprise/AppriseLocale.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@
from os.path import abspath
from .logger import logger

# Define our translation domain
DOMAIN = 'apprise'
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))

# This gets toggled to True if we succeed
GETTEXT_LOADED = False
Expand All @@ -51,43 +48,13 @@
# Initialize gettext
import gettext

# install() creates a _() in our builtins
gettext.install(DOMAIN, localedir=LOCALE_DIR)

# Toggle our flag
GETTEXT_LOADED = True

except ImportError:
# gettext isn't available; no problem, just fall back to using
# the library features without multi-language support.
import builtins
builtins.__dict__['_'] = lambda x: x # pragma: no branch


class LazyTranslation:
"""
Doesn't translate anything until str() or unicode() references
are made.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text

super().__init__(*args, **kwargs)

def __str__(self):
return gettext.gettext(self.text)


# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)
# gettext isn't available; no problem; Use the library features without
# multi-language support.
pass


class AppriseLocale:
Expand All @@ -97,15 +64,24 @@ class AppriseLocale:
"""

# Define our translation domain
_domain = 'apprise'

# The path to our translations
_locale_dir = abspath(join(dirname(__file__), 'i18n'))

# Locale regular expression
_local_re = re.compile(
r'^\s*(?P<lang>[a-z]{2})([_:]((?P<country>[a-z]{2}))?'
r'(\.(?P<enc>[a-z0-9]+))?|.+)?', re.IGNORECASE)
r'^((?P<ansii>C)|(?P<lang>([a-z]{2}))([_:](?P<country>[a-z]{2}))?)'
r'(\.(?P<enc>[a-z0-9-]+))?$', re.IGNORECASE)

# Define our default encoding
_default_encoding = 'utf-8'

# Define our default language
# The function to assign `_` by default
_fn = 'gettext'

# The language we should fall back to if all else fails
_default_language = 'en'

def __init__(self, language=None):
Expand All @@ -123,25 +99,55 @@ def __init__(self, language=None):
# Get our language
self.lang = AppriseLocale.detect_language(language)

# Our mapping to our _fn
self.__fn_map = None

if GETTEXT_LOADED is False:
# We're done
return

if self.lang:
# Add language
self.add(self.lang)

def add(self, lang=None, set_default=True):
"""
Add a language to our list
"""
lang = lang if lang else self._default_language
if lang not in self._gtobjs:
# Load our gettext object and install our language
try:
self._gtobjs[self.lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
self._gtobjs[lang] = gettext.translation(
self._domain, localedir=self._locale_dir, languages=[lang],
fallback=False)

# The non-intrusive method of applying the gettext change to
# the global namespace only
self.__fn_map = getattr(self._gtobjs[lang], self._fn)

except FileNotFoundError:
# The translation directory does not exist
logger.debug(
'Could not load translation path: %s',
join(self._locale_dir, lang))

# Fallback (handle case where self.lang does not exist)
if self.lang not in self._gtobjs:
self._gtobjs[self.lang] = gettext
self.__fn_map = getattr(self._gtobjs[self.lang], self._fn)

return False

logger.trace('Loaded language %s', lang)

# Install our language
self._gtobjs[self.lang].install()
if set_default:
logger.debug('Language set to %s', lang)
self.lang = lang

except IOError:
# This occurs if we can't access/load our translations
pass
return True

@contextlib.contextmanager
def lang_at(self, lang):
def lang_at(self, lang, mapto=_fn):
"""
The syntax works as:
with at.lang_at('fr'):
Expand All @@ -151,45 +157,31 @@ def lang_at(self, lang):
"""

if GETTEXT_LOADED is False:
# yield
yield
# Do nothing
yield None

# we're done
return

# Tidy the language
lang = AppriseLocale.detect_language(lang, detect_fallback=False)

# Now attempt to load it
try:
if lang in self._gtobjs:
if lang != self.lang:
# Install our language only if we aren't using it
# already
self._gtobjs[lang].install()

else:
self._gtobjs[lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])

# Install our language
self._gtobjs[lang].install()

if lang not in self._gtobjs and not self.add(lang, set_default=False):
# Do Nothing
yield getattr(self._gtobjs[self.lang], mapto)
else:
# Yield
yield
yield getattr(self._gtobjs[lang], mapto)

except (IOError, KeyError):
# This occurs if we can't access/load our translations
# Yield reguardless
yield
return

finally:
# Fall back to our previous language
if lang != self.lang and lang in self._gtobjs:
# Install our language
self._gtobjs[self.lang].install()
@property
def gettext(self):
"""
Return the current language gettext() function
return
Useful for assigning to `_`
"""
return self._gtobjs[self.lang].gettext

@staticmethod
def detect_language(lang=None, detect_fallback=True):
Expand Down Expand Up @@ -227,12 +219,12 @@ def detect_language(lang=None, detect_fallback=True):
# Fallback to posix detection
pass

# Linux Handling
# Built in locale library check
try:
# Acquire our locale
lang = locale.getlocale()[0]

except TypeError as e:
except (ValueError, TypeError) as e:
# This occurs when an invalid locale was parsed from the
# environment variable. While we still return None in this
# case, we want to better notify the end user of this. Users
Expand All @@ -249,13 +241,50 @@ def __getstate__(self):
Pickle Support dumps()
"""
state = self.__dict__.copy()

# Remove the unpicklable entries.
del state['_gtobjs']
del state['_AppriseLocale__fn_map']
return state

def __setstate__(self, state):
"""
Pickle Support loads()
"""
self.__dict__.update(state)
# Our mapping to our _fn
self.__fn_map = None
self._gtobjs = {}
self.add(state['lang'], set_default=True)


#
# Prepare our default LOCALE Singleton
#
LOCALE = AppriseLocale()


class LazyTranslation:
"""
Doesn't translate anything until str() or unicode() references
are made.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text

super().__init__(*args, **kwargs)

def __str__(self):
return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text


# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)
Loading

0 comments on commit f82934a

Please sign in to comment.