mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-25 07:32:19 -05:00
VoD YouTube timestamps generator (#2912)
This commit is contained in:
parent
0fae97a189
commit
48152e1185
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "~/components/elements/ChipRadio";
|
||||
import { ModeImage, StageImage } from "~/components/Image";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { shortStageName } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { RankedModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import {
|
||||
databaseTimestampNow,
|
||||
|
|
@ -264,8 +265,8 @@ function RotationCard({
|
|||
/>
|
||||
) : null}
|
||||
<ModeImage mode={shownNext.mode as RankedModeShort} width={16} />{" "}
|
||||
{t(`game-misc:STAGE_${shownNext.stageId1}` as any).split(" ")[0]},{" "}
|
||||
{t(`game-misc:STAGE_${shownNext.stageId2}` as any).split(" ")[0]}
|
||||
{shortStageName(t(`game-misc:STAGE_${shownNext.stageId1}` as any))},{" "}
|
||||
{shortStageName(t(`game-misc:STAGE_${shownNext.stageId2}` as any))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { ModeImage } from "~/components/Image";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import { shortStageName, stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import { stageImageUrl } from "~/utils/urls";
|
||||
|
|
@ -166,7 +166,7 @@ function MapButton({
|
|||
<div className={clsx(styles.mapButtonText, "text-error")}>Banned</div>
|
||||
) : null}
|
||||
<div className={styles.mapButtonLabel}>
|
||||
{t(`game-misc:STAGE_${stageId}`).split(" ")[0]}
|
||||
{shortStageName(t(`game-misc:STAGE_${stageId}`))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { TournamentRoundMaps } from "~/db/tables";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useTournament } from "~/features/tournament/routes/to.$id";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { shortStageName } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { stageImageUrl } from "~/utils/urls";
|
||||
|
|
@ -232,7 +233,7 @@ function MapButton({
|
|||
</span>
|
||||
) : null}
|
||||
<div className={styles.mapButtonLabel}>
|
||||
{t(`game-misc:STAGE_${stageId}`).split(" ")[0]}
|
||||
{shortStageName(t(`game-misc:STAGE_${stageId}`))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import clsx from "clsx";
|
||||
import { SquarePen, Trash } from "lucide-react";
|
||||
import { Check, ClipboardCopy, Copy, SquarePen, Trash } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MetaFunction } from "react-router";
|
||||
import { useLoaderData } from "react-router";
|
||||
import { LinkButton } from "~/components/elements/Button";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Image, WeaponImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
|
|
@ -13,6 +14,7 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { useTimeFormat } from "~/hooks/useTimeFormat";
|
||||
import { shortStageName } from "~/modules/in-game-lists/stage-ids";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags, type SerializeFrom } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
|
@ -30,12 +32,17 @@ import { action } from "../actions/vods.$id.server";
|
|||
import { PovUser } from "../components/VodPov";
|
||||
import { loader } from "../loaders/vods.$id.server";
|
||||
import type { Vod } from "../vods-types";
|
||||
import { canEditVideo, secondsToHoursMinutesSecondString } from "../vods-utils";
|
||||
import {
|
||||
canEditVideo,
|
||||
generateYoutubeTimestamps,
|
||||
secondsToHoursMinutesSecondString,
|
||||
} from "../vods-utils";
|
||||
import styles from "./vods.$id.module.css";
|
||||
|
||||
export { action, loader };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["vods"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
|
|
@ -115,6 +122,12 @@ export default function VodPage() {
|
|||
typeof data.vod.pov === "string" ? undefined : data.vod.pov?.id,
|
||||
}) ? (
|
||||
<div className="stack horizontal md">
|
||||
{user?.id === data.vod.submitterUserId ? (
|
||||
<CopyTimestampsButton
|
||||
matches={data.vod.matches}
|
||||
type={data.vod.type}
|
||||
/>
|
||||
) : null}
|
||||
<LinkButton
|
||||
to={newVodPage(data.vod.id)}
|
||||
size="small"
|
||||
|
|
@ -237,3 +250,122 @@ function Match({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyTimestampsButton({
|
||||
matches,
|
||||
type,
|
||||
}: {
|
||||
matches: Vod["matches"];
|
||||
type: Vod["type"];
|
||||
}) {
|
||||
const { t } = useTranslation(["vods", "weapons", "game-misc", "common"]);
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [copyTrigger, setCopyTrigger] = React.useState(0);
|
||||
const [modeFormat, setModeFormat] = React.useState<"short" | "long">("short");
|
||||
const [stageFormat, setStageFormat] = React.useState<"short" | "long">(
|
||||
"long",
|
||||
);
|
||||
|
||||
const timestamps = generateYoutubeTimestamps(matches, type, {
|
||||
weaponName: (id) => t(`weapons:MAIN_${id}` as "weapons:MAIN_0"),
|
||||
stageName: (id) => {
|
||||
const fullName = t(`game-misc:STAGE_${id}` as "game-misc:STAGE_0");
|
||||
return stageFormat === "short" ? shortStageName(fullName) : fullName;
|
||||
},
|
||||
modeName: (mode) =>
|
||||
modeFormat === "long"
|
||||
? t(`game-misc:MODE_LONG_${mode}` as "game-misc:MODE_LONG_SZ")
|
||||
: mode,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (copyTrigger === 0) return;
|
||||
|
||||
setCopied(true);
|
||||
const timeout = setTimeout(() => setCopied(false), 2000);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [copyTrigger]);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(timestamps);
|
||||
setCopyTrigger((prev) => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SendouButton
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<ClipboardCopy />}
|
||||
onPress={() => {
|
||||
setDialogOpen(true);
|
||||
setCopied(false);
|
||||
}}
|
||||
data-testid="copy-timestamps-button"
|
||||
>
|
||||
{t("vods:copyTimestamps")}
|
||||
</SendouButton>
|
||||
<SendouDialog
|
||||
isOpen={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
heading={t("vods:copyTimestamps")}
|
||||
>
|
||||
<div className="stack md">
|
||||
<div className="stack horizontal md w-full">
|
||||
<label className="flex-same-size">
|
||||
{t("vods:copyTimestamps.modeFormat")}
|
||||
<select
|
||||
value={modeFormat}
|
||||
onChange={(e) =>
|
||||
setModeFormat(e.target.value as "short" | "long")
|
||||
}
|
||||
>
|
||||
<option value="short">
|
||||
{t("vods:copyTimestamps.format.short")}
|
||||
</option>
|
||||
<option value="long">
|
||||
{t("vods:copyTimestamps.format.long")}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex-same-size">
|
||||
{t("vods:copyTimestamps.stageFormat")}
|
||||
<select
|
||||
value={stageFormat}
|
||||
onChange={(e) =>
|
||||
setStageFormat(e.target.value as "short" | "long")
|
||||
}
|
||||
>
|
||||
<option value="short">
|
||||
{t("vods:copyTimestamps.format.short")}
|
||||
</option>
|
||||
<option value="long">
|
||||
{t("vods:copyTimestamps.format.long")}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
value={timestamps}
|
||||
rows={Math.min(matches.length + 2, 15)}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-lighter text-xs">
|
||||
{t("vods:copyTimestamps.help")}
|
||||
</p>
|
||||
<SendouButton
|
||||
onPress={handleCopy}
|
||||
icon={copied ? <Check /> : <Copy />}
|
||||
>
|
||||
{copied
|
||||
? t("common:actions.copied")
|
||||
: t("common:actions.copyToClipboard")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
</SendouDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
MainWeaponId,
|
||||
ModeShort,
|
||||
StageId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import {
|
||||
extractYoutubeIdFromVideoUrl,
|
||||
generateYoutubeTimestamps,
|
||||
hoursMinutesSecondsStringToSeconds,
|
||||
secondsToHoursMinutesSecondString,
|
||||
} from "./vods-utils";
|
||||
|
|
@ -66,6 +72,129 @@ describe("secondsToHoursMinutesSecondString", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function makeMatch(overrides: {
|
||||
startsAt: number;
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
weapons: MainWeaponId[];
|
||||
}) {
|
||||
return { id: 1, ...overrides };
|
||||
}
|
||||
|
||||
const WEAPON_NAMES: Record<number, string> = {
|
||||
40: "Splattershot",
|
||||
200: "Luna Blaster",
|
||||
6010: "Tenta Brella",
|
||||
7010: "Tri-Stringer",
|
||||
};
|
||||
|
||||
const STAGE_NAMES: Record<number, string> = {
|
||||
0: "Scorch Gorge",
|
||||
2: "Hagglefish Market",
|
||||
7: "Mahi-Mahi Resort",
|
||||
10: "MakoMart",
|
||||
};
|
||||
|
||||
const MODE_LONG_NAMES: Record<string, string> = {
|
||||
SZ: "Splat Zones",
|
||||
TC: "Tower Control",
|
||||
RM: "Rainmaker",
|
||||
CB: "Clam Blitz",
|
||||
};
|
||||
|
||||
const RESOLVERS = {
|
||||
weaponName: (id: number) => WEAPON_NAMES[id] ?? String(id),
|
||||
stageName: (id: number) => STAGE_NAMES[id] ?? String(id),
|
||||
modeName: (mode: string) => mode,
|
||||
};
|
||||
|
||||
const LONG_MODE_RESOLVERS = {
|
||||
...RESOLVERS,
|
||||
modeName: (mode: string) => MODE_LONG_NAMES[mode] ?? mode,
|
||||
};
|
||||
|
||||
describe("generateYoutubeTimestamps", () => {
|
||||
it("should include intro line when first match starts after 0", () => {
|
||||
const matches = [
|
||||
makeMatch({
|
||||
startsAt: 521,
|
||||
mode: "SZ",
|
||||
stageId: 7,
|
||||
weapons: [40 as MainWeaponId],
|
||||
}),
|
||||
makeMatch({
|
||||
startsAt: 759,
|
||||
mode: "TC",
|
||||
stageId: 2,
|
||||
weapons: [7010 as MainWeaponId],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = generateYoutubeTimestamps(matches, "TOURNAMENT", RESOLVERS);
|
||||
|
||||
expect(result).toBe(
|
||||
"0:00 Intro\n8:41 Splattershot / SZ Mahi-Mahi Resort\n12:39 Tri-Stringer / TC Hagglefish Market",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not include intro line when first match starts at 0", () => {
|
||||
const matches = [
|
||||
makeMatch({
|
||||
startsAt: 0,
|
||||
mode: "RM",
|
||||
stageId: 0,
|
||||
weapons: [40 as MainWeaponId],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = generateYoutubeTimestamps(matches, "SCRIM", RESOLVERS);
|
||||
|
||||
expect(result).toBe("0:00 Splattershot / RM Scorch Gorge");
|
||||
});
|
||||
|
||||
it("should not include weapon for CAST type", () => {
|
||||
const matches = [
|
||||
makeMatch({
|
||||
startsAt: 25,
|
||||
mode: "CB",
|
||||
stageId: 10,
|
||||
weapons: [200 as MainWeaponId, 6010 as MainWeaponId],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = generateYoutubeTimestamps(matches, "CAST", RESOLVERS);
|
||||
|
||||
expect(result).toBe("0:00 Intro\n0:25 CB MakoMart");
|
||||
});
|
||||
|
||||
it("should use long mode names when resolver returns them", () => {
|
||||
const matches = [
|
||||
makeMatch({
|
||||
startsAt: 521,
|
||||
mode: "SZ",
|
||||
stageId: 7,
|
||||
weapons: [40 as MainWeaponId],
|
||||
}),
|
||||
makeMatch({
|
||||
startsAt: 759,
|
||||
mode: "RM",
|
||||
stageId: 2,
|
||||
weapons: [7010 as MainWeaponId],
|
||||
}),
|
||||
];
|
||||
|
||||
const result = generateYoutubeTimestamps(
|
||||
matches,
|
||||
"TOURNAMENT",
|
||||
LONG_MODE_RESOLVERS,
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
"0:00 Intro\n8:41 Splattershot / Splat Zones Mahi-Mahi Resort\n12:39 Tri-Stringer / Rainmaker Hagglefish Market",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hoursMinutesSecondsStringToSeconds", () => {
|
||||
it("should convert HH:MM:SS format to seconds", () => {
|
||||
const result = hoursMinutesSecondsStringToSeconds("1:01:01");
|
||||
|
|
|
|||
|
|
@ -95,3 +95,36 @@ export function hoursMinutesSecondsStringToSeconds(
|
|||
export function youtubeIdToYoutubeUrl(youtubeId: string) {
|
||||
return `https://www.youtube.com/watch?v=${youtubeId}`;
|
||||
}
|
||||
|
||||
export function generateYoutubeTimestamps(
|
||||
matches: Vod["matches"],
|
||||
type: Vod["type"],
|
||||
resolvers: {
|
||||
weaponName: (weaponId: number) => string;
|
||||
stageName: (stageId: number) => string;
|
||||
modeName: (mode: string) => string;
|
||||
},
|
||||
) {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (matches.length > 0 && matches[0].startsAt > 0) {
|
||||
lines.push("0:00 Intro");
|
||||
}
|
||||
|
||||
const isCast = type === "CAST";
|
||||
|
||||
for (const match of matches) {
|
||||
const timestamp = secondsToHoursMinutesSecondString(match.startsAt);
|
||||
const stage = resolvers.stageName(match.stageId);
|
||||
const mode = resolvers.modeName(match.mode);
|
||||
|
||||
const weaponPart =
|
||||
!isCast && match.weapons.length === 1
|
||||
? `${resolvers.weaponName(match.weapons[0])} / `
|
||||
: "";
|
||||
|
||||
lines.push(`${timestamp} ${weaponPart}${mode} ${stage}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,3 +54,7 @@ export const stagesObj = {
|
|||
LEMURIA_HUB: 23,
|
||||
URCHIN_UNDERPASS: 24,
|
||||
} as const;
|
||||
|
||||
export function shortStageName(fullName: string) {
|
||||
return fullName.split(" ")[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,15 @@ test.describe("VoDs page", () => {
|
|||
await page.getByText(formattedDate).isVisible();
|
||||
await page.getByTestId("weapon-img-4001").isVisible();
|
||||
await page.getByTestId("weapon-img-6010").isVisible();
|
||||
|
||||
await page.getByTestId("copy-timestamps-button").click();
|
||||
await page.getByText("0:00 Intro").isVisible();
|
||||
await page
|
||||
.getByText("0:20 Zink Mini Splatling / TC Hammerhead Bridge")
|
||||
.isVisible();
|
||||
await page
|
||||
.getByText("5:55 Tenta Brella / RM Museum d'Alfonsino")
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
test("adds video (cast)", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Kopiér til udklipsholder",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Skab",
|
||||
"actions.close": "Luk",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "Skriv venligst en besked til helpdesken på vores Discordserver for at få rettigheder til at uploade videoer.",
|
||||
"deleteConfirm": "Vil du slette videoen: '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "In die Zwischenablage kopieren",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "Discord-Zeitstempel kopieren",
|
||||
"actions.create": "Erstellen",
|
||||
"actions.close": "Schließen",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Show more",
|
||||
"actions.showLess": "Show less",
|
||||
"actions.copyToClipboard": "Copy to clipboard",
|
||||
"actions.copied": "Copied!",
|
||||
"actions.copyTimestampForDiscord": "Copy Discord timestamp",
|
||||
"actions.create": "Create",
|
||||
"actions.close": "Close",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "No videos found matching this filter. Are we missing something? See the FAQ page for information on how to gain VoD uploading permissions.",
|
||||
"gainPerms": "Please post on the helpdesk of our Discord to gain permissions to upload videos",
|
||||
"deleteConfirm": "Delete video with the title '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": "Failed to load YouTube preview"
|
||||
"errors.youtubePreviewFailed": "Failed to load YouTube preview",
|
||||
"copyTimestamps": "Copy timestamps",
|
||||
"copyTimestamps.help": "Paste these into your YouTube video description to create chapter markers.",
|
||||
"copyTimestamps.modeFormat": "Mode format",
|
||||
"copyTimestamps.stageFormat": "Stage format",
|
||||
"copyTimestamps.format.short": "Short",
|
||||
"copyTimestamps.format.long": "Long"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Mostrar más",
|
||||
"actions.showLess": "Mostrar menos",
|
||||
"actions.copyToClipboard": "Copiar al portapapeles",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "Copiar timestamp para Discord",
|
||||
"actions.create": "Crear",
|
||||
"actions.close": "Cerrar",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "No se han encontrado vídeos que coincidan con este filtro. ¿Nos falta alguno? Consulta la página de Preguntas frecuentes para saber cómo obtener permisos para subir vídeos (VoD).",
|
||||
"gainPerms": "Por favor envía un mensaje en el 'helpdesk' de nuestro Discord para obtener permiso para subir videos.",
|
||||
"deleteConfirm": "¿Borrar el vídeo con el título '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": "Error al cargar la vista previa de YouTube"
|
||||
"errors.youtubePreviewFailed": "Error al cargar la vista previa de YouTube",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Mostrar más",
|
||||
"actions.showLess": "Mostrar menos",
|
||||
"actions.copyToClipboard": "Copiar a clipboard",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Crear",
|
||||
"actions.close": "Cerrar",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "Por favor envia un mensaje en el 'helpdesk' de nuestro Discord para obtener permiso para subir videos.",
|
||||
"deleteConfirm": "¿Borrar video con el título de '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Copier dans le presse-papier",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Créer",
|
||||
"actions.close": "Fermer",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Montrer more",
|
||||
"actions.showLess": "Montrer moins",
|
||||
"actions.copyToClipboard": "Copier dans le presse-papier",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Créer",
|
||||
"actions.close": "Fermer",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "Aucune vidéo a été trouvé. Est-ce qu'il nous manque quelque chose ? Consultez la page FAQ pour savoir comment obtenir les autorisations de téléchargement VoD.",
|
||||
"gainPerms": "Demandez sur le channel helpdesk de notre discord pour avoir les permissions de publier vos VoD",
|
||||
"deleteConfirm": "Supprimer la vidéo '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "ההעתקה ללוח",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "יצירה",
|
||||
"actions.close": "סגירה",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Mostra di più",
|
||||
"actions.showLess": "Mostra di meno",
|
||||
"actions.copyToClipboard": "Copia",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Crea",
|
||||
"actions.close": "Chiudi",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "Nessun video trovato con questo filtro. Ci stiamo dimenticando di qualcosa? Consulta il FAQ per informazioni su come ottenere il permesso per caricare VoD",
|
||||
"gainPerms": "Si prega di postare sull'helpdesk del nostro Discord per ottenere permessi per caricare VoD",
|
||||
"deleteConfirm": "Cancellare video il titolo '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "もっと見せる",
|
||||
"actions.showLess": "表示を減らす",
|
||||
"actions.copyToClipboard": "クリップボードにコピーする",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "作成",
|
||||
"actions.close": "閉じる",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "動画をアップロードする権限を取得するためディスコードのヘルプデスクにメッセージを送ってください。",
|
||||
"deleteConfirm": "動画:'{{title}}'を消去しますか??",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "클립보드로 복사",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "생성",
|
||||
"actions.close": "닫기",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Kopiëren naar klembord",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "",
|
||||
"actions.close": "",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Skopiuj do schowka",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Stwórz",
|
||||
"actions.close": "Zamknij",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Mostrar mais",
|
||||
"actions.showLess": "Mostrar menos",
|
||||
"actions.copyToClipboard": "Copiar para a área de transferência",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Criar",
|
||||
"actions.close": "Fechar",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "Por favor, faça uma postagem (em Inglês) no helpdesk do nosso Discord para obter permissões de upload de vídeos.",
|
||||
"deleteConfirm": "",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "Показать больше",
|
||||
"actions.showLess": "Показать меньше",
|
||||
"actions.copyToClipboard": "Скопировать в буфер обмена",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Создать",
|
||||
"actions.close": "Закрыть",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "Видео, совпадающие с фильтрами, не найдены. Может, мы что-то упустили? Посмотрите страницу FAQ с информацией о получении прав на загрузку VoD.",
|
||||
"gainPerms": "Чтобы получить возможность загружать видео, пожалуйста, обратитесь в helpdesk на нашем Discord сервере",
|
||||
"deleteConfirm": "Удалить видео под названием '{{title}}'?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@
|
|||
"actions.showMore": "显示更多",
|
||||
"actions.showLess": "显示更少",
|
||||
"actions.copyToClipboard": "复制到剪贴板",
|
||||
"actions.copied": "",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "创建",
|
||||
"actions.close": "关闭",
|
||||
|
|
|
|||
|
|
@ -32,5 +32,11 @@
|
|||
"noVods": "",
|
||||
"gainPerms": "请在我们的Discord服务器的helpdesk频道申请获取上传视频的资格。",
|
||||
"deleteConfirm": "确认要删除 '{{title}}' 吗?",
|
||||
"errors.youtubePreviewFailed": ""
|
||||
"errors.youtubePreviewFailed": "",
|
||||
"copyTimestamps": "",
|
||||
"copyTimestamps.help": "",
|
||||
"copyTimestamps.modeFormat": "",
|
||||
"copyTimestamps.stageFormat": "",
|
||||
"copyTimestamps.format.short": "",
|
||||
"copyTimestamps.format.long": ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user