This commit is contained in:
Softer 2026-07-14 10:46:18 +03:00 committed by GitHub
commit 3f801081db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 357 additions and 6 deletions

View File

@ -277,6 +277,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

@ -1220,12 +1220,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

@ -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
}

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.7
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

@ -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())