fix(clip): a clip refusal speaks the user's language, and a choice in the clip form no longer sends it

The backend refuses a clip with a structured reason, but the client showed every one as the generic
"couldn't make the clip": each reason now has its own message in both languages, held to the
backend's reasons in both directions by the contract test. A segmented toggle's and the priority
stepper's buttons had no type, so inside a form one of them submitted it.
This commit is contained in:
2026-10-10 12:52:12 +02:00
parent 50157cc2fd
commit e301632765
7 changed files with 59 additions and 2 deletions
@@ -170,6 +170,12 @@ const youtubeReasons = [
backend("youtube/client.py").match(/^YouTubeRefusal = Literal\[([^\]]*)\]/m)?.[1] ?? ""
).matchAll(/"(\w+)"/g),
].map((m) => m[1]!);
// A clip the server will not cut → `clip.errors.<reason>`: every `ClipError("<reason>")` in the backend.
const clipReasons = [
...new Set(
appSources().flatMap((src) => [...src.matchAll(/ClipError\("(\w+)"\)/g)].map((m) => m[1]!)),
),
];
// The closed English vocabularies the backend WRITES into data, named through `vocab.<group>`
// (lib/vocab) by their letters-and-digits key. A word the backend adds shows in English until named —
@@ -233,6 +239,7 @@ describe("backend ids have their names in both languages", () => {
expect(goneWhats).toEqual(expect.arrayContaining(["playlist", "download", "job"]));
expect(opaqueWhats).toEqual([]);
expect(youtubeReasons).toEqual(["quota", "reconnect", "unavailable", "gone"]);
expect(clipReasons).toEqual(expect.arrayContaining(["too_long", "past_end"]));
expect(crewJobs).toEqual(
expect.arrayContaining(["Director", "Creator", "Director of Photography"]),
);
@@ -336,6 +343,13 @@ describe("backend ids have their names in both languages", () => {
).toEqual([]);
});
it(`${lang}: every reason a clip is refused, and none it cannot be`, () => {
expect(missing(clipReasons.map((r) => `clip.errors.${r}`))).toEqual([]);
expect([...idsUnder(keys, "clip.errors")].filter((r) => !clipReasons.includes(r))).toEqual(
[],
);
});
it(`${lang}: every crew job, auto-tag and built-in profile the backend writes, and no other`, () => {
for (const [group, words] of Object.entries(VOCAB)) {
const wanted = words.map((w) => `vocab.${group}.${vocabKey(w)}`);
+11 -1
View File
@@ -17,5 +17,15 @@
"cut": "Cut clip",
"queued": "The clip is being made — it will be in your downloads",
"view": "Downloads",
"failed": "Couldn’t make the clip"
"failed": "Couldn’t make the clip",
"errors": {
"bad_source": "This video can’t be clipped.",
"bad_range": "The clip’s start or end isn’t valid.",
"too_short": "A clip is at least a second long.",
"too_long": "A clip is at most 10 minutes long.",
"bad_audio": "That audio track isn’t available.",
"not_found": "This item is no longer in the library.",
"media_missing": "The item’s file can’t be reached from here.",
"past_end": "The clip runs past the end of the video."
}
}
+11 -1
View File
@@ -17,5 +17,15 @@
"cut": "Klip vágása",
"queued": "Készül a klip — a letöltéseid közé kerül",
"view": "Letöltések",
"failed": "Nem sikerült elkészíteni a klipet"
"failed": "Nem sikerült elkészíteni a klipet",
"errors": {
"bad_source": "Ebből a videóból nem lehet klipet vágni.",
"bad_range": "A klip eleje vagy vége érvénytelen.",
"too_short": "A klip legalább egy másodperc.",
"too_long": "A klip legfeljebb 10 perc lehet.",
"bad_audio": "Ez a hangsáv nem érhető el.",
"not_found": "Ez az elem már nincs a könyvtárban.",
"media_missing": "Az elem fájlja innen nem érhető el.",
"past_end": "A klip túlnyúlik a videó végén."
}
}