Skip to content

gh-64243: Implement locale.getlocale fall back in gettext.find #131477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
10 changes: 7 additions & 3 deletions Doc/library/gettext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ install themselves in the built-in namespace as the function :func:`!_`.
strings, where each string is a language code.

If *localedir* is not given, then the default system locale directory is used.
[#]_ If *languages* is not given, then the following environment variables are
searched: :envvar:`LANGUAGE`, :envvar:`LC_ALL`, :envvar:`LC_MESSAGES`, and
:envvar:`LANG`. The first one returning a non-empty value is used for the
[#]_ If *languages* is not given, then the environment variable :envvar:`LANGUAGE`
is searched, it falls back to the current locale or to the environment
variables :envvar:`LC_ALL`, :envvar:`LC_MESSAGES`, and
:envvar:`LANG` where the first one returning a non-empty value is used for the
*languages* variable. The environment variables should contain a colon separated
list of languages, which will be split on the colon to produce the expected list
of language code strings.
Expand All @@ -147,6 +148,9 @@ install themselves in the built-in namespace as the function :func:`!_`.
of all file names, in the order in which they appear in the languages list or
the environment variables.

.. versionchanged:: next
:func:`locale.setlocale` is used to generate *languages* if *languages* is
not provided.

.. function:: translation(domain, localedir=None, languages=None, class_=None, fallback=False)

Expand Down
17 changes: 11 additions & 6 deletions Lib/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import operator
import os
import sys
import locale


__all__ = ['NullTranslations', 'GNUTranslations', 'Catalog',
Expand Down Expand Up @@ -229,7 +230,6 @@ def func(n):


def _expand_lang(loc):
import locale
loc = locale.normalize(loc)
COMPONENT_CODESET = 1 << 0
COMPONENT_TERRITORY = 1 << 1
Expand Down Expand Up @@ -491,11 +491,16 @@ def find(domain, localedir=None, languages=None, all=False):
localedir = _default_localedir
if languages is None:
languages = []
for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
val = os.environ.get(envar)
if val:
languages = val.split(':')
break
if val := os.environ.get('LANGUAGE'):
languages = val.split(':')
elif (loc := locale.getlocale()) != (None, None):
languages = [".".join(filter(None, loc))]
else:
for envar in ('LC_ALL', 'LC_MESSAGES', 'LANG'):
val = os.environ.get(envar)
if val:
languages = val.split(':')
break
if 'C' not in languages:
languages.append('C')
# now normalize and expand the languages
Expand Down
65 changes: 57 additions & 8 deletions Lib/test/test_gettext.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import locale
import os
import base64
import gettext
Expand Down Expand Up @@ -875,17 +876,65 @@ def create_mo_file(self, lang):
f.write(GNU_MO_DATA)
return mo_file

def test_find_with_env_vars(self):
# test that find correctly finds the environment variables
# when languages are not supplied
mo_file = self.create_mo_file("ga_IE")
def _for_all_vars(self, mo_file, locale, expected=True):
for var in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
self.env.set(var, 'ga_IE')
self.env.set(var, locale)
result = gettext.find("mofile",
localedir=os.path.join(self.tempdir, "locale"))
self.assertEqual(result, mo_file)
if expected:
self.assertEqual(mo_file, result)
else:
self.assertIsNone(result)
self.env.unset(var)

@unittest.mock.patch("locale.getlocale", return_value=(None, None))
def test_find_with_env_vars(self, patch_getlocale):
# test that find correctly finds the environment variables
# when languages are not supplied
mo_file = self.create_mo_file("ca_ES")
self._for_all_vars(mo_file, "ca_ES")
self._for_all_vars(mo_file, "ca_ES.UTF-8")
self._for_all_vars(mo_file, "ca_ES.UTF-8.mo")
self._for_all_vars(mo_file, "es_ES:ca_ES:fr_FR")
self._for_all_vars(mo_file, "ca_ES@euro")
self._for_all_vars(mo_file, "ca_ES.UTF-8@euro")
self._for_all_vars(mo_file, "ca_ES@valencia")
self._for_all_vars(mo_file, "C", expected=False)
self._for_all_vars(mo_file, "C.UTF-8", expected=False)

@unittest.mock.patch('gettext._expand_lang')
def test_encoding_not_ignored(self, patch_expand_lang):
self.env.set('LANGUAGE', 'ga_IE.UTF-8')
gettext.find("mofile")
patch_expand_lang.assert_any_call('ga_IE.UTF-8')
self.env.unset('LANGUAGE')

def test_find_LANGUAGE_priority(self):
self.env.set('LANGUAGE', 'ga_IE')
self.env.set('LC_ALL', 'C')
orig = locale.setlocale(locale.LC_ALL)
self.addCleanup(lambda: locale.setlocale(locale.LC_ALL, orig))
locale.setlocale(locale.LC_ALL, 'C')
mo_file = self.create_mo_file("ga_IE")
result = gettext.find("mofile", localedir=os.path.join(self.tempdir, "locale"))
self.assertEqual(result, mo_file)

def test_process_vars_override(self):
orig = locale.setlocale(locale.LC_ALL)
self.addCleanup(lambda: locale.setlocale(locale.LC_ALL, orig))
mo_file = self.create_mo_file("ca_ES")
for loc in ("ca_ES", "ca_ES.UTF-8", "ca_ES@euro", "ca_ES@valencia"):
try:
locale.setlocale(locale.LC_ALL, loc)
except locale.Error:
self.skipTest('platform does not support locale')
result = gettext.find("mofile", localedir=os.path.join(self.tempdir, "locale"))
self.assertEqual(mo_file, result)
for loc in ("C", "C.UTF-8"):
locale.setlocale(locale.LC_ALL, loc)
result = gettext.find("mofile", localedir=os.path.join(self.tempdir, "locale"))
self.assertIsNone(result)

def test_find_with_languages(self):
# test that passed languages are used
self.env.set('LANGUAGE', 'pt_BR')
Expand Down Expand Up @@ -934,14 +983,14 @@ def test__all__(self):

@cpython_only
def test_lazy_import(self):
ensure_lazy_imports("gettext", {"re", "warnings", "locale"})
ensure_lazy_imports("gettext", {"re", "warnings"})


if __name__ == '__main__':
unittest.main()


# For reference, here's the .po file used to created the GNU_MO_DATA above.
# For reference, here's the .po file used to create the GNU_MO_DATA above.
#
# The original version was automatically generated from the sources with
# pygettext. Later it was manually modified to add plural forms support.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Implement a fall back to :func:`locale.getlocale` in :func:`gettext.find` if
*languages* is not provided and :envvar:`LANGUAGE` is not set.
Loading