701ced5ddb
Apply diff anaconda-21.48.21-1..anaconda-22.20.13-1
566 lines
19 KiB
Python
566 lines
19 KiB
Python
# Localization classes and functions
|
|
#
|
|
# Copyright (C) 2012-2013 Red Hat, Inc.
|
|
#
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program; if not, write to the
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
# source code or documentation are not subject to the GNU General Public
|
|
# License and may only be used or replicated with the express permission of
|
|
# Red Hat, Inc.
|
|
#
|
|
# Red Hat Author(s): Martin Gracik <mgracik@redhat.com>
|
|
# Vratislav Podzimek <vpodzime@redhat.com>
|
|
#
|
|
|
|
import gettext
|
|
import os
|
|
import re
|
|
import langtable
|
|
import locale as locale_mod
|
|
import glob
|
|
from collections import namedtuple
|
|
|
|
from pyanaconda import constants
|
|
from pyanaconda.iutil import upcase_first_letter, setenv
|
|
|
|
import logging
|
|
log = logging.getLogger("anaconda")
|
|
|
|
LOCALE_CONF_FILE_PATH = "/etc/locale.conf"
|
|
|
|
#e.g. 'SR_RS.UTF-8@latin'
|
|
LANGCODE_RE = re.compile(r'(?P<language>[A-Za-z]+)'
|
|
r'(_(?P<territory>[A-Za-z]+))?'
|
|
r'(\.(?P<encoding>[-A-Za-z0-9]+))?'
|
|
r'(@(?P<script>[-A-Za-z0-9]+))?')
|
|
|
|
class LocalizationConfigError(Exception):
|
|
"""Exception class for localization configuration related problems"""
|
|
|
|
pass
|
|
|
|
class InvalidLocaleSpec(LocalizationConfigError):
|
|
"""Exception class for the errors related to invalid locale specs"""
|
|
|
|
pass
|
|
|
|
def parse_langcode(langcode):
|
|
"""
|
|
For a given langcode (e.g. 'SR_RS.UTF-8@latin') returns a dictionary
|
|
with the following keys and example values:
|
|
|
|
'language' : 'SR'
|
|
'territory' : 'RS'
|
|
'encoding' : 'UTF-8'
|
|
'script' : 'latin'
|
|
|
|
or None if the given string doesn't match the LANGCODE_RE.
|
|
|
|
"""
|
|
|
|
if not langcode:
|
|
return None
|
|
|
|
match = LANGCODE_RE.match(langcode)
|
|
if match:
|
|
return match.groupdict()
|
|
else:
|
|
return None
|
|
|
|
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 langcode_matches_locale(langcode, locale):
|
|
"""
|
|
Function that tells if the given langcode matches the given locale. I.e. if
|
|
all parts of appearing in the langcode (language, territory, script and
|
|
encoding) are the same as the matching parts of the locale.
|
|
|
|
:param langcode: a langcode (e.g. en, en_US, en_US@latin, etc.)
|
|
:type langcode: str
|
|
:param locale: a valid locale (e.g. en_US.UTF-8 or sr_RS.UTF-8@latin, etc.)
|
|
:type locale: str
|
|
:return: whether the given langcode matches the given locale or not
|
|
:rtype: bool
|
|
|
|
"""
|
|
|
|
langcode_parts = parse_langcode(langcode)
|
|
locale_parts = parse_langcode(locale)
|
|
|
|
if not langcode_parts or not locale_parts:
|
|
# to match, both need to be valid langcodes (need to have at least
|
|
# language specified)
|
|
return False
|
|
|
|
# Check parts one after another. If some part appears in the langcode and
|
|
# doesn't match the one from the locale (or is missing in the locale),
|
|
# return False, otherwise they match
|
|
for part in ("language", "territory", "script", "encoding"):
|
|
if langcode_parts[part] and langcode_parts[part] != locale_parts.get(part):
|
|
return False
|
|
|
|
return True
|
|
|
|
def find_best_locale_match(locale, langcodes):
|
|
"""
|
|
Find the best match for the locale in a list of langcodes. This is useful
|
|
when e.g. pt_BR is a locale and there are possibilities to choose an item
|
|
(e.g. rnote) for a list containing both pt and pt_BR or even also pt_PT.
|
|
|
|
:param locale: a valid locale (e.g. en_US.UTF-8 or sr_RS.UTF-8@latin, etc.)
|
|
:type locale: str
|
|
:param langcodes: a list or generator of langcodes (e.g. en, en_US, en_US@latin, etc.)
|
|
:type langcodes: list(str) or generator(str)
|
|
:return: the best matching langcode from the list of None if none matches
|
|
:rtype: str or None
|
|
|
|
"""
|
|
|
|
score_map = { "language" : 1000,
|
|
"territory": 100,
|
|
"script" : 10,
|
|
"encoding" : 1 }
|
|
|
|
def get_match_score(locale, langcode):
|
|
score = 0
|
|
|
|
locale_parts = parse_langcode(locale)
|
|
langcode_parts = parse_langcode(langcode)
|
|
if not locale_parts or not langcode_parts:
|
|
return score
|
|
|
|
for part, part_score in score_map.items():
|
|
if locale_parts[part] and langcode_parts[part]:
|
|
if locale_parts[part] == langcode_parts[part]:
|
|
# match
|
|
score += part_score
|
|
else:
|
|
# not match
|
|
score -= part_score
|
|
elif langcode_parts[part] and not locale_parts[part]:
|
|
# langcode has something the locale doesn't have
|
|
score -= part_score
|
|
|
|
return score
|
|
|
|
scores = []
|
|
|
|
# get score for each langcode
|
|
for langcode in langcodes:
|
|
scores.append((langcode, get_match_score(locale, langcode)))
|
|
|
|
# find the best one
|
|
sorted_langcodes = sorted(scores, key=lambda item_score: item_score[1], reverse=True)
|
|
|
|
# matches matching only script or encoding or both are not useful
|
|
if sorted_langcodes and sorted_langcodes[0][1] > score_map["territory"]:
|
|
return sorted_langcodes[0][0]
|
|
else:
|
|
return None
|
|
|
|
def setup_locale(locale, lang=None):
|
|
"""
|
|
Procedure setting the system to use the given locale and store it in to the
|
|
ksdata.lang object (if given). DOES NOT PERFORM ANY CHECKS OF THE GIVEN
|
|
LOCALE.
|
|
|
|
$LANG must be set by the caller in order to set the language used by gettext.
|
|
Doing this in a thread-safe way is up to the caller.
|
|
|
|
:param locale: locale to setup
|
|
:type locale: str
|
|
:param lang: ksdata.lang object or None
|
|
:return: None
|
|
:rtype: None
|
|
|
|
"""
|
|
|
|
if lang:
|
|
lang.lang = locale
|
|
|
|
setenv("LANG", locale)
|
|
locale_mod.setlocale(locale_mod.LC_ALL, 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"],
|
|
territoryIdQuery=parts.get("territory", ""),
|
|
scriptIdQuery=parts.get("script", ""))
|
|
|
|
return upcase_first_letter(name)
|
|
|
|
def get_available_translations(localedir=None):
|
|
"""
|
|
Method that generates (i.e. returns a generator) available translations for
|
|
the installer in the given localedir.
|
|
|
|
:type localedir: str
|
|
:return: generator yielding available translations (languages)
|
|
:rtype: generator yielding strings
|
|
|
|
"""
|
|
|
|
localedir = localedir or gettext._default_localedir
|
|
|
|
# usually there are no message files for en
|
|
messagefiles = sorted(glob.glob(localedir + "/*/LC_MESSAGES/anaconda.mo") +
|
|
["blob/en/blob/blob"])
|
|
trans_gen = (path.split(os.path.sep)[-3] for path in messagefiles)
|
|
|
|
langs = set()
|
|
|
|
for trans in trans_gen:
|
|
parts = parse_langcode(trans)
|
|
lang = parts.get("language", "")
|
|
if lang and lang not in langs:
|
|
langs.add(lang)
|
|
# check if there are any locales for the language
|
|
locales = get_language_locales(lang)
|
|
if not locales:
|
|
continue
|
|
|
|
yield lang
|
|
|
|
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 get_locale_territory(locale):
|
|
"""
|
|
Function returning locale's territory.
|
|
|
|
:param locale: locale string (see LANGCODE_RE)
|
|
:type locale: str
|
|
:return: territory or None
|
|
:rtype: str or None
|
|
: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 parts.get("territory", None)
|
|
|
|
def get_xlated_timezone(tz_spec_part):
|
|
"""
|
|
Function returning translated name of a region, city or complete timezone
|
|
name according to the current value of the $LANG variable.
|
|
|
|
:param tz_spec_part: a region, city or complete timezone name
|
|
:type tz_spec_part: str
|
|
:return: translated name of the given region, city or timezone
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
locale = os.environ.get("LANG", constants.DEFAULT_LANG)
|
|
parts = parse_langcode(locale)
|
|
if "language" not in parts:
|
|
raise InvalidLocaleSpec("'%s' is not a valid locale" % locale)
|
|
|
|
xlated = langtable.timezone_name(tz_spec_part, languageIdQuery=parts["language"],
|
|
territoryIdQuery=parts.get("territory", ""),
|
|
scriptIdQuery=parts.get("script", ""))
|
|
|
|
return xlated
|
|
|
|
def write_language_configuration(lang, root):
|
|
"""
|
|
Write language configuration to the $root/etc/locale.conf file.
|
|
|
|
:param lang: ksdata.lang object
|
|
:param root: path to the root of the installed system
|
|
|
|
"""
|
|
|
|
try:
|
|
fpath = os.path.normpath(root + LOCALE_CONF_FILE_PATH)
|
|
with open(fpath, "w") as fobj:
|
|
fobj.write('LANG="%s"\n' % lang.lang)
|
|
except IOError as ioerr:
|
|
msg = "Cannot write language configuration file: %s" % ioerr.strerror
|
|
raise LocalizationConfigError(msg)
|
|
|
|
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.
|
|
|
|
This method must be run before any other threads are started.
|
|
|
|
: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 IOError:
|
|
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)
|
|
|
|
os.environ["LANG"] = locales[0] # pylint: disable=environment-modify
|
|
|
|
_DateFieldSpec = namedtuple("DateFieldSpec", ["format", "suffix"])
|
|
|
|
def resolve_date_format(year, month, day, fail_safe=True):
|
|
"""
|
|
Puts the year, month and day objects in the right order according to the
|
|
currently set locale and provides format specification for each of the
|
|
fields.
|
|
|
|
:param year: any object or value representing year
|
|
:type year: any
|
|
:param month: any object or value representing month
|
|
:type month: any
|
|
:param day: any object or value representing day
|
|
:type day: any
|
|
:param bool fail_safe: whether to fall back to default in case of invalid
|
|
format or raise exception instead
|
|
:returns: a pair where the first field contains a tuple with the year, month
|
|
and day objects/values put in the right order and where the second
|
|
field contains a tuple with three :class:`_DateFieldSpec` objects
|
|
specifying formats respectively to the first (year, month, day)
|
|
field, e.g. ((year, month, day), (y_fmt, m_fmt, d_fmt))
|
|
:rtype: tuple
|
|
:raise ValueError: in case currently set locale has unsupported date
|
|
format and fail_safe is set to False
|
|
|
|
"""
|
|
|
|
FAIL_SAFE_DEFAULT = "%Y-%m-%d"
|
|
|
|
def order_terms_formats(fmt_str):
|
|
# see date (1), 'O' (not '0') is a mystery, 'E' is Buddhist calendar, '(.*)'
|
|
# is an arbitrary suffix
|
|
field_spec_re = re.compile(r'([-_0OE^#]*)([yYmbBde])(.*)')
|
|
|
|
# see date (1)
|
|
fmt_str = fmt_str.replace("%F", "%Y-%m-%d")
|
|
|
|
# e.g. "%d.%m.%Y" -> ['d.', 'm.', 'Y']
|
|
fields = fmt_str.split("%")[1:]
|
|
|
|
ordered_terms = []
|
|
ordered_formats = []
|
|
for field in fields:
|
|
match = field_spec_re.match(field)
|
|
if not match:
|
|
# ignore fields we are not interested in (like %A for weekday name, etc.)
|
|
continue
|
|
|
|
prefix, item, suffix = match.groups()
|
|
if item in ("d", "e"):
|
|
# "e" is the same as "_d"
|
|
ordered_terms.append(day)
|
|
elif item in ("Y", "y"):
|
|
# 4-digit year, 2-digit year
|
|
ordered_terms.append(year)
|
|
elif item in ("m", "b", "B"):
|
|
# month number, short month name, long month name
|
|
ordered_terms.append(month)
|
|
|
|
# "%" + prefix + item gives a format for date/time formatting functions
|
|
ordered_formats.append(_DateFieldSpec("%" + prefix + item, suffix.strip()))
|
|
|
|
if len(ordered_terms) != 3 or len(ordered_formats) != 3:
|
|
raise ValueError("Not all fields successfully identified in the format '%s'" % fmt_str)
|
|
|
|
return (tuple(ordered_terms), tuple(ordered_formats))
|
|
|
|
fmt_str = locale_mod.nl_langinfo(locale_mod.D_FMT)
|
|
|
|
if not fmt_str or "%" not in fmt_str:
|
|
if fail_safe:
|
|
# use some sane default
|
|
fmt_str = FAIL_SAFE_DEFAULT
|
|
else:
|
|
raise ValueError("Invalid date format string for current locale: '%s'" % fmt_str)
|
|
|
|
try:
|
|
return order_terms_formats(fmt_str)
|
|
except ValueError:
|
|
if not fail_safe:
|
|
raise
|
|
else:
|
|
# if this call fails too, something is going terribly wrong and we
|
|
# should be informed about it
|
|
return order_terms_formats(FAIL_SAFE_DEFAULT)
|