fix(i18n): the video menu's download row says "Download…", and counted copy is pluralized
- The row opens the download dialog, so it is labelled so; the download's state ("Downloaded",
"In queue") is its hint, not its label.
- The share dialog's expiry ("1 days") and a link's view count ("1 views") go through i18next's
plural forms (`count`).
This commit is contained in:
@@ -160,7 +160,7 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted flex-wrap">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Eye className="w-3 h-3" /> {t("downloads.share.views", { n: link.view_count })}
|
||||
<Eye className="w-3 h-3" /> {t("downloads.share.views", { count: link.view_count })}
|
||||
</span>
|
||||
{badges.map((b) => (
|
||||
<span
|
||||
@@ -307,7 +307,7 @@ function LinkShare({ job }: { job: DownloadJob }) {
|
||||
>
|
||||
{d === null
|
||||
? t("downloads.share.expiryNever")
|
||||
: t("downloads.share.days", { n: d })}
|
||||
: t("downloads.share.days", { count: d })}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// E28 review J4: the video menu's download row was labelled with the download's STATE ("Downloaded",
|
||||
// "In queue") — a row that reads as a status yet opens the download dialog. It is "Download…" now,
|
||||
// the state riding along as the row's hint.
|
||||
import type { ReactNode } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("react-i18next")>()),
|
||||
useTranslation: () => ({ t: (k: string) => k }),
|
||||
}));
|
||||
vi.mock("./navigationContext", () => ({ useNavigationActions: () => ({ openChannel: vi.fn() }) }));
|
||||
vi.mock("./comfortPin", () => ({
|
||||
pinVideo: vi.fn(),
|
||||
useComfortPin: () => ({ item: () => [] }),
|
||||
}));
|
||||
|
||||
import { api, type Video } from "../lib/api";
|
||||
import { useVideoActions } from "./useVideoActions";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const video = { id: "v1", title: "A video", status: "new", position_seconds: 0 } as Video;
|
||||
|
||||
function downloadRow(state: string | undefined) {
|
||||
vi.spyOn(api, "downloadIndex").mockResolvedValue(
|
||||
(state ? { v1: state } : {}) as Awaited<ReturnType<typeof api.downloadIndex>>,
|
||||
);
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useVideoActions({ onState: vi.fn(), onToggleSave: vi.fn() }, true),
|
||||
{ wrapper },
|
||||
);
|
||||
return () => result.current.items(video, null).find((i) => i.id === "download")!;
|
||||
}
|
||||
|
||||
describe("the video menu's download row", () => {
|
||||
it("says what it does, whatever the state, and carries the state as its hint", async () => {
|
||||
const done = downloadRow("done");
|
||||
await waitFor(() => expect(done().title).toBe("downloads.button.downloaded"));
|
||||
expect(done().label).toBe("downloads.menu.download");
|
||||
cleanup();
|
||||
const none = downloadRow(undefined);
|
||||
await waitFor(() => expect(none().label).toBe("downloads.menu.download"));
|
||||
expect(none().title).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -74,12 +74,14 @@ export function useVideoActions(handlers: VideoActionHandlers, open: boolean) {
|
||||
const hidden = v.status === "hidden";
|
||||
const inProgress = v.position_seconds > 0 && !watched;
|
||||
const dl = index.data?.[v.id];
|
||||
const dlLabel =
|
||||
// The row opens the download dialog, so it says so; the download's state is its hint, not its
|
||||
// label — "Downloaded" read as a status, not as a row that opens something (E28 review J4).
|
||||
const dlState =
|
||||
dl === "done"
|
||||
? t("downloads.button.downloaded")
|
||||
: dl === "queued" || dl === "running" || dl === "paused"
|
||||
? t("downloads.button.queued")
|
||||
: t("downloads.button.label");
|
||||
: undefined;
|
||||
// The two panels open beside the object; a menu opened with no element to anchor to (none of
|
||||
// the triggers does that today) leaves them out rather than opening them at the corner.
|
||||
const at = (kind: "playlist" | "details") =>
|
||||
@@ -140,7 +142,8 @@ export function useVideoActions(handlers: VideoActionHandlers, open: boolean) {
|
||||
? [
|
||||
{
|
||||
id: "download",
|
||||
label: dlLabel,
|
||||
label: t("downloads.menu.download"),
|
||||
title: dlState,
|
||||
icon: <Download className={icon} />,
|
||||
onSelect: () => setFollowUp({ kind: "download", video: v }),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"navLabel": "Downloads",
|
||||
"menu": {
|
||||
"download": "Download…"
|
||||
},
|
||||
"button": {
|
||||
"label": "Download",
|
||||
"queued": "In queue",
|
||||
@@ -124,13 +127,15 @@
|
||||
"allowDownload": "Allow download",
|
||||
"expiry": "Expires",
|
||||
"expiryNever": "Never",
|
||||
"days": "{{n}} days",
|
||||
"days_one": "{{count}} day",
|
||||
"days_other": "{{count}} days",
|
||||
"password": "Password (optional)",
|
||||
"copy": "Copy link",
|
||||
"copyFailed": "Couldn’t copy",
|
||||
"revoke": "Revoke",
|
||||
"revokeConfirm": "This link will stop working immediately.",
|
||||
"views": "{{n}} views",
|
||||
"views_one": "{{count}} view",
|
||||
"views_other": "{{count}} views",
|
||||
"expiresOn": "expires {{date}}",
|
||||
"hasPassword": "password",
|
||||
"downloadable": "downloadable",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"navLabel": "Letöltések",
|
||||
"menu": {
|
||||
"download": "Letöltés…"
|
||||
},
|
||||
"button": {
|
||||
"label": "Letöltés",
|
||||
"queued": "Sorban",
|
||||
@@ -124,13 +127,15 @@
|
||||
"allowDownload": "Letöltés engedélyezése",
|
||||
"expiry": "Lejárat",
|
||||
"expiryNever": "Soha",
|
||||
"days": "{{n}} nap",
|
||||
"days_one": "{{count}} nap",
|
||||
"days_other": "{{count}} nap",
|
||||
"password": "Jelszó (opcionális)",
|
||||
"copy": "Link másolása",
|
||||
"copyFailed": "Nem sikerült másolni",
|
||||
"revoke": "Visszavonás",
|
||||
"revokeConfirm": "A link azonnal használhatatlanná válik.",
|
||||
"views": "{{n}} megtekintés",
|
||||
"views_one": "{{count}} megtekintés",
|
||||
"views_other": "{{count}} megtekintés",
|
||||
"expiresOn": "lejár: {{date}}",
|
||||
"hasPassword": "jelszó",
|
||||
"downloadable": "letölthető",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// E28 review J5: a count in copy goes through i18next's plural forms (`count`), never a bare `n` —
|
||||
// the share dialog's expiry read "1 days".
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import i18n from "./index";
|
||||
|
||||
describe("counted copy", () => {
|
||||
it("the share link's expiry is singular for one day", async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
expect(i18n.t("downloads.share.days", { count: 1 })).toBe("1 day");
|
||||
expect(i18n.t("downloads.share.days", { count: 7 })).toBe("7 days");
|
||||
await i18n.changeLanguage("hu");
|
||||
expect(i18n.t("downloads.share.days", { count: 1 })).toBe("1 nap");
|
||||
await i18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
it("a link's view count is singular for one view", async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
expect(i18n.t("downloads.share.views", { count: 1 })).toBe("1 view");
|
||||
expect(i18n.t("downloads.share.views", { count: 0 })).toBe("0 views");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user