Skip to content

Commit

Permalink
init: Lazy-initialize translations, fix order dependency (#1838)
Browse files Browse the repository at this point in the history
In the original code, if the translation domains (GUI, lib, modules) are
ordered differently, the GUI may not be translated. The Python gettext
documentation is silent on using install with multiple domains, but separate
install calls are used to switch between different languages, so even if
multiple install calls ever resulted in all domains being used, it is not
guaranteed. The new code uses translation object directly and adds multiple
domains using fallbacks. Now GUI translations are loaded regardless of
the order of domains in the calls.

The original code requires the GISBASE variable to be set before first import
is made which makes it difficult to bootstrap. The new code delays the
initialization of translations until the first call to the translation
function removing the need to set GISBASE before the import.
A null translation as used as an ultimate fallback trying to avoid translation issues
being mixed with issues with a session/runtime setup.

The code now contains a detailed information about the current translation
approach to clarify some of the pitfalls of the current approach, specifically
the consequences of adding the translation function named underscore to
the global namespace.
  • Loading branch information
wenzeslaus committed Nov 16, 2021
1 parent 0e43bf3 commit f4313c4
Showing 1 changed file with 94 additions and 22 deletions.
116 changes: 94 additions & 22 deletions python/grass/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,104 @@
import gettext
# MODULE: grass
#
# AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
#
# PURPOSE: Top level file of the grass package and its initialization
#
# COPYRIGHT: (C) 2021 Vaclav Petras, and by the GRASS Development Team
#
# This program is free software under the GNU General Public
# License (>=v2). Read the file COPYING that comes with GRASS
# for details.

"""Top-level GRASS GIS Python package
Importing the package (or any subpackage) initializes translation functions
so that the function ``_`` appears in the global namespace (as an additional build-in).
"""

import builtins as _builtins
import os

import six

# Setup i18N
# Setup translations
#
# Calling `gettext.install()` injects `_()` in the builtins namespace and
# thus it becomes available globally (i.e. in the same process). Any python
# code that needs to use _() should make sure that it firsts imports this
# library.
# The translations in the GRASS GIS Python package grass, GRASS Python modules
# (scripts), and the wxPython GUI (wxGUI) are handled as application translations,
# rather than Python module or package translations. This means that that translation
# function called `_` (underscore) is added to buildins namespace. When the grass
# package or any subpackage (or an object from there) is imported, the `_` function
# becomes available globally (i.e. for all Python code in the same process). This is
# the good part.

# Unfortunately, creating `_` in the global namespace has unitended consequences such
# as issues in interactive shells, with doctests, and it is conflicting with a common
# practice of using `_` for unused variables. This is the same behavior as with the
# common function `gettext.install()` which also adds `_` into the global namespace.
# The solution to this is to use the module translations approach instead of
# application translation approach. Without changing the overall approach, the current
# code can be modified to allow for using imports of translation functions instead of
# relying on the buildins when desired as a transitional state before removing the
# modification of buildins.
#
# Note: we need to do this at the beginning of this module in order to
# ensure that the injection happens before anything else gets imported.
# The current code mitigates two other issues associated with `gettext.install()`
# approach: First, it delays initialization of translations to the time when they are
# needed instead of doing it during import time (and possibly failing when
# environmental variables pointing to the source file are not set properly). Second,
# it adds multiple domains as fallback unlike `gettext.install()` which simply uses
# the last used domain.
#
# For more info please check the following links:
# - https://docs.python.org/2/library/gettext.html#gettext.install
# - https://pymotw.com/2//gettext/index.html#application-vs-module-localization
# For more info, please check the following links:
# - https://docs.python.org/3/library/gettext.html#gettext.translation
# - https://github.com/python/cpython/blob/main/Lib/gettext.py (esp. install function)
# - https://pymotw.com/3//gettext/index.html#application-vs-module-localization
# - https://www.wefearchange.org/2012/06/the-right-way-to-internationalize-your.html
#
_LOCALE_DIR = os.path.join(os.getenv("GISBASE"), "locale")
if six.PY2:
gettext.install("grasslibs", _LOCALE_DIR, unicode=True)
gettext.install("grassmods", _LOCALE_DIR, unicode=True)
gettext.install("grasswxpy", _LOCALE_DIR, unicode=True)
else:
gettext.install("grasslibs", _LOCALE_DIR)
gettext.install("grassmods", _LOCALE_DIR)
gettext.install("grasswxpy", _LOCALE_DIR)


def _translate(text):
"""Get translated version of text
The first call to this function initializes translations, i.e., simply importing
the package does not require the translations to be availabe. However, a first
call to translate a message will do the initialization first before translating
the message.
"""
if _translate.translation is None:
# Initialize translations if needed. This should happen (only) during the
# the first call of the function.
try:
import gettext # pylint: disable=import-outside-toplevel

gisbase = os.environ["GISBASE"]
locale_dir = os.path.join(gisbase, "locale")
# With fallback set to True, not finding the translations files for
# a language or domain results in a use of null translation, so this
# does not raise an exception even if the locale settings is broken
# or the translation files were not installed.
fallback = True
translation = gettext.translation(
"grasslibs", locale_dir, fallback=fallback
)
# Add other domains as fallback.
translation.add_fallback(
gettext.translation("grassmods", locale_dir, fallback=fallback)
)
translation.add_fallback(
gettext.translation("grasswxpy", locale_dir, fallback=fallback)
)
# Store the resulting translation object.
_translate.translation = translation
except (KeyError, ImportError):
# If the environmental variable is not set or there is no gettext,
# use null translation as an ultimate fallback.
_translate.translation = gettext.NullTranslations()
return _translate.translation.gettext(text)


# Initialize the translation attribute of the translate function to indicate
# that the translations are not initialized.
_translate.translation = None

_builtins.__dict__["_"] = _translate


__all__ = ["script", "temporal"]
Expand Down

0 comments on commit f4313c4

Please sign in to comment.