feat: locale-aware command templates, debounced auto-sync, entity pickers

- Locale-aware templates: CommandTemplateSlot now has a locale column,
  allowing each slot to have per-language variants (EN/RU). Templates
  are resolved at runtime from the Telegram user's language_code.

- Merged system configs: "Default Commands (EN)" and "(RU)" merged
  into a single "Default Commands" config with locale-aware slots.
  Migration handles existing data automatically.

- Configurable command descriptions: hardcoded COMMAND_DESCRIPTIONS
  replaced with desc_* template slots (desc_status, desc_help, etc.)
  that users can customize per locale. setMyCommands registers all
  locales explicitly.

- Removed locale from CommandConfig: no longer needed since locale
  is derived from the Telegram user's language at runtime.

- Debounced command auto-sync: after command config/tracker changes,
  affected bots are marked dirty and synced after a 30s debounce
  window. Manual "Sync with Telegram" button still works.

- Entity pickers in LinkedTargetsSection: replaced 6 plain <select>
  elements with EntitySelect components (search, icons, keyboard nav).
  Added onselect callback and size="sm" props to EntitySelect.
This commit is contained in:
2026-03-22 03:14:51 +03:00
parent 751097b347
commit 1167d138a3
47 changed files with 604 additions and 230 deletions
@@ -62,6 +62,20 @@ IMMICH_CAPABILITIES = ProviderCapabilities(
{"name": "memory", "description": "/memory On This Day photos"},
{"name": "rate_limited", "description": "Rate limit warning message"},
{"name": "no_results", "description": "Empty results fallback"},
{"name": "desc_status", "description": "Menu description for /status"},
{"name": "desc_albums", "description": "Menu description for /albums"},
{"name": "desc_events", "description": "Menu description for /events"},
{"name": "desc_summary", "description": "Menu description for /summary"},
{"name": "desc_latest", "description": "Menu description for /latest"},
{"name": "desc_memory", "description": "Menu description for /memory"},
{"name": "desc_random", "description": "Menu description for /random"},
{"name": "desc_search", "description": "Menu description for /search"},
{"name": "desc_find", "description": "Menu description for /find"},
{"name": "desc_person", "description": "Menu description for /person"},
{"name": "desc_place", "description": "Menu description for /place"},
{"name": "desc_favorites", "description": "Menu description for /favorites"},
{"name": "desc_people", "description": "Menu description for /people"},
{"name": "desc_help", "description": "Menu description for /help"},
],
events=[
{"name": "assets_added", "description": "New assets detected in album"},
@@ -0,0 +1 @@
List tracked albums
@@ -0,0 +1 @@
Show recent events
@@ -0,0 +1 @@
Search by filename
@@ -0,0 +1 @@
Show available commands
@@ -0,0 +1 @@
Show latest photos
@@ -0,0 +1 @@
On This Day memories
@@ -0,0 +1 @@
List detected people
@@ -0,0 +1 @@
Find photos of person
@@ -0,0 +1 @@
Find photos by location
@@ -0,0 +1 @@
Send random photo
@@ -0,0 +1 @@
Smart search (AI)
@@ -0,0 +1 @@
Show tracker status
@@ -0,0 +1 @@
Send album summary now
@@ -7,13 +7,24 @@ _LOGGER = logging.getLogger(__name__)
_DEFAULTS_DIR = Path(__file__).parent
# All command template slot names (file stem = slot name)
# Response template slot names (file stem = slot name)
COMMAND_SLOT_NAMES = [
"start", "help", "status", "albums", "events", "people",
"search", "latest", "favorites", "random", "summary", "memory",
"rate_limited", "no_results",
]
# Description slots for Telegram command menu (desc_{cmd} -> short text)
COMMAND_DESC_SLOT_NAMES = [
"desc_status", "desc_albums", "desc_events", "desc_summary",
"desc_latest", "desc_memory", "desc_random", "desc_search",
"desc_find", "desc_person", "desc_place", "desc_favorites",
"desc_people", "desc_help",
]
# All slot names (response + description)
ALL_SLOT_NAMES = COMMAND_SLOT_NAMES + COMMAND_DESC_SLOT_NAMES
def load_default_command_templates(locale: str = "en") -> dict[str, str]:
"""Load default command template strings for a locale.
@@ -26,7 +37,7 @@ def load_default_command_templates(locale: str = "en") -> dict[str, str]:
return {}
templates: dict[str, str] = {}
for slot_name in COMMAND_SLOT_NAMES:
for slot_name in ALL_SLOT_NAMES:
filepath = locale_dir / f"{slot_name}.jinja2"
if filepath.exists():
templates[slot_name] = filepath.read_text(encoding="utf-8").strip()
@@ -0,0 +1 @@
Список отслеживаемых альбомов
@@ -0,0 +1 @@
Показать последние события
@@ -0,0 +1 @@
Показать избранное
@@ -0,0 +1 @@
Поиск по имени файла
@@ -0,0 +1 @@
Показать доступные команды
@@ -0,0 +1 @@
Показать последние фото
@@ -0,0 +1 @@
Воспоминания за этот день
@@ -0,0 +1 @@
Список людей
@@ -0,0 +1 @@
Найти фото человека
@@ -0,0 +1 @@
Найти фото по месту
@@ -0,0 +1 @@
Отправить случайное фото
@@ -0,0 +1 @@
Умный поиск (AI)
@@ -0,0 +1 @@
Показать статус трекеров
@@ -0,0 +1 @@
Отправить сводку альбомов