|
8 | 8 | from archinstall.lib.bootloader.utils import validate_bootloader_layout |
9 | 9 | from archinstall.lib.configuration import ConfigurationOutput, save_config |
10 | 10 | from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu |
| 11 | +from archinstall.lib.exceptions import SysCallError |
11 | 12 | from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone |
12 | 13 | from archinstall.lib.general.system_menu import select_kernel, select_swap |
13 | 14 | from archinstall.lib.hardware import SysInfo |
| 15 | +from archinstall.lib.locale import list_timezones |
14 | 16 | from archinstall.lib.locale.locale_menu import LocaleMenu |
15 | 17 | from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey |
| 18 | +from archinstall.lib.menu.helpers import Confirmation |
16 | 19 | from archinstall.lib.mirror.mirror_handler import MirrorListHandler |
17 | 20 | from archinstall.lib.mirror.mirror_menu import MirrorMenu |
18 | 21 | from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration |
|
27 | 30 | from archinstall.lib.models.pacman import PacmanConfiguration |
28 | 31 | from archinstall.lib.models.profile import ProfileConfiguration |
29 | 32 | from archinstall.lib.network.network_menu import select_network |
30 | | -from archinstall.lib.output import FormattedOutput |
| 33 | +from archinstall.lib.output import FormattedOutput, debug |
31 | 34 | from archinstall.lib.packages.packages import list_available_packages, select_additional_packages |
32 | 35 | from archinstall.lib.pacman.config import PacmanConfig |
33 | 36 | 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 |
35 | 38 | from archinstall.tui.ui.components import tui |
36 | 39 | from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup |
| 40 | +from archinstall.tui.ui.result import ResultType |
37 | 41 |
|
38 | 42 |
|
39 | 43 | class GlobalMenu(AbstractMenu[None]): |
@@ -160,7 +164,7 @@ def _get_menu_options(self) -> list[MenuItem]: |
160 | 164 | MenuItem( |
161 | 165 | text=tr('Timezone'), |
162 | 166 | action=select_timezone, |
163 | | - value='UTC', |
| 167 | + value=DEFAULT_TIMEZONE, |
164 | 168 | preview_action=self._prev_tz, |
165 | 169 | key='timezone', |
166 | 170 | ), |
@@ -254,8 +258,80 @@ async def _select_archinstall_language(self, preset: Language) -> Language: |
254 | 258 |
|
255 | 259 | self._update_lang_text() |
256 | 260 |
|
| 261 | + await self._maybe_apply_language_to_locale(language) |
| 262 | + |
257 | 263 | return language |
258 | 264 |
|
| 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 | + |
259 | 335 | def _prev_archinstall_language(self, item: MenuItem) -> str | None: |
260 | 336 | if not item.value: |
261 | 337 | return None |
|
0 commit comments