feat(i18n): every server refusal is said by a record the contract holds

A refusal code or reason with no sentence fell through to the generic
"couldn't carry out that action" unnoticed: the parity test is green when
EN and HU miss the same key. One did (an edit refused for a file that is
not video or audio).

- lib/api/refusalCodes.ts: which sentence answers each code, as data;
  localizeDetail says a refusal through it, unchanged for every reader.
- backendIds contract: every detail code the backend sends is in exactly
  one record, every reason it can carry has its sentence in EN and HU,
  and no family names a reason the backend cannot send.
- The reasons that travel in a variable are Literal types (edit, move,
  cookies, quota), so the type checker refuses an unknown one too.
- downloads.errors.edit.not_media in EN and HU.
This commit is contained in:
2026-10-30 23:36:25 +01:00
parent b3be568e36
commit 4379f76747
14 changed files with 381 additions and 123 deletions
+7 -2
View File
@@ -9,7 +9,7 @@ unique key, a row lock taken before a presence check. The comfort shelf's writes
from datetime import datetime, timezone
from typing import cast
from typing import Literal, cast
from sqlalchemy import Table, bindparam, delete, exists, func, select, text, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
@@ -301,10 +301,15 @@ def set_progress(db: Session, user: User, video_id: str, position: int) -> None:
# --- playlists -------------------------------------------------------------------------------
# Every reason a move is refused; the UI names each one (`playlists.moveErrors.<reason>`, held by the
# frontend's backendIds contract, which reads this Literal).
MoveReason = Literal["same_playlist", "not_in_playlist"]
class MoveRefused(Exception):
"""A move between playlists that cannot happen: `reason` is the code the UI translates."""
def __init__(self, reason: str):
def __init__(self, reason: MoveReason):
super().__init__(reason)
self.reason = reason