diff --git a/Doc/library/gettext.rst b/Doc/library/gettext.rst index d0de83907eb297..3a352ccea6f582 100644 --- a/Doc/library/gettext.rst +++ b/Doc/library/gettext.rst @@ -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. @@ -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) diff --git a/Lib/gettext.py b/Lib/gettext.py index 6c11ab2b1eb570..55397026ba3f08 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -46,6 +46,7 @@ import operator import os import sys +import locale __all__ = ['NullTranslations', 'GNUTranslations', 'Catalog', @@ -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 @@ -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 diff --git a/Lib/test/test_gettext.py b/Lib/test/test_gettext.py index 33b7d75e3ff203..0a976fce084c4b 100644 --- a/Lib/test/test_gettext.py +++ b/Lib/test/test_gettext.py @@ -1,3 +1,4 @@ +import locale import os import base64 import gettext @@ -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') @@ -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. diff --git a/Misc/NEWS.d/next/Library/2025-03-19-17-30-00.gh-issue-64243.fuheq3.rst b/Misc/NEWS.d/next/Library/2025-03-19-17-30-00.gh-issue-64243.fuheq3.rst new file mode 100644 index 00000000000000..8e301340c4fa48 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-19-17-30-00.gh-issue-64243.fuheq3.rst @@ -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.