Skip to content

Commit 4746b8b

Browse files
committed
Offer to apply archinstall language to target system locale and timezone
After picking an installer language with a known locale mapping, prompt the user to apply sys_lang, sys_enc, console_font, and (where the country maps to a single IANA zone) timezone to the target system. Each row is shown only when its target value differs from the current setting, so re-picking a language with fewer mappings resets stale fields too. default_timezone is set in languages.json only for unambiguous single mainland zones; multi-zone countries (en_US, ru_RU, es_ES, pt_BR, ...) and codes that span countries are left without a default. Timezone candidates are validated against list_timezones().
1 parent 76629ec commit 4746b8b

7 files changed

Lines changed: 214 additions & 45 deletions

File tree

archinstall/lib/global_menu.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
from archinstall.lib.bootloader.utils import validate_bootloader_layout
99
from archinstall.lib.configuration import ConfigurationOutput, save_config
1010
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
11+
from archinstall.lib.exceptions import SysCallError
1112
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
1213
from archinstall.lib.general.system_menu import select_kernel, select_swap
1314
from archinstall.lib.hardware import SysInfo
15+
from archinstall.lib.locale import list_timezones
1416
from archinstall.lib.locale.locale_menu import LocaleMenu
1517
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
18+
from archinstall.lib.menu.helpers import Confirmation
1619
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
1720
from archinstall.lib.mirror.mirror_menu import MirrorMenu
1821
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
@@ -27,13 +30,14 @@
2730
from archinstall.lib.models.pacman import PacmanConfiguration
2831
from archinstall.lib.models.profile import ProfileConfiguration
2932
from archinstall.lib.network.network_menu import select_network
30-
from archinstall.lib.output import FormattedOutput
33+
from archinstall.lib.output import FormattedOutput, debug
3134
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
3235
from archinstall.lib.pacman.config import PacmanConfig
3336
from archinstall.lib.pacman.pacman_menu import PacmanMenu
34-
from archinstall.lib.translationhandler import Language, tr, translation_handler
37+
from archinstall.lib.translationhandler import DEFAULT_TIMEZONE, Language, tr, translation_handler
3538
from archinstall.tui.ui.components import tui
3639
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup
40+
from archinstall.tui.ui.result import ResultType
3741

3842

3943
class GlobalMenu(AbstractMenu[None]):
@@ -160,7 +164,7 @@ def _get_menu_options(self) -> list[MenuItem]:
160164
MenuItem(
161165
text=tr('Timezone'),
162166
action=select_timezone,
163-
value='UTC',
167+
value=DEFAULT_TIMEZONE,
164168
preview_action=self._prev_tz,
165169
key='timezone',
166170
),
@@ -254,8 +258,80 @@ async def _select_archinstall_language(self, preset: Language) -> Language:
254258

255259
self._update_lang_text()
256260

261+
await self._maybe_apply_language_to_locale(language)
262+
257263
return language
258264

