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.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: