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:
@@ -0,0 +1,61 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A server refusal said through the records (`refusalCodes`), in both languages, by the real i18n —
|
||||
// one case per record shape, so moving the dispatch into data changed what no reader sees.
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import i18n from "../../i18n";
|
||||
import { localizeDetail } from "./refusalText";
|
||||
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
describe.each(["en", "hu"])("localizeDetail (%s)", (lng) => {
|
||||
const say = async (d: unknown) => {
|
||||
await i18n.changeLanguage(lng);
|
||||
return localizeDetail(d);
|
||||
};
|
||||
|
||||
it("names an edit refused for a file that is not video or audio", async () => {
|
||||
const said = await say({ code: "edit", reason: "not_media" });
|
||||
expect(said).toBe(i18n.t("downloads.errors.edit.not_media"));
|
||||
expect(said).not.toBe(i18n.t("downloads.errors.edit.generic"));
|
||||
});
|
||||
|
||||
it("says a family's reason, and its own fallback for a reason it does not know", async () => {
|
||||
expect(await say({ code: "playlist_move", reason: "same_playlist" })).toBe(
|
||||
i18n.t("playlists.moveErrors.same_playlist"),
|
||||
);
|
||||
expect(await say({ code: "playlist_move", reason: "nope" })).toBe(
|
||||
i18n.t("playlists.moveFailed"),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads a sign-in refusal's one-line form", async () => {
|
||||
expect(await say({ code: "login", reason: "suspended" })).toBe(
|
||||
i18n.t("welcome.auth.blocked.suspended.short"),
|
||||
);
|
||||
});
|
||||
|
||||
it("puts a refusal's numbers into its sentence", async () => {
|
||||
expect(await say({ code: "quota", reason: "max_jobs", limit: 7 })).toContain("7");
|
||||
expect(await say({ code: "reset", reason: "weak_password", min: 12 })).toContain("12");
|
||||
expect(await say({ code: "refused", reason: "view_name_taken", name: "Esti" })).toContain(
|
||||
"Esti",
|
||||
);
|
||||
});
|
||||
|
||||
it("says a shared-sentence code's fallback, and a fixed code's one sentence", async () => {
|
||||
expect(await say({ code: "reset", reason: "invalid" })).toBe(
|
||||
i18n.t("welcome.auth.deadLink.short"),
|
||||
);
|
||||
expect(await say({ code: "sync_cooldown" })).toBe(i18n.t("errors.syncCooldown"));
|
||||
});
|
||||
|
||||
it("reads a code no screen shows, or one it does not know, as the generic failure", async () => {
|
||||
expect(await say({ code: "bad_cursor" })).toBe(i18n.t("errors.generic"));
|
||||
expect(await say({ code: "never_sent" })).toBe(i18n.t("errors.generic"));
|
||||
expect(await say({ code: "constructor" })).toBe(i18n.t("errors.generic"));
|
||||
expect(await say(null)).toBe(i18n.t("errors.generic"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user