265+
async def _maybe_apply_language_to_locale(self, language: Language) -> None:
266+
"""Offer to mirror the selected archinstall language into the target system locale.
267+
268+
Triggered only when the language has a sys_lang mapping, since otherwise
269+
there is no target locale to offer. Console font and timezone rows are
270+
added to the prompt only when their language-derived target value differs
271+
from the current setting, so re-picking a language with fewer mappings
272+
(for example switching from Ukrainian to German, which has no console_font
273+
of its own) resets the stale Ukrainian font alongside the new locale.
274+
"""
275+
if not language.sys_lang:
276+
return
277+
278+
locale_item = self._item_group.find_by_key('locale_config')
279+
locale_config: LocaleConfiguration | None = locale_item.value
280+
if not locale_config:
281+
return
282+
283+
tz_item = self._item_group.find_by_key('timezone')
284+
current_tz: str = tz_item.value or DEFAULT_TIMEZONE
285+
target_tz = language.target_timezone
286+
offer_tz = self._is_timezone_offerable(target_tz, current_tz)
287+
288+
diff = locale_config.language_diff(language)
289+
if diff.is_empty() and not offer_tz:
290+
return
291+
292+
rows = diff.labeled_rows()
293+
if offer_tz:
294+
rows.append((tr('Timezone'), target_tz))
295+
296+
if not await self._confirm_locale_apply(rows):
297+
return
298+
299+
locale_config.apply_language_diff(diff)
300+
if offer_tz:
301+
tz_item.value = target_tz
302+
303+
def _is_timezone_offerable(self, target_tz: str, current_tz: str) -> bool:
304+
"""Return True when the candidate differs from the current and exists in tzdata.
305+
306+
The same source the timezone menu reads from, so we never offer a value
307+
the user could not have selected manually. UTC is always present, so this
308+
is effectively a no-op for the reset-to-default case.
309+
"""
310+
if target_tz == current_tz:
311+
return False
312+
try:
313+
return target_tz in list_timezones()
314+
except SysCallError as err:
315+
debug(f'Failed to validate target timezone {target_tz}: {err}')
316+
return False
317+
318+
async def _confirm_locale_apply(self, rows: list[tuple[str, str]]) -> bool:
319+
"""Render and show the confirmation dialog for the locale changes."""
320+
label_w = max(len(label) for label, _ in rows)
321+
data_lines = [f' {label.ljust(label_w)} : {value}' for label, value in rows]
322+
323+
question = tr('Use this language as the target system language as well?')
324+
header = tr('The following settings will be applied:')
325+
326+
# The TUI centers every line of the prompt independently, so pad all
327+
# lines to a common width; otherwise the colon column drifts.
328+
width = max(len(question), len(header), *(len(line) for line in data_lines))
329+
separator = '=' * width
330+
prompt = question.ljust(width) + '\n\n' + header.ljust(width) + '\n' + separator + '\n' + '\n'.join(line.ljust(width) for line in data_lines) + '\n'
331+
332+
result = await Confirmation(header=prompt, preset=True).show()
333+
return result.type_ == ResultType.Selection and result.item() == MenuItem.yes()
334+
259335
def _prev_archinstall_language(self, item: MenuItem) -> str | None:
260336
if not item.value:
261337
return None

archinstall/lib/models/locale.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,36 @@
22
from typing import Any, Self
33

44
from archinstall.lib.locale.utils import get_kb_layout
5-
from archinstall.lib.translationhandler import tr
5+
from archinstall.lib.translationhandler import DEFAULT_CONSOLE_FONT, Language, tr
6+
7+
8+
@dataclass
9+
class LocaleLanguageDiff:
10+
"""Locale fields to write when applying a Language to a LocaleConfiguration.
11+
12+
Each field carries the new value, or None when no change is needed. sys_enc
13+
is paired with sys_lang so the encoding row is shown alongside the locale
14+
row in the confirmation dialog, even when the encoding portion itself does
15+
not change.
16+
"""
17+
18+
sys_lang: str | None = None
19+
sys_enc: str | None = None
20+
console_font: str | None = None
21+
22+
def is_empty(self) -> bool:
23+
return self.sys_lang is None and self.sys_enc is None and self.console_font is None
24+
25+
def labeled_rows(self) -> list[tuple[str, str]]:
26+
"""Return [(label, value)] for fields that would change."""
27+
rows: list[tuple[str, str]] = []
28+
if self.sys_lang is not None:
29+
rows.append((tr('Locale language'), self.sys_lang))
30+
if self.sys_enc is not None:
31+
rows.append((tr('Locale encoding'), self.sys_enc))
32+
if self.console_font is not None:
33+
rows.append((tr('Console font'), self.console_font))
34+
return rows
635

736

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

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

