"""UI texts from `config/texts.yaml`, validated at startup so a missing key fails fast instead of mid-dialog.""" from pathlib import Path import yaml from pydantic import BaseModel, ConfigDict, field_validator from app.db.models import AlertKind class _Strict(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) class MenuTexts(_Strict): commit: str status: str logs: str alerts: str members: str back: str cancel: str placeholder: str class StartTexts(_Strict): welcome: str no_access: str invite_rejected: str class HelpTexts(_Strict): text: str class SectionTexts(_Strict): cancelled: str class MembersTexts(_Strict): title: str line: str never: str invite: str revoke: str confirm: str confirm_yes: str confirm_no: str revoked: str not_revoked: str invite_caption: str demo_invite: str demo_invite_caption: str revoke_guests: str confirm_guests: str guests_revoked: str class CommitTexts(_Strict): title: str empty: str page: str up: str prev: str next: str dir_button: str file_button: str new_file: str new_dir: str file_card: str last_commit: str no_commits: str view: str replace: str delete: str to_dir: str soon: str unavailable: str sent_as_file: str ask_file_name: str ask_dir_name: str bad_name: str exists: str protected: str ask_content: str too_big: str binary: str encoding: str syntax: str fix: str force: str added: str modified: str deleted: str draft_button: str draft_title: str draft_empty: str unstage: str draft_full: str in_draft_new: str in_draft_modified: str in_draft_deleted: str mark_new: str mark_modified: str mark_deleted: str op_add: str op_modify: str op_delete: str message_button: str message_current: str message_missing: str ask_message: str bad_message: str push_button: str commit_button: str edit_message: str push_confirm: str push_go: str pushing: str pushed: str conflict: str overwrite: str push_empty: str push_limit: str push_failed: str class PipelineTexts(_Strict): ci_pending: str ci_running: str ci_success: str ci_failed: str ci_timeout: str cd_waiting: str cd_running: str cd_success: str cd_failed: str cd_not_run: str cd_superseded: str notify_ci_ok: str notify_ci_failed: str notify_ci_timeout: str notify_cd_ok: str deploy_failed: str removed: str log_file: str class StatusTexts(_Strict): title: str line: str empty: str unavailable: str card: str none: str no_limit: str check_button: str refresh: str to_list: str to_card: str checking: str report_title: str report_container: str report_line: str check_running: str check_health: str check_restarts: str check_cpu: str check_mem: str check_log_errors: str check_http: str days: str hours: str minutes: str seconds: str restart: str restarted: str restart_failed: str class LogsTexts(_Strict): title: str empty: str unavailable: str container: str tail: str since: str minutes: str hours: str grep_button: str agent_button: str agent_ask: str agent_off: str agent_thinking: str agent_refused: str agent_quota: str agent_unavailable: str agent_not_understood: str head_agent: str to_list: str to_container: str ask_grep: str head_tail: str head_grep: str nothing: str truncated: str partial: str skipped: str sent_as_file: str err_empty: str err_quotes: str err_flag: str err_context: str err_extra: str err_too_long: str err_regex: str class AlertsTexts(_Strict): kinds: dict[AlertKind, str] details: dict[AlertKind, str] message: str repeats: str resolved: str logs_button: str status_button: str list_button: str list_title: str list_empty: str list_line: str active_mark: str resolved_mark: str page: str prev: str next: str card: str ongoing: str to_list: str host: str gone: str @field_validator("kinds", "details") @classmethod def _every_kind(cls, value: dict[AlertKind, str]) -> dict[AlertKind, str]: missing = set(AlertKind) - set(value) if missing: raise ValueError(f"no text for {sorted(missing)}") return value class Texts(_Strict): menu: MenuTexts start: StartTexts help: HelpTexts section: SectionTexts members: MembersTexts commit: CommitTexts pipeline: PipelineTexts status: StatusTexts logs: LogsTexts alerts: AlertsTexts def load_texts(config_dir: Path) -> Texts: with (config_dir / "texts.yaml").open(encoding="utf-8") as f: return Texts.model_validate(yaml.safe_load(f))