Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import ConfigurationOutput, save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
from archinstall.lib.general.system_menu import select_kernel, select_swap
from archinstall.lib.hardware import SysInfo
from archinstall.lib.locale import list_timezones
from archinstall.lib.locale.locale_menu import LocaleMenu
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
Expand All @@ -27,13 +30,14 @@
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.translationhandler import DEFAULT_TIMEZONE, Language, tr, translation_handler
from archinstall.tui.ui.components import tui
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import ResultType


class GlobalMenu(AbstractMenu[None]):
Expand Down Expand Up @@ -160,7 +164,7 @@ def _get_menu_options(self) -> list[MenuItem]:
MenuItem(
text=tr('Timezone'),
action=select_timezone,
value='UTC',
value=DEFAULT_TIMEZONE,
preview_action=self._prev_tz,
key='timezone',
),
Expand Down Expand Up @@ -254,8 +258,80 @@ async def _select_archinstall_language(self, preset: Language) -> Language:

self._update_lang_text()

await self._maybe_apply_language_to_locale(language)

return language

async def _maybe_apply_language_to_locale(self, language: Language) -> None:
"""Offer to mirror the selected archinstall language into the target system locale.

Triggered only when the language has a sys_lang mapping, since otherwise
there is no target locale to offer. Console font and timezone rows are
added to the prompt only when their language-derived target value differs
from the current setting, so re-picking a language with fewer mappings
(for example switching from Ukrainian to German, which has no console_font
of its own) resets the stale Ukrainian font alongside the new locale.
"""
if not language.sys_lang:
return

locale_item = self._item_group.find_by_key('locale_config')
locale_config: LocaleConfiguration | None = locale_item.value
if not locale_config:
return

tz_item = self._item_group.find_by_key('timezone')
current_tz: str = tz_item.value or DEFAULT_TIMEZONE
target_tz = language.target_timezone
offer_tz = self._is_timezone_offerable(target_tz, current_tz)

diff = locale_config.language_diff(language)
if diff.is_empty() and not offer_tz:
return

rows = diff.labeled_rows()
if offer_tz:
rows.append((tr('Timezone'), target_tz))

if not await self._confirm_locale_apply(rows):
return

locale_config.apply_language_diff(diff)
if offer_tz:
tz_item.value = target_tz

def _is_timezone_offerable(self, target_tz: str, current_tz: str) -> bool:
"""Return True when the candidate differs from the current and exists in tzdata.

The same source the timezone menu reads from, so we never offer a value
the user could not have selected manually. UTC is always present, so this
is effectively a no-op for the reset-to-default case.
"""
if target_tz == current_tz:
return False
try:
return target_tz in list_timezones()
except SysCallError as err:
debug(f'Failed to validate target timezone {target_tz}: {err}')
return False

async def _confirm_locale_apply(self, rows: list[tuple[str, str]]) -> bool:
"""Render and show the confirmation dialog for the locale changes."""
label_w = max(len(label) for label, _ in rows)
data_lines = [f' {label.ljust(label_w)} : {value}' for label, value in rows]

question = tr('Use this language as the target system language as well?')
header = tr('The following settings will be applied:')

# The TUI centers every line of the prompt independently, so pad all
# lines to a common width; otherwise the colon column drifts.
width = max(len(question), len(header), *(len(line) for line in data_lines))
separator = '=' * width
prompt = question.ljust(width) + '\n\n' + header.ljust(width) + '\n' + separator + '\n' + '\n'.join(line.ljust(width) for line in data_lines) + '\n'

result = await Confirmation(header=prompt, preset=True).show()
return result.type_ == ResultType.Selection and result.item() == MenuItem.yes()

def _prev_archinstall_language(self, item: MenuItem) -> str | None:
if not item.value:
return None
Expand Down
63 changes: 61 additions & 2 deletions archinstall/lib/models/locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,36 @@
from typing import Any, Self

from archinstall.lib.locale.utils import get_kb_layout
from archinstall.lib.translationhandler import tr
from archinstall.lib.translationhandler import DEFAULT_CONSOLE_FONT, Language, tr


@dataclass
class LocaleLanguageDiff:
"""Locale fields to write when applying a Language to a LocaleConfiguration.

Each field carries the new value, or None when no change is needed. sys_enc
is paired with sys_lang so the encoding row is shown alongside the locale
row in the confirmation dialog, even when the encoding portion itself does
not change.
"""

sys_lang: str | None = None
sys_enc: str | None = None
console_font: str | None = None

def is_empty(self) -> bool:
return self.sys_lang is None and self.sys_enc is None and self.console_font is None

