mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
Merge pull request #1122 from sanitz/fix/week-allday-visible-week
fix(calendar): size the week view all-day area from the visible week
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { CalendarWeekView } from '../calendar-week-view';
|
||||
import type { CalendarEvent, Calendar, CalendarTask } from '@/lib/jmap/types';
|
||||
|
||||
const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[];
|
||||
|
||||
function makeTask(id: string, due: string, overrides: Partial<CalendarTask> = {}): CalendarTask {
|
||||
return {
|
||||
id,
|
||||
'@type': 'Task',
|
||||
uid: id,
|
||||
title: 'Task ' + id,
|
||||
description: '',
|
||||
due,
|
||||
start: null,
|
||||
duration: null,
|
||||
timeZone: null,
|
||||
showWithoutTime: true,
|
||||
progress: 'needs-action',
|
||||
priority: 0,
|
||||
privacy: 'public',
|
||||
keywords: null,
|
||||
categories: null,
|
||||
color: null,
|
||||
created: null,
|
||||
updated: '2026-09-01T00:00:00Z',
|
||||
recurrenceRules: null,
|
||||
alerts: null,
|
||||
relatedTo: null,
|
||||
calendarIds: { 'cal-1': true },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeAllDayEvent(id: string, start: string): CalendarEvent {
|
||||
return {
|
||||
id,
|
||||
'@type': 'Event',
|
||||
uid: id,
|
||||
title: 'Event ' + id,
|
||||
start,
|
||||
duration: 'P1D',
|
||||
showWithoutTime: true,
|
||||
calendarIds: { 'cal-1': true },
|
||||
} as unknown as CalendarEvent;
|
||||
}
|
||||
|
||||
type Props = React.ComponentProps<typeof CalendarWeekView>;
|
||||
|
||||
function renderView(overrides: Partial<Props> = {}) {
|
||||
const props: Props = {
|
||||
focus: { date: new Date(2026, 8, 9), nonce: 0 },
|
||||
windowKey: 'week:2026-09-07',
|
||||
// Monday 7 Sep to Sunday 13 Sep 2026
|
||||
rangeStart: new Date(2026, 8, 7),
|
||||
rangeEnd: new Date(2026, 8, 13),
|
||||
selectedDate: new Date(2026, 8, 9),
|
||||
events: [],
|
||||
calendars,
|
||||
onSelectDate: vi.fn(),
|
||||
onSelectEvent: vi.fn(),
|
||||
onCreateAtTime: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
return { props, ...render(<CalendarWeekView {...props} />) };
|
||||
}
|
||||
|
||||
function chipSlot(taskId: string): HTMLElement {
|
||||
const chip = document.querySelector<HTMLElement>(`[data-calendar-task="${taskId}"]`);
|
||||
if (!chip?.parentElement) throw new Error(`no chip for ${taskId}`);
|
||||
return chip.parentElement;
|
||||
}
|
||||
|
||||
describe('CalendarWeekView all-day and tasks height cap', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('stacks tasks directly below events of their own day instead of global allDayRowCount', () => {
|
||||
// Monday 2026-09-07 has 2 all-day events
|
||||
// Tuesday 2026-09-08 has NO events, but 1 task
|
||||
renderView({
|
||||
events: [
|
||||
makeAllDayEvent('e1', '2026-09-07'),
|
||||
makeAllDayEvent('e2', '2026-09-07'),
|
||||
],
|
||||
tasks: [
|
||||
makeTask('t-tue', '2026-09-08'),
|
||||
],
|
||||
});
|
||||
|
||||
// Tuesday's task must start at row 0 (top: 2px), NOT pushed down to row 2 (top: 50px)
|
||||
const tueSlot = chipSlot('t-tue');
|
||||
expect(tueSlot.style.top).toBe('2px');
|
||||
});
|
||||
|
||||
it('caps all-day height to default max rows (3 rows = 76px) and provides an expand toggle when crowded', () => {
|
||||
// Monday 2026-09-07 has 5 tasks (rows 0..4)
|
||||
renderView({
|
||||
tasks: [
|
||||
makeTask('t1', '2026-09-07'),
|
||||
makeTask('t2', '2026-09-07'),
|
||||
makeTask('t3', '2026-09-07'),
|
||||
makeTask('t4', '2026-09-07'),
|
||||
makeTask('t5', '2026-09-07'),
|
||||
],
|
||||
});
|
||||
|
||||
const toggle = screen.getByRole('button', { name: /all-day-expand/i });
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
// Default height is capped to 3 rows (3 * 24 + 4 = 76px)
|
||||
const allDayGrid = document.querySelector<HTMLElement>('[data-testid="all-day-grid"]');
|
||||
expect(allDayGrid).toBeInTheDocument();
|
||||
expect(allDayGrid?.style.height).toBe('76px');
|
||||
|
||||
// Click toggle to expand
|
||||
fireEvent.click(toggle);
|
||||
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(allDayGrid?.style.height).toBe('124px'); // 5 * 24 + 4
|
||||
});
|
||||
|
||||
it('sizes the all-day area from the visible week, not from other loaded weeks (#1293)', () => {
|
||||
// Three weeks loaded (free scrolling), the focused first week is empty;
|
||||
// a crowded day two weeks later must not add height or a toggle here.
|
||||
renderView({
|
||||
rangeStart: new Date(2026, 8, 7),
|
||||
rangeEnd: new Date(2026, 8, 27),
|
||||
windowKey: 'week:2026-09-07:3w',
|
||||
events: [
|
||||
makeAllDayEvent('e1', '2026-09-23'),
|
||||
makeAllDayEvent('e2', '2026-09-23'),
|
||||
makeAllDayEvent('e3', '2026-09-23'),
|
||||
makeAllDayEvent('e4', '2026-09-23'),
|
||||
],
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: /all-day-expand/i })).toBeNull();
|
||||
expect(document.querySelector('[data-testid="all-day-grid"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('still sizes the all-day area from entries in the visible week (#1293)', () => {
|
||||
renderView({
|
||||
rangeStart: new Date(2026, 8, 7),
|
||||
rangeEnd: new Date(2026, 8, 27),
|
||||
windowKey: 'week:2026-09-07:3w',
|
||||
events: [
|
||||
makeAllDayEvent('e1', '2026-09-08'),
|
||||
makeAllDayEvent('e2', '2026-09-23'),
|
||||
makeAllDayEvent('e3', '2026-09-23'),
|
||||
makeAllDayEvent('e4', '2026-09-23'),
|
||||
],
|
||||
});
|
||||
|
||||
const allDayGrid = document.querySelector<HTMLElement>('[data-testid="all-day-grid"]');
|
||||
expect(allDayGrid?.style.height).toBe('28px');
|
||||
expect(screen.queryByRole('button', { name: /all-day-expand/i })).toBeNull();
|
||||
});
|
||||
|
||||
it('resizes the all-day area when scrolling to a crowded week (#1293)', async () => {
|
||||
renderView({
|
||||
rangeStart: new Date(2026, 8, 7),
|
||||
rangeEnd: new Date(2026, 8, 27),
|
||||
windowKey: 'week:2026-09-07:3w',
|
||||
events: [
|
||||
makeAllDayEvent('e1', '2026-09-23'),
|
||||
makeAllDayEvent('e2', '2026-09-23'),
|
||||
makeAllDayEvent('e3', '2026-09-23'),
|
||||
makeAllDayEvent('e4', '2026-09-23'),
|
||||
],
|
||||
});
|
||||
expect(document.querySelector('[data-testid="all-day-grid"]')).toBeNull();
|
||||
|
||||
// Scroll the strip to Monday 21 Sep: 14 columns of the measured width
|
||||
const strip = document.querySelector<HTMLElement>('[role="grid"]')!;
|
||||
const columns = document.querySelector<HTMLElement>('[style*="grid-template-columns"]')!;
|
||||
const colWidth = parseInt(/(\d+)px/.exec(columns.style.gridTemplateColumns)![1], 10);
|
||||
strip.scrollLeft = 14 * colWidth;
|
||||
fireEvent.scroll(strip);
|
||||
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
||||
|
||||
const allDayGrid = await screen.findByTestId('all-day-grid');
|
||||
expect(allDayGrid.style.height).toBe('76px');
|
||||
expect(screen.getByRole('button', { name: /all-day-expand/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -100,15 +100,22 @@ export function CalendarWeekView({
|
||||
return Math.max(MIN_COL_WIDTH, Math.floor((root.clientWidth - gutterWidth) / 7));
|
||||
}, [isMobile, gutterWidth]);
|
||||
const [colWidth, setColWidth] = useState(MOBILE_COL_WIDTH);
|
||||
const [viewportWidth, setViewportWidth] = useState(0);
|
||||
// First day in view, reported by the scroll handler below (#1293)
|
||||
const [visibleDayKey, setVisibleDayKey] = useState<string | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root) return;
|
||||
setColWidth(measureColWidth(root));
|
||||
const measure = () => {
|
||||
setColWidth(measureColWidth(root));
|
||||
setViewportWidth(Math.max(0, root.clientWidth - gutterWidth));
|
||||
};
|
||||
measure();
|
||||
if (typeof ResizeObserver === "undefined") return;
|
||||
const observer = new ResizeObserver(() => setColWidth(measureColWidth(root)));
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(root);
|
||||
return () => observer.disconnect();
|
||||
}, [measureColWidth]);
|
||||
}, [measureColWidth, gutterWidth]);
|
||||
|
||||
// Every scroll offset is computed with the column width that is rendered.
|
||||
// When that width changes (first measurement, resize) keep the same day at
|
||||
@@ -177,26 +184,55 @@ export function CalendarWeekView({
|
||||
return packWeekSegments([...explicitAllDay, ...timedFullDay]);
|
||||
}, [events, days]);
|
||||
|
||||
const allDayRowCount = useMemo(() => {
|
||||
return allDaySegments.reduce((maxRows, segment) => Math.max(maxRows, segment.row + 1), 0);
|
||||
}, [allDaySegments]);
|
||||
|
||||
const tasksByDay = useMemo(() => groupTasksByDueDay(tasks), [tasks]);
|
||||
|
||||
// Max tasks on any single loaded day
|
||||
const taskRowCount = useMemo(() => {
|
||||
// Tasks stack under the events of their own day rather than globally (#1107, #1270)
|
||||
const dayEventRows = useMemo(() => {
|
||||
return days.map((_, dayIndex) =>
|
||||
allDaySegments.reduce(
|
||||
(rows, segment) =>
|
||||
dayIndex >= segment.startIndex && dayIndex < segment.startIndex + segment.span
|
||||
? Math.max(rows, segment.row + 1)
|
||||
: rows,
|
||||
0,
|
||||
)
|
||||
);
|
||||
}, [days, allDaySegments]);
|
||||
|
||||
// The strip holds weeks to months of days (#759), but the all-day area is
|
||||
// sized from the columns in view: a crowded day in another week must not
|
||||
// add empty rows or an expand toggle here (#1293). The first visible day is
|
||||
// tracked by date, so columns prepended while loading do not shift it.
|
||||
const visibleStartIndex = useMemo(() => {
|
||||
const tracked = visibleDayKey ? days.findIndex((d) => dayKey(d) === visibleDayKey) : -1;
|
||||
if (tracked >= 0) return tracked;
|
||||
const target = isMobile ? focus.date : startOfWeek(focus.date, { weekStartsOn: weekStart });
|
||||
return Math.max(0, Math.min(days.length - 1, differenceInCalendarDays(target, rangeStart)));
|
||||
}, [visibleDayKey, days, isMobile, focus.date, weekStart, rangeStart]);
|
||||
const visibleColumns = isMobile ? Math.max(1, Math.ceil(viewportWidth / colWidth)) : 7;
|
||||
|
||||
// Max combined rows (events + tasks) across the visible days
|
||||
const maxContentRows = useMemo(() => {
|
||||
let max = 0;
|
||||
for (const day of days) {
|
||||
const key = format(day, "yyyy-MM-dd");
|
||||
const count = tasksByDay.get(key)?.length ?? 0;
|
||||
if (count > max) max = count;
|
||||
const end = Math.min(days.length, visibleStartIndex + visibleColumns);
|
||||
for (let i = visibleStartIndex; i < end; i++) {
|
||||
const key = format(days[i], "yyyy-MM-dd");
|
||||
const taskCount = tasksByDay.get(key)?.length ?? 0;
|
||||
const total = dayEventRows[i] + taskCount;
|
||||
if (total > max) max = total;
|
||||
}
|
||||
return max;
|
||||
}, [tasksByDay, days]);
|
||||
}, [days, dayEventRows, tasksByDay, visibleStartIndex, visibleColumns]);
|
||||
|
||||
const hasAllDay = useMemo(() => {
|
||||
return allDaySegments.length > 0 || taskRowCount > 0;
|
||||
}, [allDaySegments, taskRowCount]);
|
||||
return maxContentRows > 0;
|
||||
}, [maxContentRows]);
|
||||
|
||||
const DEFAULT_ALL_DAY_MAX_ROWS = 3;
|
||||
const [isAllDayExpanded, setIsAllDayExpanded] = useState(false);
|
||||
const isExpandable = maxContentRows > DEFAULT_ALL_DAY_MAX_ROWS;
|
||||
const visibleRows = isAllDayExpanded ? maxContentRows : Math.min(DEFAULT_ALL_DAY_MAX_ROWS, maxContentRows);
|
||||
const allDayHeight = Math.max(28, visibleRows * 24 + 4);
|
||||
|
||||
useEffect(() => {
|
||||
if (rootRef.current) {
|
||||
@@ -258,6 +294,7 @@ export function CalendarWeekView({
|
||||
const key = dayKey(day);
|
||||
if (key === visibleKeyRef.current) return;
|
||||
visibleKeyRef.current = key;
|
||||
setVisibleDayKey(key);
|
||||
onVisibleDateChange?.(day);
|
||||
});
|
||||
}, [colWidth, colCount, days, onVisibleDateChange]);
|
||||
@@ -323,14 +360,34 @@ export function CalendarWeekView({
|
||||
{hasAllDay && (
|
||||
<div className="flex border-b border-border">
|
||||
<div
|
||||
className={cn(gutterClass, "text-[10px] text-muted-foreground p-1 text-end")}
|
||||
style={{ minHeight: Math.max(28, (allDayRowCount + taskRowCount) * 24 + 4) }}
|
||||
className={cn(gutterClass, "text-[10px] text-muted-foreground p-1 text-end flex flex-col justify-between")}
|
||||
style={{ height: allDayHeight }}
|
||||
>
|
||||
{t("events.all_day")}
|
||||
<span>{t("events.all_day")}</span>
|
||||
{isExpandable && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="all-day-expand"
|
||||
aria-expanded={isAllDayExpanded}
|
||||
onClick={() => setIsAllDayExpanded((prev) => !prev)}
|
||||
className="self-end mt-auto text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-0.5 rounded px-1 py-0.5 hover:bg-muted transition-colors cursor-pointer"
|
||||
title={isAllDayExpanded ? t("events.show_less") : t("events.show_more")}
|
||||
>
|
||||
{isAllDayExpanded ? (
|
||||
<span>▲</span>
|
||||
) : (
|
||||
<>
|
||||
<span>+{maxContentRows - DEFAULT_ALL_DAY_MAX_ROWS}</span>
|
||||
<span>▼</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative grid border-s border-border"
|
||||
style={{ ...columnsStyle, minHeight: Math.max(28, (allDayRowCount + taskRowCount) * 24 + 4) }}
|
||||
data-testid="all-day-grid"
|
||||
className="relative grid border-s border-border overflow-hidden"
|
||||
style={{ ...columnsStyle, height: allDayHeight }}
|
||||
>
|
||||
{days.map((day) => (
|
||||
<div
|
||||
@@ -371,34 +428,33 @@ export function CalendarWeekView({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Task chips in all-day area */}
|
||||
{taskRowCount > 0 && (
|
||||
<div className="absolute inset-x-0 pointer-events-none" style={{ top: allDayRowCount * 24 + 2 }}>
|
||||
{days.map((day, dayIndex) => {
|
||||
const key = format(day, "yyyy-MM-dd");
|
||||
const dayTasks = tasksByDay.get(key) || [];
|
||||
return dayTasks.map((task, taskIndex) => (
|
||||
<div
|
||||
key={`task-${task.id}`}
|
||||
className="absolute px-0.5 pointer-events-auto"
|
||||
style={{
|
||||
left: `calc(${(dayIndex / colCount) * 100}% + 1px)`,
|
||||
width: `calc(${(1 / colCount) * 100}% - 2px)`,
|
||||
top: taskIndex * 24,
|
||||
height: 20,
|
||||
}}
|
||||
>
|
||||
<CalendarTaskChip
|
||||
task={task}
|
||||
calendar={calendars.find(c => task.calendarIds[c.id])}
|
||||
onToggleComplete={onToggleTaskComplete}
|
||||
onSelect={onSelectTask}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{/* Task chips in all-day area, stacked under events of their day (#1107, #1270) */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{days.map((day, dayIndex) => {
|
||||
const key = format(day, "yyyy-MM-dd");
|
||||
const dayTasks = tasksByDay.get(key) || [];
|
||||
const baseRow = dayEventRows[dayIndex];
|
||||
return dayTasks.map((task, taskIndex) => (
|
||||
<div
|
||||
key={`task-${task.id}`}
|
||||
className="absolute px-0.5 pointer-events-auto"
|
||||
style={{
|
||||
left: `calc(${(dayIndex / colCount) * 100}% + 1px)`,
|
||||
width: `calc(${(1 / colCount) * 100}% - 2px)`,
|
||||
top: (baseRow + taskIndex) * 24 + 2,
|
||||
height: 20,
|
||||
}}
|
||||
>
|
||||
<CalendarTaskChip
|
||||
task={task}
|
||||
calendar={calendars.find(c => task.calendarIds[c.id])}
|
||||
onToggleComplete={onToggleTaskComplete}
|
||||
onSelect={onSelectTask}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "لا توجد أحداث",
|
||||
"all_day": "طوال اليوم",
|
||||
"more": "+{count} أخرى",
|
||||
"show_more": "إظهار المزيد",
|
||||
"show_less": "إظهار أقل",
|
||||
"no_title": "(بلا عنوان)",
|
||||
"resize": "تغيير حجم الحدث",
|
||||
"duplicate": "تكرار",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Cap esdeveniment",
|
||||
"all_day": "Tot el dia",
|
||||
"more": "+{count} més",
|
||||
"show_more": "Mostra'n més",
|
||||
"show_less": "Mostra'n menys",
|
||||
"no_title": "(Sense títol)",
|
||||
"resize": "Redimensiona l'esdeveniment",
|
||||
"duplicate": "Duplica",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Žádné události",
|
||||
"all_day": "Celý den",
|
||||
"more": "+{count} dalších",
|
||||
"show_more": "Zobrazit více",
|
||||
"show_less": "Zobrazit méně",
|
||||
"no_title": "(Bez názvu)",
|
||||
"resize": "Změnit velikost události",
|
||||
"duplicate": "Duplikovat",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Ingen begivenheder",
|
||||
"all_day": "Hele dagen",
|
||||
"more": "+{count} mere",
|
||||
"show_more": "Vis mere",
|
||||
"show_less": "Vis mindre",
|
||||
"no_title": "(Ingen titel)",
|
||||
"resize": "Tilpas størrelse på begivenhed",
|
||||
"duplicate": "Duplikér",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Keine Termine",
|
||||
"all_day": "Ganztägig",
|
||||
"more": "+{count} weitere",
|
||||
"show_more": "Mehr anzeigen",
|
||||
"show_less": "Weniger anzeigen",
|
||||
"no_title": "(Kein Titel)",
|
||||
"resize": "Termingröße ändern",
|
||||
"duplicate": "Duplizieren",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "No events",
|
||||
"all_day": "All day",
|
||||
"more": "+{count} more",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"no_title": "(No title)",
|
||||
"resize": "Resize event",
|
||||
"duplicate": "Duplicate",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Sin eventos",
|
||||
"all_day": "Todo el día",
|
||||
"more": "+{count} más",
|
||||
"show_more": "Mostrar más",
|
||||
"show_less": "Mostrar menos",
|
||||
"no_title": "(Sin título)",
|
||||
"resize": "Redimensionar evento",
|
||||
"duplicate": "Duplicar",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "رویدادی نیست",
|
||||
"all_day": "تمام روز",
|
||||
"more": "+{count} بیشتر",
|
||||
"show_more": "نمایش بیشتر",
|
||||
"show_less": "نمایش کمتر",
|
||||
"no_title": "(بدون عنوان)",
|
||||
"resize": "تغییر اندازه رویداد",
|
||||
"duplicate": "کپی",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Aucun événement",
|
||||
"all_day": "Toute la journée",
|
||||
"more": "+{count} de plus",
|
||||
"show_more": "Afficher plus",
|
||||
"show_less": "Afficher moins",
|
||||
"no_title": "(Sans titre)",
|
||||
"resize": "Redimensionner l'événement",
|
||||
"duplicate": "Dupliquer",
|
||||
|
||||
@@ -2776,6 +2776,8 @@
|
||||
"no_events": "אין אירועים",
|
||||
"all_day": "כל היום",
|
||||
"more": "+{count}נוספים",
|
||||
"show_more": "הצג עוד",
|
||||
"show_less": "הצג פחות",
|
||||
"no_title": "(ללא כותרת)",
|
||||
"resize": "שנה את גודל האירוע",
|
||||
"duplicate": "שכפול",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Nincs esemény",
|
||||
"all_day": "Egész napos",
|
||||
"more": "+{count} további",
|
||||
"show_more": "Több mutatása",
|
||||
"show_less": "Kevesebb mutatása",
|
||||
"no_title": "(Nincs cím)",
|
||||
"resize": "Esemény átméretezése",
|
||||
"duplicate": "Duplikálás",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Nessun evento",
|
||||
"all_day": "Tutto il giorno",
|
||||
"more": "+{count} altri",
|
||||
"show_more": "Mostra di più",
|
||||
"show_less": "Mostra meno",
|
||||
"no_title": "(Senza titolo)",
|
||||
"resize": "Ridimensiona evento",
|
||||
"duplicate": "Duplica",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "予定なし",
|
||||
"all_day": "終日",
|
||||
"more": "他{count}件",
|
||||
"show_more": "展開する",
|
||||
"show_less": "折りたたむ",
|
||||
"no_title": "(タイトルなし)",
|
||||
"resize": "イベントのサイズ変更",
|
||||
"duplicate": "複製",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "일정이 없어요",
|
||||
"all_day": "하루 종일",
|
||||
"more": "+{count}개 더보기",
|
||||
"show_more": "더 보기",
|
||||
"show_less": "간략히 보기",
|
||||
"no_title": "(제목 없음)",
|
||||
"resize": "일정 크기 조절",
|
||||
"duplicate": "복제",
|
||||
|
||||
@@ -2851,6 +2851,8 @@
|
||||
"no_events": "Nav pasākumu",
|
||||
"all_day": "Visa diena",
|
||||
"more": "vēl {count}",
|
||||
"show_more": "Rādīt vairāk",
|
||||
"show_less": "Rādīt mazāk",
|
||||
"no_title": "(Bez virsraksta)",
|
||||
"resize": "Mainīt pasākuma laiku",
|
||||
"duplicate": "Dublēt",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Үйл явдал байхгүй",
|
||||
"all_day": "Бүтэн өдөр",
|
||||
"more": "+{count} дэлгэрэнгүй",
|
||||
"show_more": "Илүү харуулах",
|
||||
"show_less": "Бага харуулах",
|
||||
"no_title": "(Гарчиггүй)",
|
||||
"resize": "Үйл явдлын хэмжээг өөрчлөх",
|
||||
"duplicate": "Давхардсан",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Ingen hendelser",
|
||||
"all_day": "Hele dagen",
|
||||
"more": "+{count} til",
|
||||
"show_more": "Vis flere",
|
||||
"show_less": "Vis færre",
|
||||
"no_title": "(Ingen tittel)",
|
||||
"resize": "Endre størrelsen på hendelsen",
|
||||
"duplicate": "Dupliser",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Geen evenementen",
|
||||
"all_day": "Hele dag",
|
||||
"more": "+{count} meer",
|
||||
"show_more": "Meer tonen",
|
||||
"show_less": "Minder tonen",
|
||||
"no_title": "(Geen titel)",
|
||||
"resize": "Evenement formaat wijzigen",
|
||||
"duplicate": "Dupliceren",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Brak wydarzeń",
|
||||
"all_day": "Cały dzień",
|
||||
"more": "+{count} więcej",
|
||||
"show_more": "Pokaż więcej",
|
||||
"show_less": "Pokaż mniej",
|
||||
"no_title": "(Bez tytułu)",
|
||||
"resize": "Zmień rozmiar wydarzenia",
|
||||
"duplicate": "Duplikuj",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Nenhum evento",
|
||||
"all_day": "Dia inteiro",
|
||||
"more": "+{count} mais",
|
||||
"show_more": "Mostrar mais",
|
||||
"show_less": "Mostrar menos",
|
||||
"no_title": "(Sem título)",
|
||||
"resize": "Redimensionar evento",
|
||||
"duplicate": "Duplicar",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Fără evenimente",
|
||||
"all_day": "Toată ziua",
|
||||
"more": "+{count} mai multe",
|
||||
"show_more": "Afișați mai mult",
|
||||
"show_less": "Afișați mai puțin",
|
||||
"no_title": "(Fără titlu)",
|
||||
"resize": "Eveniment de redimensionare",
|
||||
"duplicate": "Duplicare",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Нет событий",
|
||||
"all_day": "Весь день",
|
||||
"more": "ещё {count}",
|
||||
"show_more": "Развернуть",
|
||||
"show_less": "Свернуть",
|
||||
"no_title": "(Без названия)",
|
||||
"resize": "Изменить размер события",
|
||||
"duplicate": "Дублировать",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Žiadne udalosti",
|
||||
"all_day": "Celý deň",
|
||||
"more": "+{count} ďalších",
|
||||
"show_more": "Zobraziť viac",
|
||||
"show_less": "Zobraziť menej",
|
||||
"no_title": "(Bez názvu)",
|
||||
"resize": "Zmeniť veľkosť udalosti",
|
||||
"duplicate": "Duplikovať",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Etkinlik yok",
|
||||
"all_day": "Tüm gün",
|
||||
"more": "+{count} daha",
|
||||
"show_more": "Daha fazla göster",
|
||||
"show_less": "Daha az göster",
|
||||
"no_title": "(Başlık yok)",
|
||||
"resize": "Etkinliği yeniden boyutlandır",
|
||||
"duplicate": "Çoğalt",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "Подій немає",
|
||||
"all_day": "Весь день",
|
||||
"more": "+ ще {count}",
|
||||
"show_more": "Показувати більше",
|
||||
"show_less": "Показувати менше",
|
||||
"no_title": "(Без назви)",
|
||||
"resize": "Змінити розмір події",
|
||||
"duplicate": "дублікат",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "沒有活動",
|
||||
"all_day": "全天",
|
||||
"more": "另有 {count} 個",
|
||||
"show_more": "顯示更多",
|
||||
"show_less": "顯示較少",
|
||||
"no_title": "(無標題)",
|
||||
"resize": "調整活動大小",
|
||||
"duplicate": "建立副本",
|
||||
|
||||
@@ -2852,6 +2852,8 @@
|
||||
"no_events": "没有活动",
|
||||
"all_day": "全天",
|
||||
"more": "+{count} 更多",
|
||||
"show_more": "展开",
|
||||
"show_less": "收起",
|
||||
"no_title": "(无标题)",
|
||||
"resize": "调整事件大小",
|
||||
"duplicate": "复制",
|
||||
|
||||
Reference in New Issue
Block a user