fix: align calendar invitation icon with sender avatar column

This commit is contained in:
Linus Rath
2026-05-06 00:45:37 +02:00
parent b5e0189938
commit 2903e56cf6
18 changed files with 304 additions and 257 deletions
+154 -123
View File
@@ -37,6 +37,7 @@ import {
} from '@/lib/calendar-invitation'; } from '@/lib/calendar-invitation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { sanitizeColor } from '@/components/calendar/event-card'; import { sanitizeColor } from '@/components/calendar/event-card';
import { RecipientPopover } from './recipient-popover';
interface InvitationChangeItem { interface InvitationChangeItem {
label: string; label: string;
@@ -327,25 +328,26 @@ function buildParticipantsForRsvp(
); );
} }
function getMethodAccentClass(method: InvitationMethod, actorStatus?: string | null): string { function getMethodIconTone(method: InvitationMethod, actorStatus?: string | null): string {
switch (method) { switch (method) {
case 'cancel': case 'cancel':
case 'declinecounter': case 'declinecounter':
return 'border-l-red-500 dark:border-l-red-400'; return 'bg-destructive/15 text-destructive';
case 'request':
case 'add':
return 'border-l-blue-500 dark:border-l-blue-400';
case 'counter': case 'counter':
return 'border-l-amber-500 dark:border-l-amber-400'; return 'bg-warning/15 text-warning';
case 'reply': case 'reply':
switch (actorStatus) { switch (actorStatus) {
case 'accepted': return 'border-l-green-500 dark:border-l-green-400'; case 'accepted': return 'bg-success/15 text-success';
case 'tentative': return 'border-l-amber-500 dark:border-l-amber-400'; case 'tentative': return 'bg-warning/15 text-warning';
case 'declined': return 'border-l-red-500 dark:border-l-red-400'; case 'declined': return 'bg-destructive/15 text-destructive';
default: return 'border-l-blue-500 dark:border-l-blue-400'; default: return 'bg-primary/15 text-primary';
} }
case 'request':
case 'add':
case 'publish':
return 'bg-primary/15 text-primary';
default: default:
return 'border-l-slate-400 dark:border-l-slate-500'; return 'bg-muted text-muted-foreground';
} }
} }
@@ -500,7 +502,6 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
const actorName = actorSummary?.name || actorSummary?.email || t('actor_unknown'); const actorName = actorSummary?.name || actorSummary?.email || t('actor_unknown');
const actorStatus = getParticipationLabel(t, actorSummary?.participationStatus ?? null); const actorStatus = getParticipationLabel(t, actorSummary?.participationStatus ?? null);
const actorMessage = actorSummary ? getActorMessage(t, method, actorName, actorStatus) : null; const actorMessage = actorSummary ? getActorMessage(t, method, actorName, actorStatus) : null;
const actionFeedback = actionNotice;
// For REQUEST method, allow RSVP even if we can't find the user in participants: // For REQUEST method, allow RSVP even if we can't find the user in participants:
// the email was sent TO the user, so they are an attendee. handleRsvp handles // the email was sent TO the user, so they are an attendee. handleRsvp handles
// the import-then-find-participant flow for this case. // the import-then-find-participant flow for this case.
@@ -735,130 +736,151 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
} }
}; };
const accentClass = getMethodAccentClass(method, actorSummary?.participationStatus);
if (state === 'loading') { if (state === 'loading') {
return ( return (
<div className="rounded-lg border border-border bg-muted/20 px-4 py-3 flex items-center gap-2.5"> <div className="flex items-center gap-3">
<Calendar className="w-4 h-4 text-primary flex-shrink-0" /> <div className="w-10 h-10 rounded-full bg-muted text-muted-foreground flex items-center justify-center flex-shrink-0 shadow-sm">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" /> <Calendar className="w-5 h-5" />
<span className="text-sm text-muted-foreground">{t('loading')}</span> </div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>{t('loading')}</span>
</div>
</div> </div>
); );
} }
if (state === 'error') { if (state === 'error') {
return ( return (
<div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 flex items-center gap-2.5"> <div className="flex items-center gap-3">
<AlertCircle className="w-4 h-4 text-destructive flex-shrink-0" /> <div className="w-10 h-10 rounded-full bg-destructive/15 text-destructive flex items-center justify-center flex-shrink-0 shadow-sm">
<AlertCircle className="w-5 h-5" />
</div>
<span className="text-sm text-destructive">{t('parse_error')}</span> <span className="text-sm text-destructive">{t('parse_error')}</span>
</div> </div>
); );
} }
const iconTone = getMethodIconTone(method, actorSummary?.participationStatus);
const hasStatusPills = Boolean(
existingEvent
|| userIsOrganizer
|| (participationLabel && myParticipant)
|| actionNotice
|| (parsedEvent?.status && parsedEvent.status !== 'confirmed')
);
const showActionsRow = showDetails && (
canRespond
|| (supportsCalendar && !existingEvent && allowsImport && !isResponseOnly && !isCancellation)
|| canApplyProposal
|| (supportsCalendar && (existingEvent || parsedEvent))
|| !supportsCalendar
);
return ( return (
<div className={cn("rounded-lg border border-border overflow-hidden border-l-4", accentClass)}> <div className="flex items-start gap-3">
{/* Header */} {/* Avatar-style icon */}
<div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-border"> <div className={cn(
<div className="flex items-center gap-2 min-w-0"> "w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 shadow-sm",
iconTone,
)}>
{isCancellation ? ( {isCancellation ? (
<CalendarX className="w-4 h-4 text-destructive flex-shrink-0" /> <CalendarX className="w-5 h-5" />
) : ( ) : (
<Calendar className="w-4 h-4 text-primary flex-shrink-0" /> <Calendar className="w-5 h-5" />
)} )}
<span className="text-sm font-medium text-foreground truncate">{bannerTitle}</span>
</div> </div>
{/* Content column */}
<div className="flex-1 min-w-0 space-y-2">
{/* Eyebrow + title + collapse */}
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
{bannerTitle}
</div>
{summary?.title && (
<h3 className={cn(
"text-sm font-semibold leading-snug break-words",
isCancellation ? "line-through text-muted-foreground" : "text-foreground",
)}>
{summary.title}
</h3>
)}
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
{parsedEvent?.sequence != null && parsedEvent.sequence > 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground whitespace-nowrap">
{t('event_updated', { sequence: parsedEvent.sequence })}
</span>
)}
{canCollapse && ( {canCollapse && (
<button <button
type="button" type="button"
onClick={() => setIsCollapsed((prev) => !prev)} onClick={() => setIsCollapsed((prev) => !prev)}
aria-expanded={!isCollapsed} aria-expanded={!isCollapsed}
aria-label={isCollapsed ? t('expand') : t('collapse')} className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-0.5 transition-colors"
title={isCollapsed ? t('expand') : t('collapse')}
className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors flex-shrink-0"
> >
{isCollapsed ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />} {isCollapsed ? <ChevronDown className="w-3 h-3" /> : <ChevronUp className="w-3 h-3" />}
{isCollapsed ? t('expand') : t('collapse')}
</button> </button>
)} )}
</div> </div>
{/* Content */}
{showDetails && (
<div className="px-4 py-3 space-y-2.5">
<div className="lg:flex lg:gap-6">
{/* Left: Event info */}
<div className="lg:flex-1 space-y-2.5 min-w-0">
{/* Event title */}
{summary?.title && (
<div className="flex items-start justify-between gap-2">
<h3 className={cn(
"text-base font-semibold leading-snug",
isCancellation ? "line-through text-muted-foreground" : "text-foreground"
)}>
{summary.title}
</h3>
{parsedEvent?.sequence != null && parsedEvent.sequence > 0 && (
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground flex-shrink-0 whitespace-nowrap">
{t('event_updated', { sequence: parsedEvent.sequence })}
</span>
)}
</div> </div>
)}
{/* Event details */} {/* Meta rows */}
<div className="lg:flex lg:items-center lg:gap-4 lg:flex-wrap space-y-1 lg:space-y-0"> {showDetails && summary && (summary.start || summary.location || summary.attendeeCount > 0) && (
{summary?.start && ( <div className="flex flex-col gap-1 text-sm text-muted-foreground sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4 sm:gap-y-1">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground"> {summary.start && (
<span className="flex items-center gap-1.5 min-w-0">
<Clock className="w-3.5 h-3.5 flex-shrink-0" /> <Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span> <span className="truncate">
{formatDateTime(summary.start)} {formatDateTime(summary.start)}
{summary.end && ` ${formatDateTime(summary.end)}`} {summary.end && ` ${formatDateTime(summary.end)}`}
</span> </span>
</div>
)}
{summary?.location && (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<MapPin className="w-3.5 h-3.5 flex-shrink-0" />
<span>{summary.location}</span>
</div>
)}
{summary?.organizer && (
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Users className="w-3.5 h-3.5 flex-shrink-0" />
{t('organizer', { name: summary.organizer })}
</span> </span>
)} )}
{summary && summary.attendeeCount > 0 && ( {summary.location && (
<span className="text-sm text-muted-foreground">{t('attendees', { count: summary.attendeeCount })}</span> <span className="flex items-center gap-1.5 min-w-0">
<MapPin className="w-3.5 h-3.5 flex-shrink-0" />
<span className="truncate">{summary.location}</span>
</span>
)}
{summary.attendeeCount > 0 && (
<span className="text-muted-foreground/80">{t('attendees', { count: summary.attendeeCount })}</span>
)} )}
</div> </div>
</div> )}
{/* Right: Info & actor messages on large screens */} {/* Organizer row (clickable, left-aligned) */}
<div className="lg:flex-shrink-0 lg:text-right lg:max-w-xs mt-2.5 lg:mt-0 space-y-1"> {showDetails && summary?.organizer && (
{bannerInfo && ( <div className="flex items-center gap-1.5 text-sm text-muted-foreground min-w-0">
<p className="text-xs text-muted-foreground leading-relaxed">{bannerInfo}</p> <Users className="w-3.5 h-3.5 flex-shrink-0" />
)} <span className="flex-shrink-0">{t('organizer_label')}</span>
{actorMessage && ( {summary.organizerEmail ? (
<p className="text-xs text-muted-foreground">{actorMessage}</p> <RecipientPopover
)} name={summary.organizer}
{actorSummary?.participationComment && ( email={summary.organizerEmail}
<p className="text-xs text-muted-foreground italic"> className="text-sm truncate"
{t('actor_note', { comment: actorSummary.participationComment })} />
</p> ) : (
<span className="truncate text-foreground">{summary.organizer}</span>
)} )}
</div> </div>
</div> )}
{/* Status badges */} {/* Status pills */}
{(existingEvent || userIsOrganizer || (participationLabel && myParticipant) || actionFeedback || (parsedEvent?.status && parsedEvent.status !== 'confirmed')) && ( {showDetails && hasStatusPills && (
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex flex-wrap items-center gap-1.5">
{parsedEvent?.status && parsedEvent.status !== 'confirmed' && ( {parsedEvent?.status && parsedEvent.status !== 'confirmed' && (
<span className={cn( <span className={cn(
"rounded-full px-2 py-0.5 text-[11px] font-medium", "rounded-full px-2 py-0.5 text-[11px] font-medium",
parsedEvent.status === 'cancelled' parsedEvent.status === 'cancelled'
? "bg-destructive/15 text-destructive" ? "bg-destructive/15 text-destructive"
: "bg-warning/15 text-warning" : "bg-warning/15 text-warning",
)}> )}>
{t(`event_status_${parsedEvent.status}`)} {t(`event_status_${parsedEvent.status}`)}
</span> </span>
@@ -876,21 +898,45 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
{participationLabel && myParticipant && ( {participationLabel && myParticipant && (
<span className={cn( <span className={cn(
'rounded-full px-2 py-0.5 text-[11px] font-medium', 'rounded-full px-2 py-0.5 text-[11px] font-medium',
getParticipationTone(currentRsvp) getParticipationTone(currentRsvp),
)}> )}>
{t('your_response', { status: participationLabel })} {t('your_response', { status: participationLabel })}
</span> </span>
)} )}
{actionFeedback && ( {actionNotice && (
<span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success"> <span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success">
{actionFeedback} {actionNotice}
</span> </span>
)} )}
</div> </div>
)} )}
{/* Info / actor messages */}
{showDetails && (bannerInfo || actorMessage || actorSummary?.participationComment) && (
<div className="space-y-0.5 text-xs text-muted-foreground">
{bannerInfo && <p className="leading-relaxed">{bannerInfo}</p>}
{actorMessage && <p>{actorMessage}</p>}
{actorSummary?.participationComment && (
<p className="italic">{t('actor_note', { comment: actorSummary.participationComment })}</p>
)}
</div>
)}
{/* Trust warning */}
{showDetails && trustMessage && trustAssessment && (
<div className={cn(
'flex items-start gap-2 text-sm rounded-md px-3 py-2 border',
trustAssessment.level === 'warning'
? 'bg-destructive/10 text-destructive border-destructive/30'
: 'bg-warning/10 text-warning border-warning/30',
)}>
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span className="flex-1">{trustMessage}</span>
</div>
)}
{/* Proposed changes */} {/* Proposed changes */}
{proposedChanges.length > 0 && ( {showDetails && proposedChanges.length > 0 && (
<div className="rounded-md border border-border bg-muted/30 px-3 py-2.5 text-xs"> <div className="rounded-md border border-border bg-muted/30 px-3 py-2.5 text-xs">
<div className="font-medium text-foreground mb-1.5">{t('proposed_changes')}</div> <div className="font-medium text-foreground mb-1.5">{t('proposed_changes')}</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -904,32 +950,17 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
</div> </div>
)} )}
{/* Trust warning */}
{trustMessage && trustAssessment && (
<div className={cn(
'flex items-start gap-1.5 text-xs rounded-md px-3 py-2',
trustAssessment.level === 'warning'
? 'bg-destructive/10 text-destructive border border-destructive/20'
: 'bg-warning/10 text-warning border border-warning/20'
)}>
<AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
<span>{trustMessage}</span>
</div>
)}
{/* Action error */} {/* Action error */}
{actionError && ( {showDetails && actionError && (
<div className="flex items-start gap-1.5 text-xs text-destructive rounded-md px-3 py-2 bg-destructive/10 border border-destructive/20"> <div className="flex items-start gap-2 text-sm rounded-md px-3 py-2 bg-destructive/10 text-destructive border border-destructive/30">
<AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" /> <AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{actionError}</span> <span className="flex-1">{actionError}</span>
</div>
)}
</div> </div>
)} )}
{/* Actions */} {/* Actions */}
{showDetails && ( {showActionsRow && (
<div className="px-4 py-2.5 border-t border-border bg-muted/20 flex items-center gap-2 flex-wrap"> <div className="flex flex-wrap items-center gap-1.5 pt-0.5">
{canRespond && ( {canRespond && (
<> <>
<button <button
@@ -939,8 +970,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'accepted' currentRsvp === 'accepted'
? "bg-success/15 text-success border-success/20" ? "bg-success/15 text-success border-success/30"
: "text-muted-foreground hover:text-success border-border hover:border-success/30 hover:bg-success/10" : "text-muted-foreground hover:text-success border-border hover:border-success/30 hover:bg-success/10",
)} )}
> >
<Check className="w-3.5 h-3.5" /> <Check className="w-3.5 h-3.5" />
@@ -953,8 +984,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'tentative' currentRsvp === 'tentative'
? "bg-warning/15 text-warning border-warning/20" ? "bg-warning/15 text-warning border-warning/30"
: "text-muted-foreground hover:text-warning border-border hover:border-warning/30 hover:bg-warning/10" : "text-muted-foreground hover:text-warning border-border hover:border-warning/30 hover:bg-warning/10",
)} )}
> >
<HelpCircle className="w-3.5 h-3.5" /> <HelpCircle className="w-3.5 h-3.5" />
@@ -967,15 +998,14 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'declined' currentRsvp === 'declined'
? "bg-destructive/15 text-destructive border-destructive/20" ? "bg-destructive/15 text-destructive border-destructive/30"
: "text-muted-foreground hover:text-destructive border-border hover:border-destructive/30 hover:bg-destructive/10" : "text-muted-foreground hover:text-destructive border-border hover:border-destructive/30 hover:bg-destructive/10",
)} )}
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
{t('decline')} {t('decline')}
</button> </button>
<div className="w-px h-5 bg-border mx-1" />
<div className="w-px h-5 bg-border" />
</> </>
)} )}
@@ -1068,5 +1098,6 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
</div> </div>
)} )}
</div> </div>
</div>
); );
} }
+1 -1
View File
@@ -4837,7 +4837,7 @@ export function EmailViewer({
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') || {((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
hasCalendarInvitation) && ( hasCalendarInvitation) && (
<div className="border-b border-border bg-muted/30 isolate"> <div className="border-b border-border bg-muted/30 isolate">
<div className="max-w-6xl mx-auto px-6 py-1.5"> <div className="px-6 py-1.5">
<div className="flex flex-col gap-3 isolate"> <div className="flex flex-col gap-3 isolate">
{/* External Content Controls */} {/* External Content Controls */}
{hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow' && ( {hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow' && (
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Návrh na změnu zamítnut", "declined_counter_title": "Návrh na změnu zamítnut",
"cancelled_title": "Událost zrušena", "cancelled_title": "Událost zrušena",
"organizer": "Organizátor: {name}", "organizer": "Organizátor: {name}",
"organizer_label": "Organizátor:",
"attendees": "{count, plural, one {1 účastník} few {# účastníci} other {# účastníků}}", "attendees": "{count, plural, one {1 účastník} few {# účastníci} other {# účastníků}}",
"accept": "Přijmout", "accept": "Přijmout",
"maybe": "Možná", "maybe": "Možná",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Gegenvorschlag abgelehnt", "declined_counter_title": "Gegenvorschlag abgelehnt",
"cancelled_title": "Veranstaltung abgesagt", "cancelled_title": "Veranstaltung abgesagt",
"organizer": "Organisiert von {name}", "organizer": "Organisiert von {name}",
"organizer_label": "Organisiert von",
"attendees": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}", "attendees": "{count, plural, one {# Teilnehmer} other {# Teilnehmer}}",
"accept": "Annehmen", "accept": "Annehmen",
"maybe": "Vielleicht", "maybe": "Vielleicht",
+1
View File
@@ -415,6 +415,7 @@
"declined_counter_title": "Counter Proposal Declined", "declined_counter_title": "Counter Proposal Declined",
"cancelled_title": "Event Cancelled", "cancelled_title": "Event Cancelled",
"organizer": "Organized by {name}", "organizer": "Organized by {name}",
"organizer_label": "Organized by",
"attendees": "{count, plural, one {# attendee} other {# attendees}}", "attendees": "{count, plural, one {# attendee} other {# attendees}}",
"accept": "Accept", "accept": "Accept",
"maybe": "Maybe", "maybe": "Maybe",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Contrapropuesta rechazada", "declined_counter_title": "Contrapropuesta rechazada",
"cancelled_title": "Evento cancelado", "cancelled_title": "Evento cancelado",
"organizer": "Organizado por {name}", "organizer": "Organizado por {name}",
"organizer_label": "Organizado por",
"attendees": "{count, plural, one {# asistente} other {# asistentes}}", "attendees": "{count, plural, one {# asistente} other {# asistentes}}",
"accept": "Aceptar", "accept": "Aceptar",
"maybe": "Quizás", "maybe": "Quizás",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Contre-proposition refusée", "declined_counter_title": "Contre-proposition refusée",
"cancelled_title": "Événement annulé", "cancelled_title": "Événement annulé",
"organizer": "Organisé par {name}", "organizer": "Organisé par {name}",
"organizer_label": "Organisé par",
"attendees": "{count, plural, one {# participant} other {# participants}}", "attendees": "{count, plural, one {# participant} other {# participants}}",
"accept": "Accepter", "accept": "Accepter",
"maybe": "Peut-être", "maybe": "Peut-être",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Controproposta rifiutata", "declined_counter_title": "Controproposta rifiutata",
"cancelled_title": "Evento annullato", "cancelled_title": "Evento annullato",
"organizer": "Organizzato da {name}", "organizer": "Organizzato da {name}",
"organizer_label": "Organizzato da",
"attendees": "{count, plural, one {# partecipante} other {# partecipanti}}", "attendees": "{count, plural, one {# partecipante} other {# partecipanti}}",
"accept": "Accetta", "accept": "Accetta",
"maybe": "Forse", "maybe": "Forse",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "対案は拒否されました", "declined_counter_title": "対案は拒否されました",
"cancelled_title": "イベントがキャンセルされました", "cancelled_title": "イベントがキャンセルされました",
"organizer": "{name} が主催", "organizer": "{name} が主催",
"organizer_label": "主催者:",
"attendees": "{count}名の参加者", "attendees": "{count}名の参加者",
"accept": "承諾", "accept": "承諾",
"maybe": "未定", "maybe": "未定",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "시간 제안이 거절되었어요", "declined_counter_title": "시간 제안이 거절되었어요",
"cancelled_title": "일정이 취소되었어요", "cancelled_title": "일정이 취소되었어요",
"organizer": "주최자: {name}", "organizer": "주최자: {name}",
"organizer_label": "주최자:",
"attendees": "{count, plural, other {참석자 #명}}", "attendees": "{count, plural, other {참석자 #명}}",
"accept": "수락", "accept": "수락",
"maybe": "미정", "maybe": "미정",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Pretpiedāvājums noraidīts", "declined_counter_title": "Pretpiedāvājums noraidīts",
"cancelled_title": "Pasākums atcelts", "cancelled_title": "Pasākums atcelts",
"organizer": "Organizators: {name}", "organizer": "Organizators: {name}",
"organizer_label": "Organizators:",
"attendees": "{count, plural, one {# dalībnieks} other {# dalībnieki}}", "attendees": "{count, plural, one {# dalībnieks} other {# dalībnieki}}",
"accept": "Pieņemt", "accept": "Pieņemt",
"maybe": "Varbūt", "maybe": "Varbūt",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Tegenvoorstel afgewezen", "declined_counter_title": "Tegenvoorstel afgewezen",
"cancelled_title": "Evenement geannuleerd", "cancelled_title": "Evenement geannuleerd",
"organizer": "Georganiseerd door {name}", "organizer": "Georganiseerd door {name}",
"organizer_label": "Georganiseerd door",
"attendees": "{count, plural, one {# deelnemer} other {# deelnemers}}", "attendees": "{count, plural, one {# deelnemer} other {# deelnemers}}",
"accept": "Accepteren", "accept": "Accepteren",
"maybe": "Misschien", "maybe": "Misschien",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Propozycja zmian odrzucona", "declined_counter_title": "Propozycja zmian odrzucona",
"cancelled_title": "Wydarzenie anulowane", "cancelled_title": "Wydarzenie anulowane",
"organizer": "Organizator: {name}", "organizer": "Organizator: {name}",
"organizer_label": "Organizator:",
"attendees": "{count, plural, one {# uczestnik} other {# uczestników}}", "attendees": "{count, plural, one {# uczestnik} other {# uczestników}}",
"accept": "Akceptuj", "accept": "Akceptuj",
"maybe": "Może", "maybe": "Może",
+1
View File
@@ -388,6 +388,7 @@
"declined_counter_title": "Contraproposta recusada", "declined_counter_title": "Contraproposta recusada",
"cancelled_title": "Evento cancelado", "cancelled_title": "Evento cancelado",
"organizer": "Organizado por {name}", "organizer": "Organizado por {name}",
"organizer_label": "Organizado por",
"attendees": "{count, plural, one {# participante} other {# participantes}}", "attendees": "{count, plural, one {# participante} other {# participantes}}",
"accept": "Aceitar", "accept": "Aceitar",
"maybe": "Talvez", "maybe": "Talvez",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Встречное предложение отклонено", "declined_counter_title": "Встречное предложение отклонено",
"cancelled_title": "Событие отменено", "cancelled_title": "Событие отменено",
"organizer": "Организовано {name}", "organizer": "Организовано {name}",
"organizer_label": "Организовано",
"attendees": "{count, plural, one {# участник} other {# участников}}", "attendees": "{count, plural, one {# участник} other {# участников}}",
"accept": "Принять", "accept": "Принять",
"maybe": "Возможно", "maybe": "Возможно",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Karşı Teklif Reddedildi", "declined_counter_title": "Karşı Teklif Reddedildi",
"cancelled_title": "Etkinlik İptal Edildi", "cancelled_title": "Etkinlik İptal Edildi",
"organizer": "{name} tarafından düzenleniyor", "organizer": "{name} tarafından düzenleniyor",
"organizer_label": "Düzenleyen:",
"attendees": "{count, plural, one {# katılımcı} other {# katılımcı}}", "attendees": "{count, plural, one {# katılımcı} other {# katılımcı}}",
"accept": "Kabul et", "accept": "Kabul et",
"maybe": "Belki", "maybe": "Belki",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "Зустрічна пропозиція відхилена", "declined_counter_title": "Зустрічна пропозиція відхилена",
"cancelled_title": "Подію скасовано", "cancelled_title": "Подію скасовано",
"organizer": "Організовано {name}", "organizer": "Організовано {name}",
"organizer_label": "Організовано",
"attendees": "{count, plural, one {# учасник} few {# учасники} many {# учасників} other {# учасників}}", "attendees": "{count, plural, one {# учасник} few {# учасники} many {# учасників} other {# учасників}}",
"accept": "прийняти", "accept": "прийняти",
"maybe": "можливо", "maybe": "можливо",
+1
View File
@@ -390,6 +390,7 @@
"declined_counter_title": "改期建议已被拒绝", "declined_counter_title": "改期建议已被拒绝",
"cancelled_title": "活动已取消", "cancelled_title": "活动已取消",
"organizer": "组织者:{name}", "organizer": "组织者:{name}",
"organizer_label": "组织者:",
"attendees": "{count, plural, one {# 位参与者} other {# 位参与者}}", "attendees": "{count, plural, one {# 位参与者} other {# 位参与者}}",
"accept": "接受", "accept": "接受",
"maybe": "暂定", "maybe": "暂定",