Extract binding descriptions into base.pot and translate raw widget bindings (#4584)
xgettext only knew the tr keyword, so binding descriptions - translated at runtime via tr(b.description) in _translate_bindings() - never made it into base.pot. The strings added manually in #4363 were wiped by the regeneration in #4557. Teach the generator two more keywords: Binding:3 picks up our own binding definitions, and tr_noop marks strings that only exist inside the textual package, listed in the new tui/binding_descriptions.py. The list walks the full MRO of the textual classes used in components.py, since _translate_bindings() operates on the merged bindings map (e.g. Scroll Up comes from ScrollView and carries live translations in 18 locales today). Raw Input and Button widgets never went through _translate_bindings(), so their help-panel descriptions stayed English regardless of .po content - subclass them (_Input/_Button) following the _OptionList pattern to make those translations real. test_tooling/check_binding_descriptions.py verifies the list against the installed textual after upgrades (manual, not wired into CI).
This commit is contained in:
parent
919a435a6b
commit
d3ed8cd872
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@ msgstr ""
|
|||
msgid "Install to removable location"
|
||||
msgstr ""
|
||||
|
||||
msgid "Plymouth"
|
||||
msgstr ""
|
||||
|
||||
msgid "Will install to /EFI/BOOT/ (removable location, safe default)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -248,6 +251,9 @@ msgstr ""
|
|||
msgid "UEFI is not detected and some options are disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Plymouth theme"
|
||||
msgstr ""
|
||||
|
||||
msgid "The specified configuration will be applied"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -882,6 +888,10 @@ msgstr ""
|
|||
msgid "Removable"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Plymouth \"{}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Use a best-effort default partition layout"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1196,12 +1206,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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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