Skip to content
81 changes: 79 additions & 2 deletions archinstall/lib/global_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
from archinstall.lib.bootloader.utils import validate_bootloader_layout
from archinstall.lib.configuration import 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.log import debug
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 @@ -30,10 +34,11 @@
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.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
from archinstall.tui.result import ResultType


class GlobalMenu(AbstractMenu[None]):
Expand Down Expand Up @@ -160,7 +165,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 +259,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 @@ -3,7 +3,36 @@

from archinstall.lib.locale.utils import get_kb_layout
from archinstall.lib.models.config import SubConfig
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 @@ -15,7 +44,7 @@ class LocaleConfiguration(SubConfig):
# 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 Down Expand Up @@ -49,6 +78,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 @@ -133,6 +153,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 @@ -149,7 +171,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)

return languages
Expand Down
6 changes: 6 additions & 0 deletions archinstall/locales/base.pot
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,12 @@ msgid ""
"greeter"
msgstr ""

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

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

msgid "Language"
msgstr ""

Expand Down
Loading