diff --git a/pyanaconda/localization.py b/pyanaconda/localization.py index 3e44ebb462a..f1821a523ce 100644 --- a/pyanaconda/localization.py +++ b/pyanaconda/localization.py @@ -25,7 +25,9 @@ import re from collections import namedtuple +import iso639 import langtable +from xkbregistry import rxkb from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core import constants @@ -36,7 +38,10 @@ log = get_module_logger(__name__) SCRIPTS_SUPPORTED_BY_CONSOLE = {'Latn', 'Cyrl', 'Grek'} +LayoutInfo = namedtuple("LayoutInfo", ["langs", "desc"]) +Xkb_ = lambda x: gettext.translation("xkeyboard-config", fallback=True).gettext(x) +iso_ = lambda x: gettext.translation("iso_639", fallback=True).gettext(x) class LocalizationConfigError(Exception): """Exception class for localization configuration related problems""" @@ -390,6 +395,33 @@ def get_territory_locales(territory): return langtable.list_locales(territoryId=territory) +def _build_layout_infos(): + """Build localized information for keyboard layouts. + + :param rxkb_context: RXKB context (e.g., rxkb.Context()) + :return: Dictionary with layouts and their descriptions + """ + rxkb_context = rxkb.Context() + layout_infos = {} + + for layout in rxkb_context.layouts.values(): + name = layout.name + if layout.variant: + name += f" ({layout.variant})" + + langs = [] + for lang in layout.iso639_codes: + if iso639.find(iso639_2=lang): + langs.append(iso639.to_name(lang)) + + if name not in layout_infos: + layout_infos[name] = LayoutInfo(langs, layout.description) + else: + layout_infos[name].langs.extend(langs) + + return layout_infos + + def get_locale_keyboards(locale): """Function returning preferred keyboard layouts for the given locale. diff --git a/pyanaconda/modules/common/structures/keyboard_layout.py b/pyanaconda/modules/common/structures/keyboard_layout.py new file mode 100644 index 00000000000..c232e68d2b3 --- /dev/null +++ b/pyanaconda/modules/common/structures/keyboard_layout.py @@ -0,0 +1,69 @@ +# +# DBus structure for keyboard layout in localization module. +# +# Copyright (C) 2025 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. +# +from dasbus.structure import DBusData +from dasbus.typing import List, Str # Pylint: disable=wildcard-import + +__all__ = ["KeyboardLayout"] + + +class KeyboardLayout(DBusData): + """Structure representing a keyboard layout.""" + + def __init__(self): + self._layout_id = "" + self._description = "" + self._langs = [] + + @property + def layout_id(self) -> Str: + """Return the keyboard layout ID.""" + return self._layout_id + + @layout_id.setter + def layout_id(self, value: Str): + self._layout_id = value + + @property + def description(self) -> Str: + """Return the description of the layout.""" + return self._description + + @description.setter + def description(self, value: Str): + self._description = value + + @property + def langs(self) -> List[Str]: + """Return the list of associated languages.""" + return self._langs + + @langs.setter + def langs(self, value: List[Str]): + self._langs = value + + def __eq__(self, other): + """Ensure KeyboardLayout objects are correctly compared.""" + if isinstance(other, KeyboardLayout): + return ( + self.layout_id == other.layout_id + and self.description == other.description + and self.langs == other.langs + ) + return False diff --git a/pyanaconda/modules/localization/localization.py b/pyanaconda/modules/localization/localization.py index 627f39dc2f8..b30746602ea 100644 --- a/pyanaconda/modules/localization/localization.py +++ b/pyanaconda/modules/localization/localization.py @@ -17,13 +17,17 @@ # License and may only be used or replicated with the express permission of # Red Hat, Inc. # +import gettext + import langtable from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.dbus import DBus from pyanaconda.core.signal import Signal +from pyanaconda.core.string import upcase_first_letter from pyanaconda.localization import ( + _build_layout_infos, get_available_translations, get_common_languages, get_english_name, @@ -34,6 +38,7 @@ from pyanaconda.modules.common.base import KickstartService from pyanaconda.modules.common.constants.services import LOCALIZATION from pyanaconda.modules.common.containers import TaskContainer +from pyanaconda.modules.common.structures.keyboard_layout import KeyboardLayout from pyanaconda.modules.common.structures.language import LanguageData, LocaleData from pyanaconda.modules.localization.installation import ( KeyboardInstallationTask, @@ -50,6 +55,8 @@ log = get_module_logger(__name__) +Xkb_ = lambda x: gettext.translation("xkeyboard-config", fallback=True).gettext(x) +iso_ = lambda x: gettext.translation("iso_639", fallback=True).gettext(x) class LocalizationService(KickstartService): """The Localization service.""" @@ -80,6 +87,8 @@ def __init__(self): self.compositor_selected_layout_changed = Signal() self.compositor_layouts_changed = Signal() + self._layout_infos = _build_layout_infos() + self._localed_wrapper = None def publish(self): @@ -177,6 +186,69 @@ def get_locale_data(self, locale_id): return tdata + def get_layout_variant_description(self, layout_variant, with_lang=True, xlated=True): + """ + Get description of the given layout-variant. + + :param layout_variant: layout-variant specification (e.g. 'cz (qwerty)') + :type layout_variant: str + :param with_lang: whether to include language of the layout-variant (if defined) + in the description or not + :type with_lang: bool + :param xlated: whethe to return translated or english version of the description + :type xlated: bool + :return: description of the layout-variant specification (e.g. 'Czech (qwerty)') + :rtype: str + + """ + + layout_info = self._layout_infos[layout_variant] + lang = "" + # translate language and upcase its first letter, translate the + # layout-variant description + if xlated: + if len(layout_info.langs) == 1: + lang = iso_(layout_info.langs[0]) + description = Xkb_(layout_info.desc) + else: + if len(layout_info.langs) == 1: + lang = upcase_first_letter(layout_info.langs[0]) + description = layout_info.desc + + if with_lang and lang: + # ISO language/country names can be things like + # "Occitan (post 1500); Provencal", or + # "Iran, Islamic Republic of", or "Greek, Modern (1453-)" + # or "Catalan; Valencian": let's handle that gracefully + # let's also ignore case, e.g. in French all translated + # language names are lower-case for some reason + checklang = lang.split()[0].strip(",;").lower() + if checklang not in description.lower(): + return "%s (%s)" % (lang, description) + + return description + + def get_locale_keyboard_layouts(self, lang): + """Get localized keyboard layouts for a given locale. + + :param lang: locale string (e.g., "cs_CZ.UTF-8") + :return: list of dictionaries with keyboard layout information + """ + language_id = lang.split("_")[0].lower() + english_name = get_english_name(language_id) + + layouts = [] + for name, info in self._layout_infos.items(): + if english_name in info.langs: + if name: + layout = KeyboardLayout() + layout.layout_id = name + layout.description = self.get_layout_variant_description(name, with_lang=True, xlated=True) + layout.langs = info.langs + layouts.append(layout) + + return layouts + @property def language(self): """Return the language.""" diff --git a/pyanaconda/modules/localization/localization_interface.py b/pyanaconda/modules/localization/localization_interface.py index 0b46702583c..8ac51f05434 100644 --- a/pyanaconda/modules/localization/localization_interface.py +++ b/pyanaconda/modules/localization/localization_interface.py @@ -25,6 +25,7 @@ from pyanaconda.modules.common.base import KickstartModuleInterface from pyanaconda.modules.common.constants.services import LOCALIZATION from pyanaconda.modules.common.containers import TaskContainer +from pyanaconda.modules.common.structures.keyboard_layout import KeyboardLayout from pyanaconda.modules.common.structures.language import LanguageData, LocaleData @@ -91,6 +92,26 @@ def GetLocaleData(self, locale_id: Str) -> Structure: locale_data = self.implementation.get_locale_data(locale_id) return LocaleData.to_structure(locale_data) + def GetLocaleKeyboardLayouts(self, lang: Str) -> List[Structure]: + """Get keyboard layouts for the specified language. + + Returns a list of keyboard layouts available for the given language. + Each layout is represented as a `KeyboardLayout` structure. + + Example output: + [ + KeyboardLayout(layout_id="us", description="English (US)", langs=["English"]), + KeyboardLayout(layout_id="cz", description="Czech", langs=["Czech"]), + KeyboardLayout(layout_id="cz (qwerty)", description="Czech (QWERTY)", langs=["Czech"]) + ] + + :param lang: Language code string (e.g., "en_US.UTF-8") + :return: List of `KeyboardLayout` structures + """ + return KeyboardLayout.to_structure_list( + self.implementation.get_locale_keyboard_layouts(lang) + ) + @property def Language(self) -> Str: """The language the system will use.""" diff --git a/pyanaconda/ui/gui/xkl_wrapper.py b/pyanaconda/ui/gui/xkl_wrapper.py index c2261667759..11a2997304f 100644 --- a/pyanaconda/ui/gui/xkl_wrapper.py +++ b/pyanaconda/ui/gui/xkl_wrapper.py @@ -18,23 +18,19 @@ import gettext import threading -from collections import namedtuple -import iso639 from xkbregistry import rxkb from pyanaconda import localization from pyanaconda.core.async_utils import async_action_wait from pyanaconda.core.string import upcase_first_letter from pyanaconda.keyboard import normalize_layout_variant +from pyanaconda.localization import _build_layout_infos from pyanaconda.modules.common.constants.services import LOCALIZATION Xkb_ = lambda x: gettext.translation("xkeyboard-config", fallback=True).gettext(x) iso_ = lambda x: gettext.translation("iso_639", fallback=True).gettext(x) -# namedtuple for information about a keyboard layout (its language and description) -LayoutInfo = namedtuple("LayoutInfo", ["langs", "desc"]) - class XklWrapper: """ Class that used to wrap libxklavier functionality. @@ -62,26 +58,11 @@ def __init__(self): self._rxkb = rxkb.Context() self._layout_infos = {} - self._build_layout_infos() + self._layout_infos = _build_layout_infos() self._switch_opt_infos = {} self._build_switch_opt_infos() - def _build_layout_infos(self): - for layout in self._rxkb.layouts.values(): - name = layout.name - if layout.variant: - name += ' (' + layout.variant + ')' - - langs = [] - for lang in layout.iso639_codes: - if iso639.find(iso639_2=lang): - langs.append(iso639.to_name(lang)) - - if name not in self._layout_infos: - self._layout_infos[name] = LayoutInfo(langs, layout.description) - else: - self._layout_infos[name].langs.extend(langs) def _build_switch_opt_infos(self): for group in self._rxkb.option_groups: