Merge master into lang-for-target-locale

Resolve the translationhandler.py conflict
This commit is contained in:
Softer 2026-07-23 11:28:14 +03:00
commit 1eef6754cd
35 changed files with 2716 additions and 2206 deletions

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pacman --noconfirm -Syu bandit
- name: Security checkup with Bandit
run: bandit -r archinstall || exit 0

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -21,8 +21,8 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
- name: Install pre-dependencies
run: |
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel

View File

@ -26,7 +26,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pwd
- run: find .
- run: cat /etc/os-release

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -6,7 +6,7 @@ jobs:
container:
image: archlinux/archlinux:latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -7,7 +7,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -11,7 +11,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -18,7 +18,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Prepare arch
run: |
pacman-key --init

View File

@ -4,6 +4,6 @@ jobs:
ruff_format_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
- run: ruff format --diff

View File

@ -4,5 +4,5 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0

View File

@ -15,7 +15,7 @@ jobs:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks

View File

@ -26,7 +26,7 @@ jobs:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- run: pwd
- run: find .
- run: cat /etc/os-release

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.21
rev: v0.15.22
hooks:
# fix unused imports and sort them
- id: ruff

View File

@ -35,7 +35,6 @@ from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.lib.utils.util import is_valid_path
from archinstall.lib.version import get_version
from archinstall.tui.components import tui
@ -404,7 +403,7 @@ class ArchConfig:
) -> None:
save_path = dest_path or DEFAULT_SAVE_PATH
if not is_valid_path(save_path):
if not save_path.is_dir():
warn(
f'Destination directory {save_path} does not exist or is not a directory\n.',
'Configuration files can not be saved',
@ -416,7 +415,7 @@ class ArchConfig:
self.save_user_creds(save_path, password=password)
def save_user_config(self, dest_path: Path) -> None:
if not is_valid_path(dest_path):
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User configuration could not be saved.')
return
@ -430,7 +429,7 @@ class ArchConfig:
dest_path: Path,
password: str | None = None,
) -> None:
if not is_valid_path(dest_path):
if not dest_path.is_dir():
error(f'Invalid path {dest_path}. User credentials could not be saved.')
return

View File