70+
def language_diff(self, language: Language) -> LocaleLanguageDiff:
71+
"""Compute the locale fields that would change if applying this language.
72+
73+
Returns an empty diff for languages without a sys_lang mapping. console_font
74+
is offered when the language-derived target value differs - so re-picking
75+
a language with fewer mappings still resets stale fonts left over from a
76+
previous pick.
77+
"""
78+
diff = LocaleLanguageDiff()
79+
if not language.sys_lang:
80+
return diff
81+
82+
if self.sys_lang != language.sys_lang:
83+
diff.sys_lang = language.sys_lang
84+
diff.sys_enc = language.target_sys_enc or self.sys_enc
85+
86+
target_font = language.target_console_font
87+
if self.console_font != target_font:
88+
diff.console_font = target_font
89+
90+
return diff
91+
92+
def apply_language_diff(self, diff: LocaleLanguageDiff) -> None:
93+
if diff.sys_lang is not None:
94+
self.sys_lang = diff.sys_lang
95+
if diff.sys_enc is not None:
96+
self.sys_enc = diff.sys_enc
97+
if diff.console_font is not None:
98+
self.console_font = diff.console_font
99+
41100
def _load_config(self, args: dict[str, str]) -> None:
42101
if 'sys_lang' in args:
43102
self.sys_lang = args['sys_lang']

archinstall/lib/translationhandler.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,31 @@ class Language:
2121
translation_percent: int
2222
translated_lang: str | None
2323
console_font: str | None = None
24+
sys_lang: str | None = None
25+
default_timezone: str | None = None
2426

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

32+
@property
33+
def target_sys_enc(self) -> str | None:
34+
"""Encoding portion of sys_lang (e.g. 'UTF-8' from 'uk_UA.UTF-8'). None when sys_lang has no '.'."""
35+
if self.sys_lang and '.' in self.sys_lang:
36+
return self.sys_lang.split('.', 1)[1]
37+
return None
38+
39+
@property
40+
def target_console_font(self) -> str:
41+
"""Console font implied by this language; falls back to the system default."""
42+
return self.console_font or DEFAULT_CONSOLE_FONT
43+
44+
@property
45+
def target_timezone(self) -> str:
46+
"""Timezone implied by this language; falls back to UTC."""
47+
return self.default_timezone or DEFAULT_TIMEZONE
48+
3049
def is_match(self, lang_or_translated_lang: str) -> bool:
3150
if self.name_en == lang_or_translated_lang:
3251
return True
@@ -38,7 +57,8 @@ def json(self) -> str:
3857
return self.name_en
3958

4059

41-
_DEFAULT_FONT = 'default8x16'
60+
DEFAULT_CONSOLE_FONT = 'default8x16'
61+
DEFAULT_TIMEZONE = 'UTC'
4262
_ENV_FONT = os.environ.get('FONT')
4363

4464

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

72-
target = font_name or _DEFAULT_FONT
92+
target = font_name or DEFAULT_CONSOLE_FONT
7393
try:
7494
SysCommand(['setfont', target])
7595
return True
@@ -132,6 +152,8 @@ def _get_translations(self) -> list[Language]:
132152
lang = mapping_entry['lang']
133153
translated_lang = mapping_entry.get('translated_lang', None)
134154
console_font = mapping_entry.get('console_font', None)
155+
sys_lang = mapping_entry.get('sys_lang', None)
156+
default_timezone = mapping_entry.get('default_timezone', None)
135157

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

149-
language = Language(abbr, lang, translation, percent, translated_lang, console_font)
171+
language = Language(abbr, lang, translation, percent, translated_lang, console_font, sys_lang, default_timezone)
150172
languages.append(language)
151173
except FileNotFoundError as err:
152174
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")

archinstall/locales/base.pot

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ msgstr ""
163163
msgid "Archinstall language"
164164
msgstr ""
165165

166+
msgid "Use this language as the target system language as well?"
167+
msgstr ""
168+
169+
msgid "The following settings will be applied:"
170+
msgstr ""
171+
166172
msgid "Wipe all selected drives and use a best-effort default partition layout"
167173
msgstr ""
168174

0 commit comments

Comments
 (0)