[PATCH 1/3] Rewrite the localization module
Vratislav Podzimek
vpodzime at redhat.com
Fri Jul 12 11:01:36 UTC 2013
We can now use the langtable package for a lot of things and the old version of
the module was very cryptic and too complex for what we need of it.
Signed-off-by: Vratislav Podzimek <vpodzime at redhat.com>
---
anaconda.spec.in | 3 +-
pyanaconda/localization.py | 619 +++++++++++++++++++--------------------------
2 files changed, 267 insertions(+), 355 deletions(-)
diff --git a/anaconda.spec.in b/anaconda.spec.in
index 4de99a5..2cba3e6 100644
--- a/anaconda.spec.in
+++ b/anaconda.spec.in
@@ -38,7 +38,7 @@ Source0: %{name}-%{version}.tar.bz2
%define isomd5sum 1.0.10
%define fcoeutilsver 1.0.12-3.20100323git
%define iscsiver 6.2.0.870-3
-%define langtablever 0.0.5-1
+%define langtablever 0.0.7-1
BuildRequires: audit-libs-devel
BuildRequires: gettext >= %{gettextver}
@@ -88,7 +88,6 @@ Requires: yum >= %{yumver}
Requires: python-urlgrabber >= %{pythonurlgrabberver}
Requires: system-logos
Requires: pykickstart >= %{pykickstartver}
-Requires: python-babel
Requires: langtable-data >= %{langtablever}
Requires: langtable-python >= %{langtablever}
Requires: libuser-python
diff --git a/pyanaconda/localization.py b/pyanaconda/localization.py
index f9d0037..3611c87 100644
--- a/pyanaconda/localization.py
+++ b/pyanaconda/localization.py
@@ -20,28 +20,22 @@
# Vratislav Podzimek <vpodzime at redhat.com>
#
-from collections import defaultdict, deque
import gettext
-import locale as locale_mod
import os
import re
-import logging
-log = logging.getLogger("anaconda")
-
-import babel
import langtable
+import glob
-from pyanaconda.constants import DEFAULT_LANG
-
-LOCALE_PREFERENCES = {}
+import logging
+log = logging.getLogger("anaconda")
LOCALE_CONF_FILE_PATH = "/etc/locale.conf"
#e.g. 'SR_RS.UTF-8 at latin'
LANGCODE_RE = re.compile(r'(?P<language>[A-Za-z]+)'
r'(_(?P<territory>[A-Za-z]+))?'
- r'(\.(?P<codeset>[-\w]+))?'
- r'(@(?P<modifier>[-\w]+))?')
+ r'(\.(?P<encoding>[-\w]+))?'
+ r'(@(?P<script>[-\w]+))?')
class LocalizationConfigError(Exception):
"""Exception class for localization configuration related problems"""
@@ -53,178 +47,26 @@ class InvalidLocaleSpec(LocalizationConfigError):
pass
-class LocaleInfo(object):
+def _upcase_first_letter(string):
"""
- Class unifying the glibc-like and CLDR-like locales by providing a single
- API based on properties.
-
- """
-
- def __init__(self, localespec):
- """
- Constructor for the LocaleInfo class.
-
- :param localespec: locale string such as "sr_RS.UTF-8 at latin"
- (glibc-like format) or "sr_Latn_RS" (CLDR-format)
- :type localespec: str
-
- """
-
- # get the basic values
- parsed_values = parse_langcode(localespec)
- if parsed_values:
- # successfully parsed -> a glibc-like locale, try to get some more
- # information by querying langtable
-
- # language must be specified
- language = parsed_values["language"]
- territory = parsed_values.get("territory", "")
- script = parsed_values.get("modifier", "")
-
- locales = langtable.list_locales(languageId=language,
- territoryId=territory,
- scriptId=script)
- else:
- # not in the glibc-like format, use langtable to get it
- locales = langtable.list_locales(languageId=localespec)
-
- if not locales:
- msg = "'%s' is not a valid locale" % localespec
- raise InvalidLocaleSpec(msg)
-
- # locales are sorted from the most probable to the least probable
- self._locale = locales[0]
-
- parsed_values = parse_langcode(self._locale)
-
- # language must be specified
- self._language = parsed_values["language"]
-
- # try to get the other values
- self._territory = parsed_values.get("territory", "")
- self._encoding = parsed_values.get("codeset", "")
- self._script = parsed_values.get("modifier", "")
-
- # TODO: never appeared so far, needs to be parsed and stored?
- self._variant = None
-
- @property
- def language(self):
- return self._language
-
- @property
- def territory(self):
- return self._territory
-
- @property
- def script(self):
- return self._script
-
- @property
- def variant(self):
- return self._variant
-
- @property
- def encoding(self):
- return self._encoding
-
- @property
- def locale(self):
- return self._locale
-
- @property
- def english_name(self):
- name = langtable.language_name(languageId=self.language,
- territoryId=self.territory,
- scriptId=self.script,
- languageIdQuery="en")
- # some languages don't start with a capital letter
- return name.title()
-
- @property
- def display_name(self):
- name = langtable.language_name(languageId=self.language,
- territoryId=self.territory,
- scriptId=self.script)
- # some languages don't start with a capital letter
- return name.title()
-
- @property
- def short_name(self):
- return self.__repr__()
-
- def __repr__(self):
- formatstr = '{0.language}'
- if self.territory is not None:
- formatstr += '_{0.territory}'
- if self.encoding:
- formatstr += '.{0.encoding}'
- if self.script:
- formatstr += '@{0.script}'
- if self.variant is not None:
- formatstr += '#{0.variant}'
-
- return formatstr.format(self)
-
- def __str__(self):
- return self.english_name.encode('ascii', 'replace')
-
- def __unicode__(self):
- return self.english_name
-
- def __eq__(self, other):
- return repr(self) == repr(other)
-
-# XXX this should probably be somewhere else
-def partition(seq, func=bool, func_range=(True, False)):
- buffers = dict(((x, deque()) for x in func_range))
-
- def values(x, seq=iter(seq)):
- while True:
- while not buffers[x]:
- item = seq.next()
- buffers[func(item)].append(item)
-
- yield buffers[x].popleft()
-
- return tuple(values(x) for x in func_range)
-
-
-def get_all_locales():
- localeset = set()
- for localename in sorted(babel.localedata.list()):
- try:
- locale = LocaleInfo(localename)
- except InvalidLocaleSpec:
- continue
-
- if repr(locale) not in localeset:
- localeset.add(repr(locale))
- yield locale
+ Helper function that upcases the first letter of the string. Python's
+ standard string.capitalize() not only upcases the first letter but also
+ lowercases all the others. string.title() capitalizes all words in the
+ string.
+ :type string: either a str or unicode object
+ :return: the given string with the first letter upcased
+ :rtype: str or unicode (depends on the input)
-def get_available_translations(domain=None, localedir=None):
- localeset = set()
- domain = domain or gettext._current_domain
- localedir = localedir or gettext._default_localedir
-
- langs = babel.localedata.list()
- messagefiles = gettext.find(domain, localedir, langs, all=True)
- languages = [path.split(os.path.sep)[-3] for path in messagefiles]
-
- # usually there are no message files for en
- if 'en' not in languages:
- languages.append('en')
-
- for langcode in languages:
- try:
- locale = LocaleInfo(langcode)
- except InvalidLocaleSpec:
- continue
+ """
- if repr(locale) not in localeset:
- localeset.add(repr(locale))
- yield locale
+ if not string:
+ # cannot change anything
+ return string
+ elif len(string) == 1:
+ return string.upper()
+ else:
+ return string[0].upper() + string[1:]
def parse_langcode(langcode):
"""
@@ -265,7 +107,7 @@ def expand_langs(astring):
return list(langs)
base, loc, enc, script = [lang_dict[key] for key in ("language",
- "territory", "codeset", "modifier")]
+ "territory", "encoding", "script")]
if not base:
return list(langs)
@@ -288,6 +130,194 @@ def expand_langs(astring):
return list(langs)
+def is_supported_locale(locale):
+ """
+ Function that tells if the given locale is supported by the Anaconda or
+ not. We consider locales supported by the langtable as supported by the
+ Anaconda.
+
+ :param locale: locale to test
+ :type locale: str
+ :return: whether the given locale is supported or not
+ :rtype: bool
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ en_name = get_english_name(locale)
+ return bool(en_name)
+
+def setup_locale(locale, lang):
+ """
+ Procedure setting the system to use the given locale and store it in to the
+ ksdata.lang object. DOES NOT PERFORM ANY CHECKS OF THE GIVEN LOCALE.
+
+ :param locale: locale to setup
+ :type locale: str
+ :param lang: ksdata.lang object
+ :return: None
+ :rtype: None
+
+ """
+
+ lang.lang = locale
+ os.environ["LANG"] = locale
+
+def get_english_name(locale):
+ """
+ Function returning english name for the given locale.
+
+ :param locale: locale to return english name for
+ :type locale: str
+ :return: english name for the locale or empty string if unknown
+ :rtype: st
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ parts = parse_langcode(locale)
+ if "language" not in parts:
+ raise InvalidLocaleSpec("'%s' is not a valid locale" % locale)
+
+ name = langtable.language_name(languageId=parts["language"],
+ territoryId=parts.get("territory", ""),
+ scriptId=parts.get("script", ""),
+ languageIdQuery="en")
+
+ return _upcase_first_letter(name)
+
+def get_native_name(locale):
+ """
+ Function returning native name for the given locale.
+
+ :param locale: locale to return native name for
+ :type locale: str
+ :return: english name for the locale or empty string if unknown
+ :rtype: st
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ parts = parse_langcode(locale)
+ if "language" not in parts:
+ raise InvalidLocaleSpec("'%s' is not a valid locale" % locale)
+
+ name = langtable.language_name(languageId=parts["language"],
+ territoryId=parts.get("territory", ""),
+ scriptId=parts.get("script", ""),
+ languageIdQuery=parts["language"])
+
+ return _upcase_first_letter(name)
+
+def get_available_translations(domain=None, localedir=None):
+ """
+ Method that generates (i.e. returns a generator) available translations for
+ the given domain and localedir.
+
+ :type domain: str
+ :type localedir: str
+ :return: generator yielding available translations
+ :rtype: generator yielding strings
+
+ """
+
+ domain = domain or gettext._current_domain
+ localedir = localedir or gettext._default_localedir
+
+ messagefiles = sorted(glob.glob(localedir + "/*/LC_MESSAGES/anaconda.mo"))
+ trans_gen = (path.split(os.path.sep)[-3] for path in messagefiles)
+
+ # usually there are no message files for en
+ langs = {"en"}
+ yield "en_US.UTF-8"
+
+ for trans in trans_gen:
+ parts = parse_langcode(trans)
+ lang = parts.get("language", "")
+ if lang and lang not in langs:
+ langs.add(lang)
+ locales = get_language_locales(lang)
+ if not locales:
+ continue
+
+ # take the first locale (with highest rank) for the language
+ yield locales[0]
+
+def get_language_locales(lang):
+ """
+ Function returning all locales available for the given language.
+
+ :param lang: language to get available locales for
+ :type lang: str
+ :return: a list of available locales
+ :rtype: list of strings
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ parts = parse_langcode(lang)
+ if "language" not in parts:
+ raise InvalidLocaleSpec("'%s' is not a valid language" % lang)
+
+ return langtable.list_locales(languageId=parts["language"],
+ territoryId=parts.get("territory", ""),
+ scriptId=parts.get("script", ""))
+
+def get_territory_locales(territory):
+ """
+ Function returning list of locales for the given territory. The list is
+ sorted from the most probable locale to the least probable one (based on
+ langtable's ranking.
+
+ :param territory: territory to return locales for
+ :type territory: str
+ :return: list of locales
+ :rtype: list of strings
+
+ """
+
+ return langtable.list_locales(territoryId=territory)
+
+def get_locale_keyboards(locale):
+ """
+ Function returning preferred keyboard layouts for the given locale.
+
+ :param locale: locale string (see LANGCODE_RE)
+ :type locale: str
+ :return: list of preferred keyboard layouts
+ :rtype: list of strings
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ parts = parse_langcode(locale)
+ if "language" not in parts:
+ raise InvalidLocaleSpec("'%s' is not a valid locale" % locale)
+
+ return langtable.list_keyboards(languageId=parts["language"],
+ territoryId=parts.get("territory", ""),
+ scriptId=parts.get("script", ""))
+
+def get_locale_timezones(locale):
+ """
+ Function returning preferred timezones for the given locale.
+
+ :param locale: locale string (see LANGCODE_RE)
+ :type locale: str
+ :return: list of preferred timezones
+ :rtype: list of strings
+ :raise InvalidLocaleSpec: if an invalid locale is given (see LANGCODE_RE)
+
+ """
+
+ parts = parse_langcode(locale)
+ if "language" not in parts:
+ raise InvalidLocaleSpec("'%s' is not a valid locale" % locale)
+
+ return langtable.list_timezones(languageId=parts["language"],
+ territoryId=parts.get("territory", ""),
+ scriptId=parts.get("script", ""))
+
def write_language_configuration(lang, root):
"""
Write language configuration to the $root/etc/locale.conf file.
@@ -305,176 +335,59 @@ def write_language_configuration(lang, root):
msg = "Cannot write language configuration file: %s" % ioerr.strerror
raise LocalizationConfigError(msg)
-class PreferredLocale(object):
-
- @staticmethod
- def from_language(language):
- locales = defaultdict(set)
- for locale in get_all_locales():
- locales[repr(locale)].add(locale)
- locales[locale.language].add(locale)
-
- return PreferredLocale(locales[language])
-
- @staticmethod
- def from_territory(territory):
- locales = defaultdict(set)
- for locale in get_all_locales():
- locales[locale.territory].add(locale)
-
- return PreferredLocale(locales[territory])
-
- def __init__(self, localeset):
- self._localedict = {repr(locale):locale for locale in localeset}
-
- def get_all_locales(self, preferences=None):
- if preferences is None:
- preferences = []
- preferences = filter(self._localedict.__contains__, preferences)
- inside, outside = partition(self._localedict.keys(),
- func=lambda x: x in preferences)
- sorted_locales = [self._localedict[localename]
- for localename in list(inside) + list(outside)]
- return sorted_locales
-
- def get_preferred_locale(self, preferences=None):
- if preferences is None:
- preferences = []
- try:
- return self.get_all_locales(preferences)[0]
- except IndexError:
- return None
-
-
-class Language(object):
-
- def __init__(self, preferences=None, territory=None):
- if preferences is None:
- preferences = {}
-
- self.translations = {repr(locale):locale
- for locale in get_available_translations()}
- self.locales = {repr(locale):locale for locale in get_all_locales()}
-
- self.system_lang = self._get_firmware_language()
- self.install_lang = self.system_lang
- self.preferred_translation = self.translations[self.system_lang]
- self.preferred_locales = [self.locales[self.system_lang]]
- self.preferred_locale = self.preferred_locales[0]
-
- self.all_preferences = preferences
- self.preferences = self.all_preferences.get(territory, [])
- self.territory = territory
- if self.territory:
- self._get_preferred_translation_and_locales()
-
- def _get_firmware_language(self):
- try:
- n = "/sys/firmware/efi/efivars/PlatformLang-8be4df61-93ca-11d2-aa0d-00e098032b8c"
- d = open(n, 'r', 0).read()
- except:
- return DEFAULT_LANG
-
- try:
- # the contents of the file are:
- # 4-bytes of attribute data that we don't care about
- # NUL terminated ASCII string like 'en-US'.
- if len(d) < 10:
- log.debug("PlatformLang was too short")
- raise ValueError
- d = d[4:]
- if d[2] != '-':
- log.debug("PlatformLang was malformed")
- raise ValueError
-
- # they use - and we use _, so fix it...
- d = d[:2] + '_' + d[3:-1]
-
- # UEFI 2.3.1 Errata C specifies 2 aliases in common use that
- # aren't part of RFC 4646, but are allowed in PlatformLang.
- # Because why make anything simple?
- if d.startswith('zh_chs'):
- d = 'zh_Hans'
- elif d.startswith('zh_cht'):
- d = 'zh_Hant'
- d += '.UTF-8'
-
- if not self.translations.has_key(d):
- log.debug("PlatformLang was \"%s\", which is unsupported." % d)
- raise ValueError
- log.debug("Using UEFI PlatformLang \"%s\" as our language." % d)
- return d
- except ValueError:
- return DEFAULT_LANG
-
- def _get_preferred_translation_and_locales(self):
- # get locales from territory
- locales_from_territory = PreferredLocale.from_territory(self.territory)
- all_locales = locales_from_territory.get_all_locales(self.preferences)
-
- # get preferred translation
- for locale in all_locales:
- if locale.language in self.translations:
- self.preferred_translation = self.translations[locale.language]
- break
-
- for locale in all_locales:
- if locale.short_name in self.translations:
- self.preferred_translation = self.translations[locale.short_name]
- break
-
- self.preferred_locales = all_locales
-
- def select_translation(self, translation):
- translation = self.translations[translation]
- self.preferences.extend(self.all_preferences.get(translation.language, []))
-
- # get locales from translation
- locales_from_language = PreferredLocale.from_language(translation.short_name)
- all_locales = locales_from_language.get_all_locales(self.preferences)
-
- # get preferred locale
- for locale in all_locales:
- if locale in self.preferred_locales:
- self.preferred_locale = locale
- break
- else:
- try:
- self.preferred_locale = all_locales[0]
- except IndexError:
- self.preferred_locale = self.preferred_locales[0]
-
- # add the preferred locale to the beginning of locales
- if self.preferred_locale in self.preferred_locales:
- self.preferred_locales.remove(self.preferred_locale)
- self.preferred_locales.insert(0, self.preferred_locale)
-
- # if territory is not set, use the one from preferred locale
- self.territory = self.territory or self.preferred_locale.territory
-
- @property
- def install_lang_as_dict(self):
- parse_langcode(self.install_lang)
-
- @property
- def system_lang_as_dict(self):
- parse_langcode(self.system_lang)
-
- def set_install_lang(self, langcode):
- self.install_lang = langcode
-
- os.environ['LANG'] = langcode
- os.environ['LC_NUMERIC'] = 'C'
-
- try:
- locale_mod.setlocale(locale_mod.LC_ALL, '')
- except locale_mod.Error:
- pass
-
- # XXX this is the sort of thing which you should never do,
- # but we switch languages at runtime and thus need to invalidate
- # the set of languages/mofiles which gettext knows about
- gettext._translations = {}
-
- def set_system_lang(self, langcode):
- self.system_lang = langcode
+def load_firmware_language(lang):
+ """
+ Procedure that loads firmware language information (if any). It stores the
+ information in the given ksdata.lang object and sets the $LANG environment
+ variable.
+
+ :param lang: ksdata.lang object
+ :return: None
+ :rtype: None
+
+ """
+
+ if lang.lang and lang.seen:
+ # set in kickstart, do not override
+ return
+
+ try:
+ n = "/sys/firmware/efi/efivars/PlatformLang-8be4df61-93ca-11d2-aa0d-00e098032b8c"
+ d = open(n, 'r', 0).read()
+ except:
+ return
+
+ # the contents of the file are:
+ # 4-bytes of attribute data that we don't care about
+ # NUL terminated ASCII string like 'en-US'.
+ if len(d) < 10:
+ log.debug("PlatformLang was too short")
+ return
+ d = d[4:]
+ if d[2] != '-':
+ log.debug("PlatformLang was malformed")
+ return
+
+ # they use - and we use _, so fix it...
+ d = d[:2] + '_' + d[3:-1]
+
+ # UEFI 2.3.1 Errata C specifies 2 aliases in common use that
+ # aren't part of RFC 4646, but are allowed in PlatformLang.
+ # Because why make anything simple?
+ if d.startswith('zh_chs'):
+ d = 'zh_Hans'
+ elif d.startswith('zh_cht'):
+ d = 'zh_Hant'
+ d += '.UTF-8'
+
+ if not is_supported_locale(d):
+ log.debug("PlatformLang was '%s', which is unsupported." % d)
+ return
+
+ locales = get_language_locales(d)
+ if not locales:
+ log.debug("No locales found for the PlatformLang '%s'." % d)
+ return
+
+ log.debug("Using UEFI PlatformLang '%s' ('%s') as our language." % (d, locales[0]))
+ setup_locale(locales[0], lang)
--
1.7.11.7
More information about the anaconda-patches
mailing list