def labeled_rows(self) -> list[tuple[str, str]]:
"""Return [(label, value)] for fields that would change."""
rows: list[tuple[str, str]] = []
if self.sys_lang is not None:
rows.append((tr('Locale language'), self.sys_lang))
if self.sys_enc is not None:
rows.append((tr('Locale encoding'), self.sys_enc))
if self.console_font is not None:
rows.append((tr('Console font'), self.console_font))
return rows


@dataclass
Expand All @@ -14,7 +43,7 @@ class LocaleConfiguration:
# can be checked using
# zgrep "CONFIG_FONT" /proc/config.gz
# https://wiki.archlinux.org/title/Linux_console#Font
console_font: str = 'default8x16'
console_font: str = DEFAULT_CONSOLE_FONT

@classmethod
def default(cls) -> Self:
Expand All @@ -38,6 +67,36 @@ def preview(self) -> str:
output += '{}: {}'.format(tr('Console font'), self.console_font)
return output

def language_diff(self, language: Language) -> LocaleLanguageDiff:
"""Compute the locale fields that would change if applying this language.

Returns an empty diff for languages without a sys_lang mapping. console_font
is offered when the language-derived target value differs - so re-picking
a language with fewer mappings still resets stale fonts left over from a
previous pick.
"""
diff = LocaleLanguageDiff()
if not language.sys_lang:
return diff

if self.sys_lang != language.sys_lang:
diff.sys_lang = language.sys_lang
diff.sys_enc = language.target_sys_enc or self.sys_enc

target_font = language.target_console_font
if self.console_font != target_font:
diff.console_font = target_font

return diff

def apply_language_diff(self, diff: LocaleLanguageDiff) -> None:
if diff.sys_lang is not None:
self.sys_lang = diff.sys_lang
if diff.sys_enc is not None:
self.sys_enc = diff.sys_enc
if diff.console_font is not None:
self.console_font = diff.console_font

def _load_config(self, args: dict[str, str]) -> None:
if 'sys_lang' in args:
self.sys_lang = args['sys_lang']
Expand Down
28 changes: 25 additions & 3 deletions archinstall/lib/translationhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,31 @@ class Language:
translation_percent: int
translated_lang: str | None
console_font: str | None = None
sys_lang: str | None = None
default_timezone: str | None = None

@property
def display_name(self) -> str:
name = self.name_en
return f'{name} ({self.translation_percent}%)'

@property
def target_sys_enc(self) -> str | None:
"""Encoding portion of sys_lang (e.g. 'UTF-8' from 'uk_UA.UTF-8'). None when sys_lang has no '.'."""
if self.sys_lang and '.' in self.sys_lang:
return self.sys_lang.split('.', 1)[1]
return None

@property
def target_console_font(self) -> str:
"""Console font implied by this language; falls back to the system default."""
return self.console_font or DEFAULT_CONSOLE_FONT

@property
def target_timezone(self) -> str:
"""Timezone implied by this language; falls back to UTC."""
return self.default_timezone or DEFAULT_TIMEZONE

def is_match(self, lang_or_translated_lang: str) -> bool:
if self.name_en == lang_or_translated_lang:
return True
Expand All @@ -38,7 +57,8 @@ def json(self) -> str:
return self.name_en


_DEFAULT_FONT = 'default8x16'
DEFAULT_CONSOLE_FONT = 'default8x16'
DEFAULT_TIMEZONE = 'UTC'
_ENV_FONT = os.environ.get('FONT')


Expand Down Expand Up @@ -69,7 +89,7 @@ def _set_font(self, font_name: str | None) -> bool:
if not running_from_iso():
return False

target = font_name or _DEFAULT_FONT
target = font_name or DEFAULT_CONSOLE_FONT
try:
SysCommand(['setfont', target])
return True
Expand Down Expand Up @@ -132,6 +152,8 @@ def _get_translations(self) -> list[Language]:
lang = mapping_entry['lang']
translated_lang = mapping_entry.get('translated_lang', None)
console_font = mapping_entry.get('console_font', None)
sys_lang = mapping_entry.get('sys_lang', None)
default_timezone = mapping_entry.get('default_timezone', None)

try:
# get a translation for a specific language
Expand All @@ -146,7 +168,7 @@ def _get_translations(self) -> list[Language]:
# prevent cases where the .pot file is out of date and the percentage is above 100
percent = min(100, percent)

language = Language(abbr, lang, translation, percent, translated_lang, console_font)
language = Language(abbr, lang, translation, percent, translated_lang, console_font, sys_lang, default_timezone)
languages.append(language)
except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
Expand Down
6 changes: 6 additions & 0 deletions archinstall/locales/base.pot
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ msgstr ""
msgid "Archinstall language"
msgstr ""

msgid "Use this language as the target system language as well?"
msgstr ""

msgid "The following settings will be applied:"
msgstr ""

msgid "Wipe all selected drives and use a best-effort default partition layout"
msgstr ""

Expand Down
Loading