Merge master into lang-for-target-locale
Resolve the translationhandler.py conflict
This commit is contained in:
commit
1eef6754cd
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 "
|
||||
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
|
@ -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'),
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
Loading…
Reference in New Issue