feat(downloads): a clip or an edit can be a GIF, shared as a picture
A clip and an editor range can come out as a silent GIF (long side 320/480/640, 12 fps, at most 30 s, one palette per GIF); one over 30 MB is not kept. A shared picture is served inline under the preview's sandbox, and a GIF's link card is the GIF itself up to 15 MB, the still for Facebook's crawler.
This commit is contained in:
@@ -31,6 +31,8 @@
|
||||
"bad_crop": "The crop frame isn’t valid. Make it bigger, or go back to the whole picture.",
|
||||
"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."
|
||||
"past_end": "The clip runs past the end of the video.",
|
||||
"bad_gif": "That GIF size isn’t available.",
|
||||
"gif_too_long": "A GIF is at most {{seconds}} seconds long."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,9 @@
|
||||
"empty_edit": "Choose a trim range or crop area first",
|
||||
"source_not_ready": "The source download isn't ready to edit",
|
||||
"not_media": "Only video and audio can be edited",
|
||||
"bad_gif": "That GIF size isn’t available",
|
||||
"gif_range": "A GIF is made from one range with a start and an end",
|
||||
"gif_too_long": "A GIF is at most {{seconds}} seconds long",
|
||||
"generic": "Couldn't create that edit"
|
||||
},
|
||||
"upload": {
|
||||
@@ -312,6 +315,10 @@
|
||||
"short": "Couldn't download",
|
||||
"body": "Something went wrong with this download. The technical details below may help."
|
||||
},
|
||||
"gif_too_big": {
|
||||
"short": "The GIF is too large",
|
||||
"body": "This GIF came out larger than a GIF may be, so it wasn’t kept. Mark a shorter range or choose a smaller size, then make it again."
|
||||
},
|
||||
"quota_exceeded": {
|
||||
"short": "Not enough storage",
|
||||
"body": "This download doesn't fit in the storage you have left. Delete something from your library, then try again."
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"bad_crop": "A kivágókeret érvénytelen. Nagyítsd meg, vagy térj vissza a teljes képre.",
|
||||
"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."
|
||||
"past_end": "A klip túlnyúlik a videó végén.",
|
||||
"bad_gif": "Ez a GIF-méret nem választható.",
|
||||
"gif_too_long": "A GIF legfeljebb {{seconds}} másodperc lehet."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,9 @@
|
||||
"empty_edit": "Előbb válassz vágási tartományt vagy körbevágási területet",
|
||||
"source_not_ready": "A forrásletöltés még nem szerkeszthető",
|
||||
"not_media": "Csak videó és hang szerkeszthető",
|
||||
"bad_gif": "Ez a GIF-méret nem választható",
|
||||
"gif_range": "GIF egyetlen szakaszból készül, aminek eleje és vége is van",
|
||||
"gif_too_long": "A GIF legfeljebb {{seconds}} másodperc lehet",
|
||||
"generic": "Nem sikerült létrehozni a vágást"
|
||||
},
|
||||
"upload": {
|
||||
@@ -312,6 +315,10 @@
|
||||
"short": "Nem sikerült letölteni",
|
||||
"body": "Valami hiba történt ennél a letöltésnél. Az alábbi technikai részletek segíthetnek."
|
||||
},
|
||||
"gif_too_big": {
|
||||
"short": "Túl nagy a GIF",
|
||||
"body": "Ez a GIF nagyobb lett, mint amekkora egy GIF lehet, ezért nem maradt meg. Jelölj ki rövidebb szakaszt, vagy válassz kisebb méretet, és készítsd el újra."
|
||||
},
|
||||
"quota_exceeded": {
|
||||
"short": "Nincs elég tárhely",
|
||||
"body": "Ez a letöltés nem fér bele a megmaradt tárhelyedbe. Törölj valamit a könyvtáradból, aztán próbáld újra."
|
||||
|
||||
@@ -55,6 +55,15 @@ describe.each(["en", "hu"])("localizeDetail (%s)", (lng) => {
|
||||
expect(upload).not.toBe(i18n.t("downloads.errors.upload.tooLarge", { size: "" }));
|
||||
});
|
||||
|
||||
it("says a clip's cap in minutes and a GIF's in seconds, from a clip or an edit", async () => {
|
||||
expect(await say({ code: "clip", reason: "too_long", limit_s: 600 })).toContain("10");
|
||||
for (const code of ["clip", "edit"]) {
|
||||
const said = await say({ code, reason: "gif_too_long", limit_s: 30 });
|
||||
expect(said).toContain("30");
|
||||
expect(said).not.toContain("{{");
|
||||
}
|
||||
});
|
||||
|
||||
it("says a shared-sentence code's own fallback for a reason it does not know", async () => {
|
||||
expect(await say({ code: "quota", reason: "nope" })).toBe(
|
||||
i18n.t("downloads.errors.quota.generic"),
|
||||
|
||||
@@ -30,13 +30,18 @@ const byReason = new Map(
|
||||
const fixed = new Map<string, string>(Object.entries(REFUSAL_FIXED));
|
||||
|
||||
// The numbers a refusal carries, in the reader's number format: a download cap in GB, a clip cap in
|
||||
// minutes, an upload cap in bytes, a password's minimum length; a generic `refused` interpolates
|
||||
// whatever values its sentence names (`{{min}}`, `{{name}}`, …).
|
||||
// minutes (a GIF's in seconds), an upload cap in bytes, a password's minimum length; a generic
|
||||
// `refused` interpolates whatever values its sentence names (`{{min}}`, `{{name}}`, …).
|
||||
function valuesOf(o: NonNullable<Detail>): Record<string, unknown> {
|
||||
const lang = i18n.language;
|
||||
if (o.code === "refused") return { ...o };
|
||||
if (o.code === "clip")
|
||||
return { minutes: o.limit_s == null ? undefined : formatDecimal(o.limit_s / 60, lang, 1, 0) };
|
||||
if (o.code === "clip" || o.code === "edit")
|
||||
return o.limit_s == null
|
||||
? {}
|
||||
: {
|
||||
minutes: formatDecimal(o.limit_s / 60, lang, 1, 0),
|
||||
seconds: formatDecimal(o.limit_s, lang, 0, 0),
|
||||
};
|
||||
if (o.code === "quota")
|
||||
return o.reason === "max_bytes"
|
||||
? { size: formatDecimal((o.limit || 0) / 1_073_741_824, lang, 1, 0) }
|
||||
|
||||
Reference in New Issue
Block a user