refactor: Update formatUtils to use formatRelativeDownloadTime function

This commit is contained in:
Peifan Li
2026-01-02 13:25:02 -05:00
parent 2d9d7b37a6
commit c9657bad51
16 changed files with 452 additions and 145 deletions

View File

@@ -41,12 +41,11 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection, videos }) =
display: 'flex',
flexDirection: 'column',
position: 'relative',
transition: 'transform 0.2s, box-shadow 0.2s, background-color 0.3s, color 0.3s, border-color 0.3s',
transition: 'transform 0.2s, box-shadow 0.2s, background-color 0.3s, color 0.3s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: theme.shadows[8],
},
border: `1px solid ${theme.palette.secondary.main}`
}
}}
>
<CardActionArea onClick={handleClick} sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}>
@@ -76,7 +75,7 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection, videos }) =
<Chip
icon={<Folder />}
label={`${collection.videos.length} videos`}
label={collection.videos.length}
color="secondary"
size="small"
sx={{ position: 'absolute', bottom: 8, right: 8 }}

View File

@@ -2,7 +2,7 @@ import { Box, CardContent, Typography } from '@mui/material';
import React from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
import { Video } from '../../types';
import { formatDate } from '../../utils/formatUtils';
import { formatRelativeDownloadTime } from '../../utils/formatUtils';
import { VideoCardCollectionInfo } from '../../utils/videoCardUtils';
interface VideoCardContentProps {
@@ -72,7 +72,7 @@ export const VideoCardContent: React.FC<VideoCardContentProps> = ({
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
<Typography variant="caption" color="text.secondary">
{formatDate(video.date)}
{formatRelativeDownloadTime(video.addedAt, video.date, t)}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
{video.viewCount || 0} {t('views')}

View File

@@ -10,6 +10,7 @@ vi.mock('../../../contexts/LanguageContext', () => ({
vi.mock('../../../utils/formatUtils', () => ({
formatDate: () => '2023-01-01',
formatRelativeDownloadTime: () => '2023-01-01',
}));

View File

@@ -86,7 +86,7 @@ describe('CollectionCard', () => {
);
expect(screen.getByText(/Test Collection/i)).toBeInTheDocument();
expect(screen.getByText(/2 videos/i)).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
});
it('renders collection creation date', () => {
@@ -131,7 +131,7 @@ describe('CollectionCard', () => {
);
// Should show folder icon (via Material-UI icon)
expect(screen.getByText(/0 videos/i)).toBeInTheDocument();
expect(screen.getByText('0')).toBeInTheDocument();
});
it('displays up to 4 thumbnails in grid', () => {

View File

@@ -1,143 +1,297 @@
import { describe, expect, it } from 'vitest';
import { formatDate, formatDuration, formatSize, parseDuration } from '../formatUtils';
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
formatDate,
formatDuration,
formatRelativeDownloadTime,
formatSize,
parseDuration,
} from "../formatUtils";
describe('formatUtils', () => {
describe('parseDuration', () => {
it('should return 0 for undefined', () => {
expect(parseDuration(undefined)).toBe(0);
});
it('should return number as-is', () => {
expect(parseDuration(100)).toBe(100);
expect(parseDuration(0)).toBe(0);
});
it('should parse HH:MM:SS format', () => {
expect(parseDuration('1:30:45')).toBe(5445); // 1*3600 + 30*60 + 45 = 3600 + 1800 + 45
expect(parseDuration('0:5:30')).toBe(330); // 0*3600 + 5*60 + 30 = 0 + 300 + 30
expect(parseDuration('2:0:0')).toBe(7200); // 2*3600 + 0*60 + 0 = 7200
});
it('should parse MM:SS format', () => {
expect(parseDuration('5:30')).toBe(330); // 5*60 + 30
expect(parseDuration('10:15')).toBe(615); // 10*60 + 15
expect(parseDuration('0:45')).toBe(45);
});
it('should parse numeric string', () => {
expect(parseDuration('100')).toBe(100);
expect(parseDuration('0')).toBe(0);
});
it('should return 0 for invalid string', () => {
expect(parseDuration('invalid')).toBe(0);
// 'abc:def' will be parsed as NaN for each part, but the function
// will try parseInt on the whole string which also returns NaN -> 0
expect(parseDuration('abc:def')).toBe(0);
expect(parseDuration('not-a-number')).toBe(0);
});
describe("formatUtils", () => {
describe("parseDuration", () => {
it("should return 0 for undefined", () => {
expect(parseDuration(undefined)).toBe(0);
});
describe('formatDuration', () => {
it('should return 00:00 for undefined', () => {
expect(formatDuration(undefined)).toBe('00:00');
});
it('should return formatted string as-is if already formatted', () => {
expect(formatDuration('1:30:45')).toBe('1:30:45');
expect(formatDuration('5:30')).toBe('5:30');
});
it('should format seconds to MM:SS', () => {
expect(formatDuration(65)).toBe('1:05'); // 1 minute 5 seconds
expect(formatDuration(125)).toBe('2:05'); // 2 minutes 5 seconds
expect(formatDuration(45)).toBe('0:45'); // 45 seconds
expect(formatDuration(0)).toBe('00:00');
});
it('should format seconds to H:MM:SS for hours', () => {
expect(formatDuration(3665)).toBe('1:01:05'); // 1 hour 1 minute 5 seconds
expect(formatDuration(3600)).toBe('1:00:00'); // 1 hour
expect(formatDuration(7325)).toBe('2:02:05'); // 2 hours 2 minutes 5 seconds
});
it('should format numeric string', () => {
expect(formatDuration('65')).toBe('1:05');
expect(formatDuration('3665')).toBe('1:01:05');
});
it('should return 00:00 for invalid input', () => {
expect(formatDuration('invalid')).toBe('00:00');
expect(formatDuration(NaN)).toBe('00:00');
});
it("should return number as-is", () => {
expect(parseDuration(100)).toBe(100);
expect(parseDuration(0)).toBe(0);
});
describe('formatSize', () => {
it('should return "0 B" for undefined', () => {
expect(formatSize(undefined)).toBe('0 B');
});
it('should format bytes', () => {
expect(formatSize(0)).toBe('0 B');
expect(formatSize(500)).toBe('500 B');
expect(formatSize(1023)).toBe('1023 B');
});
it('should format kilobytes', () => {
expect(formatSize(1024)).toBe('1 KB');
expect(formatSize(1536)).toBe('1.5 KB');
expect(formatSize(2048)).toBe('2 KB');
expect(formatSize(10240)).toBe('10 KB');
});
it('should format megabytes', () => {
expect(formatSize(1048576)).toBe('1 MB'); // 1024 * 1024
expect(formatSize(1572864)).toBe('1.5 MB');
expect(formatSize(5242880)).toBe('5 MB');
});
it('should format gigabytes', () => {
expect(formatSize(1073741824)).toBe('1 GB'); // 1024^3
expect(formatSize(2147483648)).toBe('2 GB');
});
it('should format terabytes', () => {
expect(formatSize(1099511627776)).toBe('1 TB'); // 1024^4
});
it('should format numeric string', () => {
expect(formatSize('1024')).toBe('1 KB');
expect(formatSize('1048576')).toBe('1 MB');
});
it('should return "0 B" for invalid input', () => {
expect(formatSize('invalid')).toBe('0 B');
expect(formatSize(NaN)).toBe('0 B');
});
it("should parse HH:MM:SS format", () => {
expect(parseDuration("1:30:45")).toBe(5445); // 1*3600 + 30*60 + 45 = 3600 + 1800 + 45
expect(parseDuration("0:5:30")).toBe(330); // 0*3600 + 5*60 + 30 = 0 + 300 + 30
expect(parseDuration("2:0:0")).toBe(7200); // 2*3600 + 0*60 + 0 = 7200
});
describe('formatDate', () => {
it('should return "Unknown date" for undefined', () => {
expect(formatDate(undefined)).toBe('Unknown date');
});
it('should return "Unknown date" for invalid length', () => {
expect(formatDate('202301')).toBe('Unknown date');
expect(formatDate('202301011')).toBe('Unknown date');
expect(formatDate('2023')).toBe('Unknown date');
});
it('should format YYYYMMDD to YYYY-MM-DD', () => {
expect(formatDate('20230101')).toBe('2023-01-01');
expect(formatDate('20231225')).toBe('2023-12-25');
expect(formatDate('20200101')).toBe('2020-01-01');
expect(formatDate('20230228')).toBe('2023-02-28');
});
it('should handle edge cases', () => {
expect(formatDate('19991231')).toBe('1999-12-31');
expect(formatDate('20991231')).toBe('2099-12-31');
});
it("should parse MM:SS format", () => {
expect(parseDuration("5:30")).toBe(330); // 5*60 + 30
expect(parseDuration("10:15")).toBe(615); // 10*60 + 15
expect(parseDuration("0:45")).toBe(45);
});
it("should parse numeric string", () => {
expect(parseDuration("100")).toBe(100);
expect(parseDuration("0")).toBe(0);
});
it("should return 0 for invalid string", () => {
expect(parseDuration("invalid")).toBe(0);
// 'abc:def' will be parsed as NaN for each part, but the function
// will try parseInt on the whole string which also returns NaN -> 0
expect(parseDuration("abc:def")).toBe(0);
expect(parseDuration("not-a-number")).toBe(0);
});
});
describe("formatDuration", () => {
it("should return 00:00 for undefined", () => {
expect(formatDuration(undefined)).toBe("00:00");
});
it("should return formatted string as-is if already formatted", () => {
expect(formatDuration("1:30:45")).toBe("1:30:45");
expect(formatDuration("5:30")).toBe("5:30");
});
it("should format seconds to MM:SS", () => {
expect(formatDuration(65)).toBe("1:05"); // 1 minute 5 seconds
expect(formatDuration(125)).toBe("2:05"); // 2 minutes 5 seconds
expect(formatDuration(45)).toBe("0:45"); // 45 seconds
expect(formatDuration(0)).toBe("00:00");
});
it("should format seconds to H:MM:SS for hours", () => {
expect(formatDuration(3665)).toBe("1:01:05"); // 1 hour 1 minute 5 seconds
expect(formatDuration(3600)).toBe("1:00:00"); // 1 hour
expect(formatDuration(7325)).toBe("2:02:05"); // 2 hours 2 minutes 5 seconds
});
it("should format numeric string", () => {
expect(formatDuration("65")).toBe("1:05");
expect(formatDuration("3665")).toBe("1:01:05");
});
it("should return 00:00 for invalid input", () => {
expect(formatDuration("invalid")).toBe("00:00");
expect(formatDuration(NaN)).toBe("00:00");
});
});
describe("formatSize", () => {
it('should return "0 B" for undefined', () => {
expect(formatSize(undefined)).toBe("0 B");
});
it("should format bytes", () => {
expect(formatSize(0)).toBe("0 B");
expect(formatSize(500)).toBe("500 B");
expect(formatSize(1023)).toBe("1023 B");
});
it("should format kilobytes", () => {
expect(formatSize(1024)).toBe("1 KB");
expect(formatSize(1536)).toBe("1.5 KB");
expect(formatSize(2048)).toBe("2 KB");
expect(formatSize(10240)).toBe("10 KB");
});
it("should format megabytes", () => {
expect(formatSize(1048576)).toBe("1 MB"); // 1024 * 1024
expect(formatSize(1572864)).toBe("1.5 MB");
expect(formatSize(5242880)).toBe("5 MB");
});
it("should format gigabytes", () => {
expect(formatSize(1073741824)).toBe("1 GB"); // 1024^3
expect(formatSize(2147483648)).toBe("2 GB");
});
it("should format terabytes", () => {
expect(formatSize(1099511627776)).toBe("1 TB"); // 1024^4
});
it("should format numeric string", () => {
expect(formatSize("1024")).toBe("1 KB");
expect(formatSize("1048576")).toBe("1 MB");
});
it('should return "0 B" for invalid input', () => {
expect(formatSize("invalid")).toBe("0 B");
expect(formatSize(NaN)).toBe("0 B");
});
});
describe("formatDate", () => {
it('should return "Unknown date" for undefined', () => {
expect(formatDate(undefined)).toBe("Unknown date");
});
it('should return "Unknown date" for invalid length', () => {
expect(formatDate("202301")).toBe("Unknown date");
expect(formatDate("202301011")).toBe("Unknown date");
expect(formatDate("2023")).toBe("Unknown date");
});
it("should format YYYYMMDD to YYYY-MM-DD", () => {
expect(formatDate("20230101")).toBe("2023-01-01");
expect(formatDate("20231225")).toBe("2023-12-25");
expect(formatDate("20200101")).toBe("2020-01-01");
expect(formatDate("20230228")).toBe("2023-02-28");
});
it("should handle edge cases", () => {
expect(formatDate("19991231")).toBe("1999-12-31");
expect(formatDate("20991231")).toBe("2099-12-31");
});
});
describe("formatRelativeDownloadTime", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
const mockTranslation = (
key: string,
replacements?: Record<string, string | number>
) => {
const translations: Record<string, string> = {
justNow: "Just now",
hoursAgo: `${replacements?.hours || 0} hours ago`,
today: "Today",
thisWeek: "This week",
weeksAgo: `${replacements?.weeks || 0} weeks ago`,
unknownDate: "Unknown date",
};
return translations[key] || key;
};
it('should return "Just now" for less than 1 hour', () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const thirtyMinutesAgo = new Date("2023-01-01T11:30:00Z").toISOString();
expect(
formatRelativeDownloadTime(thirtyMinutesAgo, undefined, mockTranslation)
).toBe("Just now");
});
it('should return "X hours ago" for 1-5 hours', () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const twoHoursAgo = new Date("2023-01-01T10:00:00Z").toISOString();
expect(
formatRelativeDownloadTime(twoHoursAgo, undefined, mockTranslation)
).toBe("2 hours ago");
});
it('should return "Today" for 5-24 hours', () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const tenHoursAgo = new Date("2023-01-01T02:00:00Z").toISOString();
expect(
formatRelativeDownloadTime(tenHoursAgo, undefined, mockTranslation)
).toBe("Today");
});
it('should return "This week" for 1-7 days', () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const threeDaysAgo = new Date("2022-12-29T12:00:00Z").toISOString();
expect(
formatRelativeDownloadTime(threeDaysAgo, undefined, mockTranslation)
).toBe("This week");
});
it('should return "X weeks ago" for 1-4 weeks', () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const twoWeeksAgo = new Date("2022-12-18T12:00:00Z").toISOString();
expect(
formatRelativeDownloadTime(twoWeeksAgo, undefined, mockTranslation)
).toBe("2 weeks ago");
});
it("should return formatted date for > 4 weeks", () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const sixWeeksAgo = new Date("2022-11-20T12:00:00Z").toISOString();
const result = formatRelativeDownloadTime(
sixWeeksAgo,
"20221120",
mockTranslation
);
expect(result).toBe("2022-11-20");
});
it("should use originalDate when provided for > 4 weeks", () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const sixWeeksAgo = new Date("2022-11-20T12:00:00Z").toISOString();
expect(
formatRelativeDownloadTime(sixWeeksAgo, "20221120", mockTranslation)
).toBe("2022-11-20");
});
it('should fallback to "Unknown date" when no timestamp provided', () => {
expect(
formatRelativeDownloadTime(undefined, undefined, mockTranslation)
).toBe("Unknown date");
});
it("should use originalDate when no timestamp provided", () => {
expect(
formatRelativeDownloadTime(undefined, "20230101", mockTranslation)
).toBe("2023-01-01");
});
it("should fallback to English when no translation function provided", () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
const thirtyMinutesAgo = new Date("2023-01-01T11:30:00Z").toISOString();
expect(formatRelativeDownloadTime(thirtyMinutesAgo)).toBe("Just now");
});
it("should handle invalid date", () => {
expect(
formatRelativeDownloadTime("invalid-date", undefined, mockTranslation)
).toBe("Unknown date");
});
it("should use originalDate when date is invalid", () => {
expect(
formatRelativeDownloadTime("invalid-date", "20230101", mockTranslation)
).toBe("2023-01-01");
});
it("should format date in UTC to avoid timezone issues", () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
// Use a date that could be affected by timezone (midnight UTC)
const sixWeeksAgo = new Date("2022-11-20T00:00:00Z").toISOString();
// Should format as 2022-11-20 regardless of system timezone
const result = formatRelativeDownloadTime(
sixWeeksAgo,
undefined,
mockTranslation
);
expect(result).toBe("2022-11-20");
});
it("should handle date formatting across timezone boundaries", () => {
const now = new Date("2023-01-01T12:00:00Z");
vi.setSystemTime(now);
// Test with a date near midnight UTC to catch timezone edge cases
const sixWeeksAgo = new Date("2022-11-20T23:59:59Z").toISOString();
const result = formatRelativeDownloadTime(
sixWeeksAgo,
undefined,
mockTranslation
);
expect(result).toBe("2022-11-20");
});
});
});

View File

@@ -81,6 +81,106 @@ export const formatDate = (dateString?: string) => {
return `${year}-${month}-${day}`;
};
/**
* Format relative time from download timestamp to current time
* 0 - 1 hour: "Just now"
* 1 hour - 5 hours: "X hours ago"
* 5 hours - 24 hours: "Today"
* 1 day - 7 days: "This week"
* 1 week - 4 weeks: "X weeks ago"
* > 4 weeks: show actual date
*/
export const formatRelativeDownloadTime = (
downloadTimestamp?: string,
originalDate?: string,
t?: (key: string, replacements?: Record<string, string | number>) => string
): string => {
const getTranslation = (
key: string,
replacements?: Record<string, string | number>
): string => {
if (t) {
return t(key as any, replacements);
}
// Fallback to English if no translation function provided
const fallbacks: Record<string, string> = {
justNow: "Just now",
hoursAgo: "{hours} hours ago",
today: "Today",
thisWeek: "This week",
weeksAgo: "{weeks} weeks ago",
unknownDate: "Unknown date",
};
let text = fallbacks[key] || key;
if (replacements) {
Object.entries(replacements).forEach(([placeholder, value]) => {
text = text.replace(`{${placeholder}}`, String(value));
});
}
return text;
};
if (!downloadTimestamp) {
// Fallback to original date format if no download timestamp
return originalDate
? formatDate(originalDate)
: getTranslation("unknownDate");
}
const downloadDate = new Date(downloadTimestamp);
const now = new Date();
// Check if date is valid
if (isNaN(downloadDate.getTime())) {
return originalDate
? formatDate(originalDate)
: getTranslation("unknownDate");
}
const diffMs = now.getTime() - downloadDate.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
const diffDays = diffMs / (1000 * 60 * 60 * 24);
const diffWeeks = diffDays / 7;
// 0 - 1 hour: "Just now"
if (diffHours < 1) {
return getTranslation("justNow");
}
// 1 hour - 5 hours: "X hours ago"
if (diffHours >= 1 && diffHours < 5) {
const hours = Math.floor(diffHours);
return getTranslation("hoursAgo", { hours });
}
// 5 hours - 24 hours: "Today"
if (diffHours >= 5 && diffHours < 24) {
return getTranslation("today");
}
// 1 day - 7 days: "This week"
if (diffDays >= 1 && diffDays < 7) {
return getTranslation("thisWeek");
}
// 1 week - 4 weeks: "X周前" / "X weeks ago"
if (diffWeeks >= 1 && diffWeeks < 4) {
const weeks = Math.floor(diffWeeks);
return getTranslation("weeksAgo", { weeks });
}
// > 4 weeks: show actual date
if (originalDate) {
return formatDate(originalDate);
}
// Format download date as YYYY-MM-DD if no original date
// Use UTC methods to ensure timezone independence
const year = downloadDate.getUTCFullYear();
const month = String(downloadDate.getUTCMonth() + 1).padStart(2, "0");
const day = String(downloadDate.getUTCDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
/**
* Generate timestamp string in format YYYY-MM-DD-HH-MM-SS
* Matches the backend generateTimestamp() function format
@@ -101,14 +201,17 @@ export const generateTimestamp = (): string => {
* If path is already a full URL (starts with http:// or https://), return it as is
* Otherwise, prepend BACKEND_URL
*/
export const getFileUrl = (path: string | null | undefined, backendUrl: string): string | undefined => {
export const getFileUrl = (
path: string | null | undefined,
backendUrl: string
): string | undefined => {
if (!path) return undefined;
// Check if path is already a full URL
if (path.startsWith("http://") || path.startsWith("https://")) {
return path;
}
// Otherwise, prepend backend URL
return `${backendUrl}${path}`;
};

View File

@@ -366,6 +366,11 @@ export const ar = {
unknownDate: "تاريخ غير معروف",
part: "جزء",
collection: "مجموعة",
justNow: "الآن",
hoursAgo: "منذ {hours} ساعة",
today: "اليوم",
thisWeek: "هذا الأسبوع",
weeksAgo: "منذ {weeks} أسبوع",
// Upload Modal
selectVideoFile: "اختر ملف فيديو",

View File

@@ -334,6 +334,11 @@ export const de = {
unknownDate: "Unbekanntes Datum",
part: "Teil",
collection: "Sammlung",
justNow: "Gerade eben",
hoursAgo: "vor {hours} Stunden",
today: "Heute",
thisWeek: "Diese Woche",
weeksAgo: "vor {weeks} Wochen",
selectVideoFile: "Videodatei Auswählen",
pleaseSelectVideo: "Bitte wählen Sie eine Videodatei aus",
uploadFailed: "Upload fehlgeschlagen",

View File

@@ -375,6 +375,11 @@ export const en = {
part: "Part",
collection: "Collection",
new: "NEW",
justNow: "Just now",
hoursAgo: "{hours} hours ago",
today: "Today",
thisWeek: "This week",
weeksAgo: "{weeks} weeks ago",
// Upload Modal
selectVideoFile: "Select Video File",

View File

@@ -358,6 +358,11 @@ export const es = {
unknownDate: "Fecha desconocida",
part: "Parte",
collection: "Colección",
justNow: "Ahora mismo",
hoursAgo: "Hace {hours} horas",
today: "Hoy",
thisWeek: "Esta semana",
weeksAgo: "Hace {weeks} semanas",
selectVideoFile: "Seleccionar Archivo de Video",
pleaseSelectVideo: "Por favor seleccione un archivo de video",
uploadFailed: "Carga fallida",

View File

@@ -381,6 +381,11 @@ export const fr = {
unknownDate: "Date inconnue",
part: "Partie",
collection: "Collection",
justNow: "À l'instant",
hoursAgo: "Il y a {hours} heures",
today: "Aujourd'hui",
thisWeek: "Cette semaine",
weeksAgo: "Il y a {weeks} semaines",
// Upload Modal
selectVideoFile: "Sélectionner un fichier vidéo",

View File

@@ -359,6 +359,11 @@ export const ja = {
unknownDate: "不明な日付",
part: "パート",
collection: "コレクション",
justNow: "たった今",
hoursAgo: "{hours}時間前",
today: "今日",
thisWeek: "今週",
weeksAgo: "{weeks}週間前",
// Upload Modal
selectVideoFile: "動画ファイルを選択",

View File

@@ -356,6 +356,11 @@ export const ko = {
unknownDate: "알 수 없는 날짜",
part: "파트",
collection: "컬렉션",
justNow: "방금",
hoursAgo: "{hours}시간 전",
today: "오늘",
thisWeek: "이번 주",
weeksAgo: "{weeks}주 전",
// Upload Modal
selectVideoFile: "동영상 파일 선택",

View File

@@ -364,6 +364,11 @@ export const pt = {
unknownDate: "Data desconhecida",
part: "Parte",
collection: "Coleção",
justNow: "Agora mesmo",
hoursAgo: "Há {hours} horas",
today: "Hoje",
thisWeek: "Esta semana",
weeksAgo: "Há {weeks} semanas",
// Upload Modal
selectVideoFile: "Selecionar Arquivo de Vídeo",

View File

@@ -374,6 +374,11 @@ export const ru = {
unknownDate: "Неизвестная дата",
part: "Часть",
collection: "Коллекция",
justNow: "Только что",
hoursAgo: "{hours} часов назад",
today: "Сегодня",
thisWeek: "На этой неделе",
weeksAgo: "{weeks} недель назад",
// Upload Modal
selectVideoFile: "Выберите видеофайл",

View File

@@ -357,6 +357,11 @@ export const zh = {
unknownDate: "未知日期",
part: "分P",
collection: "合集",
justNow: "刚刚",
hoursAgo: "{hours}小时前",
today: "今天",
thisWeek: "本周",
weeksAgo: "{weeks}周前",
// Upload Modal
selectVideoFile: "选择视频文件",