@ -1,8 +1,9 @@
from pathlib import Path
from subprocess import CalledProcessError
from pydantic import BaseModel
from archinstall.lib.command import SysCommand
from archinstall.lib.command import SysCommand, run
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo
@ -29,19 +30,18 @@ def _fetch_lsblk_info(
cmd.append(str(dev_path))
try:
worker = SysCommand(cmd)
except SysCallError as err:
result = run(cmd)
except CalledProcessError as err:
# Get the output minus the message/info from lsblk if it returns a non-zero exit code.
if err.worker_log:
debug(f'Error calling lsblk: {err.worker_log.decode()}')
if stdout := err.stdout:
debug(f'Error calling lsblk: {stdout.decode().rstrip()}')
if dev_path:
raise DiskError(f'Failed to read disk "{dev_path}" with lsblk')
raise err
output = worker.output(remove_cr=False)
return LsblkOutput.model_validate_json(output)
return LsblkOutput.model_validate_json(result.stdout)
def get_lsblk_info(

View File

@ -660,16 +660,12 @@ class Installer:
# in the first column of the entry; check for both cases.
entry_re = re.compile(rf'#{lang}(\.{encoding})?{modifier} {encoding}')
lang_value = None
for index, line in enumerate(locale_gen_lines):
if entry_re.match(line):
uncommented_line = line.removeprefix('#')
locale_gen_lines[index] = uncommented_line
locale_gen_lines[index] = line.removeprefix('#')
locale_gen.write_text(''.join(locale_gen_lines))
lang_value = uncommented_line.split()[0]
break
if lang_value is None:
else:
error(f"Invalid locale: language '{locale_config.sys_lang}', encoding '{locale_config.sys_enc}'")
return False
@ -679,7 +675,7 @@ class Installer:
error(f'Failed to run locale-gen on target: {e}')
return False
(self.target / 'etc/locale.conf').write_text(f'LANG={lang_value}\n')
(self.target / 'etc/locale.conf').write_text(f'LANG={lang}.{encoding}{modifier}\n')
return True
def set_timezone(self, zone: str) -> bool:
@ -1480,20 +1476,20 @@ class Installer:
parent_dev_path = get_parent_device_path(efi_partition.safe_dev_path)
efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI'
efi_dir_path_target = efi_partition.mountpoint / 'EFI'
if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT'
else:
efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True)
try:
efi_dir_path = self.target / efi_partition.mountpoint.relative_to('/') / 'EFI'
efi_dir_path_target = efi_partition.mountpoint / 'EFI'
if bootloader_removable:
efi_dir_path = efi_dir_path / 'BOOT'
efi_dir_path_target = efi_dir_path_target / 'BOOT'
else:
efi_dir_path = efi_dir_path / 'arch-limine'
efi_dir_path_target = efi_dir_path_target / 'arch-limine'
config_path = efi_dir_path / 'limine.conf'
efi_dir_path.mkdir(parents=True, exist_ok=True)
for file in ('BOOTIA32.EFI', 'BOOTX64.EFI'):
(limine_path / file).copy_into(efi_dir_path)
except Exception as err:

View File

@ -58,7 +58,7 @@ class WifiHandler(InstanceRunnable[bool]):
async def _enable_supplicant(self, wifi_iface: str) -> bool:
self._wpa_config.load_config()
result = self._wpa_cli('status') # if it it's running it will blow up
result = self._wpa_cli('status')
if result.success:
debug('wpa_supplicant already running')
@ -72,18 +72,19 @@ class WifiHandler(InstanceRunnable[bool]):
try:
SysCommand(f'wpa_supplicant -B -i {wifi_iface} -c {self._wpa_config.config_file}')
result = self._wpa_cli('status') # if it it's running it will blow up
if result.success:
debug('successfully enabled wpa_supplicant')
return True
else:
debug(f'failed to enable wpa_supplicant: {result.error}')
return False
except SysCallError as err:
debug(f'failed to enable wpa_supplicant: {err}')
return False
result = self._wpa_cli('status')
if result.success:
debug('successfully enabled wpa_supplicant')
return True
else:
debug(f'failed to enable wpa_supplicant: {result.error}')
return False
def _find_wifi_interface(self) -> str | None:
net_path = Path('/sys/class/net')
@ -197,15 +198,6 @@ class WifiHandler(InstanceRunnable[bool]):
try:
result = SysCommand(cmd).decode()
if 'FAIL' in result:
debug(f'wpa_cli returned FAIL: {result}')
return WpaCliResult(
success=False,
error=f'wpa_cli returned a failure: {result}',
)
return WpaCliResult(success=True, response=result)
except SysCallError as err:
debug(f'error running wpa_cli command: {err}')
return WpaCliResult(
@ -213,6 +205,15 @@ class WifiHandler(InstanceRunnable[bool]):
error=f'Error running wpa_cli command: {err}',
)
if 'FAIL' in result:
debug(f'wpa_cli returned FAIL: {result}')
return WpaCliResult(
success=False,
error=f'wpa_cli returned a failure: {result}',
)
return WpaCliResult(success=True, response=result)
def _find_network_id(self, ssid: str, iface: str) -> int | None:
result = self._wpa_cli('list_networks', iface)
@ -254,18 +255,18 @@ class WifiHandler(InstanceRunnable[bool]):
try:
result = self._wpa_cli('scan_results', iface)
if not result.success:
debug(f'Failed to retrieve scan results: {result.error}')
return []
if not result.response:
debug('No wifi networks found')
return []
networks = WifiNetwork.from_wpa(result.response)
return networks
except SysCallError as err:
debug('Unable to retrieve wifi results')
raise err
if not result.success:
debug(f'Failed to retrieve scan results: {result.error}')
return []
if not result.response:
debug('No wifi networks found')
return []
networks = WifiNetwork.from_wpa(result.response)
return networks

View File

@ -258,6 +258,10 @@ class ProfileHandler:
"""
try:
data = fetch_data_from_url(url)
except ValueError:
err = tr('Unable to fetch profile from specified url: {}').format(url)
error(err)
else:
b_data = bytes(data, 'utf-8')
with NamedTemporaryFile(delete=False, suffix='.py') as fp:
@ -266,10 +270,6 @@ class ProfileHandler:
profiles = self._process_profile_file(filepath)
self.remove_custom_profiles(profiles)
self.add_custom_profiles(profiles)
except ValueError:
err = tr('Unable to fetch profile from specified url: {}').format(url)
error(err)
def _load_profile_class(self, module: ModuleType) -> list[Profile]:
"""

View File

@ -102,13 +102,14 @@ class TranslationHandler:
if not running_from_iso():
return
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
os.close(font_fd)
os.close(cmap_fd)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
try:
font_fd, font_path = tempfile.mkstemp(prefix='archinstall_font_')
cmap_fd, cmap_path = tempfile.mkstemp(prefix='archinstall_cmap_')
os.close(font_fd)
os.close(cmap_fd)
self._font_backup = Path(font_path)
self._cmap_backup = Path(cmap_path)
SysCommand(['setfont', '-O', str(self._font_backup), '-om', str(self._cmap_backup)])
except SysCallError as err:
debug(f'Failed to save console font: {err}')
@ -158,21 +159,21 @@ class TranslationHandler:
try:
# get a translation for a specific language
translation = gettext.translation('base', localedir=self._get_locales_dir(), languages=(abbr, lang))
# calculate the percentage of total translated text to total number of messages
if abbr == 'en':
percent = 100
else:
num_translations = self._get_catalog_size(translation)
percent = int((num_translations / self._total_messages) * 100)
# 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, sys_lang, default_timezone)
languages.append(language)
except FileNotFoundError as err:
raise FileNotFoundError(f"Could not locate language file for '{lang}': {err}")
# calculate the percentage of total translated text to total number of messages
if abbr == 'en':
percent = 100
else:
num_translations = self._get_catalog_size(translation)
percent = int((num_translations / self._total_messages) * 100)
# 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, sys_lang, default_timezone)
languages.append(language)
return languages
def _load_language_mappings(self) -> list[dict[str, str]]:
@ -299,6 +300,15 @@ def tr(message: str) -> str:
return str(_DeferredTranslation(message))
def tr_noop(message: str) -> str:
"""Mark a string for xgettext extraction without translating it here.
Use for strings that are translated later from a variable, e.g.
binding descriptions passed through tr() at runtime.
"""
return message
builtins._ = _DeferredTranslation # type: ignore[attr-defined]

View File

@ -9,6 +9,14 @@ if TYPE_CHECKING:
from _typeshed import DataclassInstance
def _sentence_case(text: str) -> str:
# Only capitalize the first letter of the label. The source strings and
# their translations are already written with the correct casing for each
# language, so title-casing every word is wrong outside English (it turned
# "Ім'я хоста" into "Ім'Я Хоста" and "(NTP)" into "(Ntp)").
return text[:1].upper() + text[1:]
def as_key_value_pair(
entries: dict[str, str | list[str] | bool],
ignore_empty: bool = True,
@ -33,28 +41,11 @@ def as_key_value_pair(
if isinstance(value, list):
value = '\n '.join(str(val) for val in value)
table.add_row(label.title(), f': {value}')
table.add_row(_sentence_case(label), f': {value}')
return table.stringify()
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks: list[list[str]] = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]

View File

@ -1,10 +1,8 @@
import secrets
import string
from datetime import UTC, datetime
from pathlib import Path
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from archinstall.lib.utils.format import as_columns
def timestamp() -> str:
@ -25,29 +23,3 @@ def running_from_iso() -> bool:
def generate_password(length: int = 64) -> str:
haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace
return ''.join(secrets.choice(haystack) for _ in range(length))
def format_cols(items: list[str], header: str | None = None) -> str:
if header:
text = f'{header}:\n'
else:
text = ''
nr_items = len(items)
if nr_items <= 4:
col = 1
elif nr_items <= 8:
col = 2
elif nr_items <= 12:
col = 3
else:
col = 4
text += as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join(t.strip() for t in text.split('\n'))
return text
def is_valid_path(path: Path) -> bool:
return path.exists() and path.is_dir()

View File

@ -58,3 +58,17 @@ msgstr "Wollen sie wirklich abbrechen?"
After the translations have been written, run the script once more `./locales_generator.sh <lang_abbr>` and it will auto-generate the `base.mo` file with the included translations.
After that you're all ready to go and enjoy Archinstall in the new language :)
## Key binding descriptions
`base.po` also contains the key binding descriptions shown in the footer and the help panel, for example `Scroll Up` or `Toggle option`. Translate them like any other string, but keep them short so they still fit in the footer.
Most of them come from the `textual` library, where `xgettext` cannot see them, so they are listed with `tr_noop` in `archinstall/tui/binding_descriptions.py`, a module that exists only to make them extractable and is never imported at runtime. Archinstall's own descriptions are taken from the third positional argument of `Binding()`, so that argument must never be passed as a keyword.
The list has to be verified after a `textual` upgrade, or after a new textual widget is introduced in `archinstall/tui/components.py` (the widget also has to be added to `TEXTUAL_CLASSES` in the checker). Nothing in CI checks it, so run the following from the repository root
```
python3 test_tooling/check_binding_descriptions.py
```
It prints every missing or stale entry, and an `OK` line when the list matches the installed `textual`. Fix `binding_descriptions.py` until it passes, then run `./locales_generator.sh <lang_abbr>` again.

View File

@ -344,6 +344,10 @@ msgstr ""
msgid "Iteration time"
msgstr ""
msgid ""
"No disks were detected. A disk is required to be able to install Arch Linux"
msgstr ""
msgid "Select disks for the installation"
msgstr ""
@ -1226,12 +1230,192 @@ msgstr ""
msgid "Starting device modifications in "
msgstr ""
msgid "Bottom"
msgstr ""
msgid "Copy selected text"
msgstr ""
msgid "Cursor down"
msgstr ""
msgid "Cursor left"
msgstr ""
msgid "Cursor right"
msgstr ""
msgid "Cursor up"
msgstr ""
msgid "Cut selected text"
msgstr ""
msgid "Delete all to the left"
msgstr ""
msgid "Delete all to the right"
msgstr ""
msgid "Delete character left"
msgstr ""
msgid "Delete character right"
msgstr ""
msgid "Delete left to start of word"
msgstr ""
msgid "Delete right to start of word"
msgstr ""
msgid "Down"
msgstr ""
msgid "End"
msgstr ""
msgid "First"
msgstr ""
msgid "Focus Next"
msgstr ""
msgid "Focus Previous"
msgstr ""
msgid "Go to end"
msgstr ""
msgid "Go to start"
msgstr ""
msgid "Home"
msgstr ""
msgid "Last"
msgstr ""
msgid "Move cursor left"
msgstr ""
msgid "Move cursor left a word"
msgstr ""
msgid "Move cursor left a word and select"
msgstr ""
msgid "Move cursor left and select"
msgstr ""
msgid "Move cursor right a word"
msgstr ""
msgid "Move cursor right a word and select"
msgstr ""
msgid "Move cursor right and select"
msgstr ""
msgid "Move cursor right or accept the completion suggestion"
msgstr ""
msgid "Page Down"
msgstr ""
msgid "Page Left"
msgstr ""
msgid "Page Right"
msgstr ""
msgid "Page Up"
msgstr ""
msgid "Page down"
msgstr ""
msgid "Page up"
msgstr ""
msgid "Paste text from the clipboard"
msgstr ""
msgid "Press button"
msgstr ""
msgid "Quit"
msgstr ""
msgid "Scroll Down"
msgstr ""
msgid "Scroll End"
msgstr ""
msgid "Scroll Home"
msgstr ""
msgid "Scroll Left"
msgstr ""
msgid "Scroll Right"
msgstr ""
msgid "Scroll Up"
msgstr ""
msgid "Select"
msgstr ""
msgid "Select all"
msgstr ""
msgid "Select line end"
msgstr ""
msgid "Select line start"
msgstr ""
msgid "Submit"
msgstr ""
msgid "Toggle option"
msgstr ""
msgid "Top"
msgstr ""
msgid "Up"
msgstr ""
msgid "Reset"
msgstr ""
msgid "Search"
msgstr ""
msgid "Toggle"
msgstr ""
msgid "Confirm"
msgstr ""
msgid "Focus right"
msgstr ""
msgid "Focus left"
msgstr ""
msgid "Ok"
msgstr ""
msgid "Input cannot be empty"
msgstr ""
msgid "Show/Hide help"
msgstr ""
msgid "Yes"
msgstr ""

View File

@ -221,12 +221,27 @@ msgstr "Imágenes del núcleo unificadas"
msgid "Install to removable location"
msgstr "Instalar a ubicación desmontable"
msgid "Plymouth"
msgstr "Plymouth"
msgid "Will install to /EFI/BOOT/ (removable location, safe default)"
msgstr "Instalará a /EFI/BOOT/ (ubicación desmontable, segura por defecto)"
msgid "Will install to custom location with NVRAM entry"
msgstr "Instalará a ubicación personalizada con entrada NVRAM"
msgid "Plymouth adds a cosmetic boot splash but can cause boot problems on some setups:"
msgstr "Plymouth añade una pantalla de inicio cosmética, pero puede causar problemas de arranque en algunas configuraciones:"
msgid "black screen with the NVIDIA driver"
msgstr "pantalla negra con el controlador NVIDIA"
msgid "hidden password prompt with disk encryption (LUKS)"
msgstr "solicitud de contraseña oculta con cifrado de disco (LUKS)"
msgid "Would you like to enable it?"
msgstr "¿Desea habilitarlo?"
msgid "Would you like to use unified kernel images?"
msgstr "¿Le gustaría utilizar imágenes del núcleo unificadas?"
@ -254,6 +269,9 @@ msgstr "Seleccione el gestor de arranque que desea instalar"
msgid "UEFI is not detected and some options are disabled"
msgstr "No se detecta UEFI y algunas opciones están desactivadas"
msgid "Select Plymouth theme"
msgstr "Seleccionar tema Plymouth"
msgid "The specified configuration will be applied"
msgstr "Se aplicará la configuración especificada"
@ -263,9 +281,6 @@ msgstr "¿Quiere continuar?"
msgid "Configuration preview"
msgstr "Vista previa de la configuración"
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Advertencia: no se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
msgid "No configuration"
msgstr "Sin configuración"
@ -332,6 +347,9 @@ msgstr "Contraseña"
msgid "Iteration time"
msgstr "Tiempo de iteración"
msgid "No disks were detected. A disk is required to be able to install Arch Linux"
msgstr "No se detectaron discos. Se requiere un disco para poder instalar Arch Linux"
msgid "Select disks for the installation"
msgstr "Seleccione discos para la instalación"
@ -627,16 +645,21 @@ msgstr "Color"
msgid "Kernel"
msgstr "Núcleo"
msgid "Missing configurations:\n"
msgstr "Configuraciones que faltan:\n"
msgid "No network configuration selected. Network will need to be set up manually on the installed system."
msgstr "No se ha seleccionado ninguna configuración de red. La red deberá configurarse manualmente en el sistema instalado."
#, python-brace-format
msgid "Invalid configuration: {}"
msgstr "Configuración no válida: {}"
msgid "Missing configurations:"
msgstr "Configuraciones faltantes:"
msgid "Invalid configuration:"
msgstr "Configuración no válida:"
msgid "Ready to install"
msgstr "Listo para instalar"
msgid "Warnings:"
msgstr "Advertencias:"
msgid "Profiles"
msgstr "Perfiles"
@ -858,6 +881,10 @@ msgstr "UKI activado"
msgid "Removable"
msgstr "Desmontable"
#, python-brace-format
msgid "Plymouth \"{}\""
msgstr "Plymouth «{}»"
msgid "Use a best-effort default partition layout"
msgstr "Utlizar un diseño de partición predeterminado de mejor esfuerzo"
@ -965,8 +992,8 @@ msgstr "Grupo de paquetes:"
msgid "Color enabled"
msgstr "Color activado"
#, python-brace-format
msgid "{} grphics driver"
#, fuzzy, python-brace-format
msgid "{} graphics driver"
msgstr "{} controlador de gráficos"
#, python-brace-format
@ -1032,15 +1059,15 @@ msgstr "¿Desea conectarse al Wi-Fi?"
msgid "No wifi interface found"
msgstr "No se encontró ninguna interfaz Wi-Fi"
msgid "No wifi networks found"
msgstr "No se encontró redes Wi-Fi"
msgid "Select wifi network to connect to"
msgstr "Seleccione una red Wi-Fi para conectarse"
msgid "Scanning wifi networks..."
msgstr "Buscando redes Wi-Fi..."
msgid "No wifi networks found"
msgstr "No se encontró redes Wi-Fi"
msgid "Failed setting up wifi"
msgstr "Error al configurar el Wi-Fi"
@ -1608,9 +1635,6 @@ msgstr " (predeterminado)"
#~ msgid "Subvolume name :"
#~ msgstr "Nombre del subvolumen :"
#~ msgid "Select a mount point :"
#~ msgstr "Seleccione un punto de montaje :"
#~ msgid "Select the desired subvolume options "
#~ msgstr "Seleccione las opciones de subvolumen deseadas "

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,14 @@ usage() {
}
generate_pot() {
# tr - regular translation calls
# tr_noop - extraction-only marker for strings translated later from a variable
# Binding:3 - the description argument of textual Binding() definitions,
# translated at runtime by _translate_bindings(). The description
# must be passed positionally - xgettext cannot see keyword arguments.
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
| xargs xgettext --no-location --omit-header \
--keyword='tr' --keyword='tr_noop' --keyword='Binding:3' \
-d base -o locales/base.pot
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,77 @@
"""Textual's built-in key binding descriptions, listed for xgettext.
These strings live inside the textual package, so xgettext cannot find them
when scanning this code base. At runtime _translate_bindings() in
components.py translates every binding description with tr(), including the
ones inherited from textual widgets - this module only makes those strings
visible to the extraction in locales_generator.sh (via the tr_noop keyword).
The list covers the MRO-merged bindings of the textual classes used in
components.py: _translate_bindings() operates on the merged bindings map, so
descriptions inherited from ancestors (e.g. Scroll Up from ScrollView) are
translated at runtime too and must be listed here.
Verify or regenerate the list after a textual upgrade or after introducing
a new textual widget in components.py:
python3 test_tooling/check_binding_descriptions.py
"""
from archinstall.lib.translationhandler import tr_noop
# textual 8.2.8
TEXTUAL_BINDING_DESCRIPTIONS: tuple[str, ...] = (
tr_noop('Bottom'),
tr_noop('Copy selected text'),
tr_noop('Cursor down'),
tr_noop('Cursor left'),
tr_noop('Cursor right'),
tr_noop('Cursor up'),
tr_noop('Cut selected text'),
tr_noop('Delete all to the left'),
tr_noop('Delete all to the right'),
tr_noop('Delete character left'),
tr_noop('Delete character right'),
tr_noop('Delete left to start of word'),
tr_noop('Delete right to start of word'),
tr_noop('Down'),
tr_noop('End'),
tr_noop('First'),
tr_noop('Focus Next'),
tr_noop('Focus Previous'),
tr_noop('Go to end'),
tr_noop('Go to start'),
tr_noop('Home'),
tr_noop('Last'),
tr_noop('Move cursor left'),
tr_noop('Move cursor left a word'),
tr_noop('Move cursor left a word and select'),
tr_noop('Move cursor left and select'),
tr_noop('Move cursor right a word'),
tr_noop('Move cursor right a word and select'),
tr_noop('Move cursor right and select'),
tr_noop('Move cursor right or accept the completion suggestion'),
tr_noop('Page Down'),
tr_noop('Page Left'),
tr_noop('Page Right'),
tr_noop('Page Up'),
tr_noop('Page down'),
tr_noop('Page up'),
tr_noop('Paste text from the clipboard'),
tr_noop('Press button'),
tr_noop('Quit'),
tr_noop('Scroll Down'),
tr_noop('Scroll End'),
tr_noop('Scroll Home'),
tr_noop('Scroll Left'),
tr_noop('Scroll Right'),
tr_noop('Scroll Up'),
tr_noop('Select'),
tr_noop('Select all'),
tr_noop('Select line end'),
tr_noop('Select line start'),
tr_noop('Submit'),
tr_noop('Toggle option'),
tr_noop('Top'),
tr_noop('Up'),
)

View File

@ -150,6 +150,19 @@ class LoadingScreen(BaseScreen[ValueT]):
_ = self.dismiss()
class _Input(Input):
@override
def on_mount(self) -> None:
_translate_bindings(self._merged_bindings, self._bindings)
class _Button(Button):
# no @override: Button has no on_mount in its MRO,
# textual dispatches the handler by naming convention
def on_mount(self) -> None:
_translate_bindings(self._merged_bindings, self._bindings)
class _OptionList(OptionList):
BINDINGS: ClassVar = [
Binding('down', 'cursor_down', 'Down', show=True),
@ -309,7 +322,7 @@ class OptionListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
yield _Input(placeholder='/filter', id='filter-input')
yield Footer()
@ -540,7 +553,7 @@ class SelectListScreen(BaseScreen[ValueT]):
yield ScrollableContainer(preview_label)
if self._filter:
yield Input(placeholder='/filter', id='filter-input')
yield _Input(placeholder='/filter', id='filter-input')
yield Footer()
@ -702,12 +715,12 @@ class ConfirmationScreen(BaseScreen[ValueT]):
with Vertical(classes='content-container'):
with Horizontal(classes='buttons-container'):
for item in self._group.items:
yield Button(item.text, id=item.key)
yield _Button(item.text, id=item.key)
else:
with Vertical():
with Horizontal(classes='buttons-container'):
for item in self._group.items:
yield Button(item.text, id=item.key)
yield _Button(item.text, id=item.key)
yield Rule(orientation='horizontal')
if self._preview_header is not None:
@ -848,7 +861,7 @@ class InputScreen(BaseScreen[str]):
with Center(classes='container-wrapper'):
with Vertical(classes='input-content'):
yield Input(
yield _Input(
placeholder=self._placeholder,
password=self._password,
value=self._default_value,

View File

@ -22,7 +22,7 @@ dependencies = [
"pydantic==2.13.4",
"cryptography==49.0.0",
"textual==8.2.8",
"markdown-it-py==4.0.0",
"markdown-it-py==4.2.0",
"linkify-it-py==2.1.0",
]
@ -36,8 +36,8 @@ log = ["systemd_python==236"]
dev = [
"mypy==2.3.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.21",
"pre-commit==4.6.1",
"ruff==0.15.22",
"pylint==4.0.6",
"pytest==9.1.1",
"hypothesis>=6.152.4",

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Verify archinstall/tui/binding_descriptions.py against the installed textual.
Run manually after a textual upgrade or after introducing a new textual
widget in archinstall/tui/components.py:
python3 test_tooling/check_binding_descriptions.py
The script parses the tr_noop() entries from binding_descriptions.py (no
archinstall import needed) and compares them with the MRO-merged binding
descriptions of the textual classes used in components.py. Exits non-zero
on any difference and prints the exact missing/stale entries.
"""
import re
import sys
from pathlib import Path
from textual.app import App
from textual.screen import Screen
from textual.widgets import Button, DataTable, HelpPanel, Input, OptionList, SelectionList
# Must mirror the textual widget classes used in archinstall/tui/components.py.
# Extend when a new widget is introduced there.
TEXTUAL_CLASSES = (App, Screen, Button, DataTable, HelpPanel, Input, OptionList, SelectionList)
MODULE_PATH = Path(__file__).parent.parent / 'archinstall/tui/binding_descriptions.py'
def live_descriptions() -> set[str]:
descriptions: set[str] = set()
# Walk the full MRO: _translate_bindings() operates on the merged bindings
# map, so descriptions inherited from ancestors are translated too.
for cls in TEXTUAL_CLASSES:
for klass in cls.__mro__:
for binding in klass.__dict__.get('BINDINGS', []):
description = getattr(binding, 'description', None)
if not description and isinstance(binding, tuple) and len(binding) > 2:
description = binding[2]
if description:
descriptions.add(str(description))
return descriptions
def listed_descriptions() -> set[str]:
content = MODULE_PATH.read_text(encoding='utf-8')
return set(re.findall(r"tr_noop\('([^']+)'\)", content))
def main() -> int:
live = live_descriptions()
listed = listed_descriptions()
if live == listed:
print(f'OK: {len(listed)} descriptions in sync with installed textual')
return 0
for description in sorted(live - listed):
print(f'missing from binding_descriptions.py: {description}')
for description in sorted(listed - live):
print(f'stale in binding_descriptions.py: {description}')
return 1
if __name__ == '__main__':
sys.exit(main())