feat: order Mountain View's message menu like Gmail's and add Find emails from

This commit is contained in:
Linus Rath
2026-10-12 01:45:35 +02:00
parent 986daef90b
commit 5efe718cf0
31 changed files with 399 additions and 207 deletions
@@ -0,0 +1,67 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Email, Mailbox } from '@/lib/jmap/types';
import { useSettingsStore } from '@/stores/settings-store';
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
vi.mock('../rules-menu', () => ({ RulesContextSubMenu: () => null }));
vi.mock('@/hooks/use-copy-link', () => ({ useCopyLink: () => vi.fn() }));
const { EmailContextMenu } = await import('../email-context-menu');
const rights = { mayAddItems: true } as Mailbox['myRights'];
const mailboxes = [
{ id: 'inbox', name: 'Inbox', role: 'inbox', myRights: rights },
{ id: 'archive', name: 'Archive', role: 'archive', myRights: rights },
] as unknown as Mailbox[];
const email = {
id: 'e1',
keywords: { $seen: true },
mailboxIds: { inbox: true },
from: [{ name: 'Sarah Kim', email: 'sarah@example.com' }],
} as unknown as Email;
function renderMenu(props: Partial<React.ComponentProps<typeof EmailContextMenu>> = {}) {
return render(
<EmailContextMenu
email={email}
position={{ x: 0, y: 0 }}
isOpen
onClose={() => {}}
menuRef={{ current: null }}
mailboxes={mailboxes}
selectedMailbox="inbox"
onArchive={() => {}}
onDelete={() => {}}
{...props}
/>,
);
}
/** The menu's entries by test id, top to bottom. */
const order = () => [...document.querySelectorAll('[data-testid^="ctx-"]')].map((e) => e.getAttribute('data-testid'));
describe('message menu under Mountain View', () => {
beforeEach(() => useSettingsStore.setState({ interfaceLayout: 'mountain-view' } as never));
it("puts Mark as read with Archive and Delete, before Move to, as Gmail does", () => {
renderMenu();
const ids = order();
expect(ids.indexOf('ctx-delete')).toBeLessThan(ids.indexOf('ctx-mark-unread'));
expect(ids.indexOf('ctx-mark-unread')).toBeLessThan(ids.indexOf('ctx-move-to'));
});
it('finds the mail from the sender', () => {
const onFindFromSender = vi.fn();
renderMenu({ onFindFromSender });
fireEvent.click(screen.getByTestId('ctx-find-from'));
expect(onFindFromSender).toHaveBeenCalledTimes(1);
});
it('keeps the default order outside Mountain View', () => {
useSettingsStore.setState({ interfaceLayout: 'default' } as never);
renderMenu({ onFindFromSender: vi.fn() });
const ids = order();
expect(ids.indexOf('ctx-move-to')).toBeLessThan(ids.indexOf('ctx-mark-unread'));
});
});
+271 -180
View File
@@ -2,6 +2,7 @@
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import { useSettingsStore } from "@/stores/settings-store";
import { Email, Mailbox } from "@/lib/jmap/types";
import {
ContextMenu,
@@ -36,6 +37,7 @@ import {
Link as LinkIcon,
MessagesSquare,
Copy,
Search,
} from "@/components/icons";
import { buildMailPath } from "@/lib/deep-links";
import { useCopyLink } from "@/hooks/use-copy-link";
@@ -82,6 +84,8 @@ interface EmailContextMenuProps {
onReplyAll?: () => void;
onForward?: () => void;
onForwardAsAttachment?: () => void;
/** Mountain View: search for the mail this sender has sent. */
onFindFromSender?: () => void;
onMarkAsRead?: (read: boolean) => void;
onToggleStar?: () => void;
onTogglePinned?: () => void;
@@ -148,6 +152,7 @@ export function EmailContextMenu({
onReplyAll,
onForward,
onForwardAsAttachment,
onFindFromSender,
onMarkAsRead,
onToggleStar,
onTogglePinned,
@@ -173,6 +178,7 @@ export function EmailContextMenu({
onCopyToAccount,
}: EmailContextMenuProps) {
const t = useTranslations("context_menu");
const mountainView = useSettingsStore((state) => state.interfaceLayout === "mountain-view");
const tSidebar = useTranslations("sidebar");
const tEmailViewer = useTranslations("email_viewer");
const tDeepLink = useTranslations("deep_link");
@@ -260,6 +266,238 @@ export function EmailContextMenu({
// Stable, so the Rules entry does not re-resolve its account every render.
const ruleSenderEmails = useMemo(() => ruleEmails ?? [email], [ruleEmails, email]);
// The menu's entries, composed below in the default layout's order or,
// under Mountain View, in Gmail's.
const replyBlock = (
<>
{/* Single email actions - Reply, Reply All, Forward */}
{!isScheduled && !showBatchActions && (
<>
<ContextMenuItem
icon={Reply}
label={t("reply")}
onClick={() => handleAction(onReply!)}
disabled={!onReply}
/>
<ContextMenuItem
icon={ReplyAll}
label={t("reply_all")}
onClick={() => handleAction(onReplyAll!)}
disabled={!onReplyAll}
/>
<ContextMenuItem
icon={Forward}
label={t("forward")}
onClick={() => handleAction(onForward!)}
disabled={!onForward}
/>
<ContextMenuItem
icon={Paperclip}
label={tEmailViewer("forward_as_attachment")}
onClick={() => handleAction(onForwardAsAttachment!)}
disabled={!onForwardAsAttachment || !email.blobId}
/>
<ContextMenuSeparator />
</>
)}
</>
);
const permalinkBlock = (
<>
{/* Permalinks (#733). The conversation entry only appears when the
message actually belongs to a thread worth linking to. */}
{!showBatchActions && (
<>
<ContextMenuItem
icon={LinkIcon}
label={tDeepLink("copy_message")}
onClick={() => handleAction(() => {
void copyLink(buildMailPath({ mailboxId: null, emailId: email.id, threadId: null }));
})}
/>
{email.threadId && (
<ContextMenuItem
icon={MessagesSquare}
label={tDeepLink("copy_conversation")}
onClick={() => handleAction(() => {
void copyLink(buildMailPath({ mailboxId: null, emailId: null, threadId: email.threadId! }));
})}
/>
)}
<ContextMenuSeparator />
</>
)}
</>
);
const archiveItem = (
<>
{/* Archive */}
<ContextMenuItem
icon={Archive}
label={t("archive")}
onClick={() =>
handleAction(showBatchActions ? onBatchArchive! : onArchive!)
}
disabled={showBatchActions ? !onBatchArchive : !onArchive}
/>
</>
);
const deleteItem = (
<>
{/* Delete */}
<ContextMenuItem
icon={Trash2}
label={t("delete")}
testId="ctx-delete"
onClick={() =>
handleAction(showBatchActions ? onBatchDelete! : onDelete!)
}
disabled={showBatchActions ? !onBatchDelete : !onDelete}
destructive={!mountainView}
/>
</>
);
const readItem = (
<>
{/* Mark as read/unread */}
<ContextMenuItem
icon={isUnread ? MailOpen : Mail}
label={isUnread ? t("mark_read") : t("mark_unread")}
testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"}
onClick={() =>
handleAction(() =>
showBatchActions
? onBatchMarkAsRead?.(isUnread)
: onMarkAsRead?.(isUnread)
)
}
/>
</>
);
const moveItem = (
<>
{/* Move to submenu */}
{moveTree.length > 0 && (
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")} testId="ctx-move-to">
{renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) =>
showBatchActions
? onBatchMoveToMailbox?.(mailboxId)
: onMoveToMailbox?.(mailboxId)
)}
</ContextMenuSubMenu>
)}
</>
);
const copyItem = (
<>
{/* Copy to another connected account, keeping the originals */}
{onCopyToAccount && copyTrees.length > 0 && (
<ContextMenuSubMenu icon={Copy} label={t("copy_to_account")} testId="ctx-copy-to">
{copyTrees.map((target) => (
<div key={target.accountId}>
<ContextMenuHeader>{target.label}</ContextMenuHeader>
{renderMailboxNodes(target.tree, target.targetIds, `copy-to:${target.accountId}:`, (mailboxId) =>
onCopyToAccount(target.accountId, mailboxId)
)}
</div>
))}
</ContextMenuSubMenu>
)}
</>
);
const rulesItem = (
<>
{/* Rules: filter rules made from the sender(s), saved as Sieve */}
<RulesContextSubMenu anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} />
</>
);
const starItem = (
<>
{/* Star/Unstar - only for single email */}
{!showBatchActions && (
<ContextMenuItem
icon={Star}
label={isStarred ? t("unstar") : t("star")}
onClick={() => handleAction(onToggleStar!)}
disabled={!onToggleStar}
testId={isStarred ? "ctx-unstar" : "ctx-star"}
/>
)}
</>
);
const pinItem = (
<>
{/* Pin/Unpin - only for single email; pinned mails float to the top of the list */}
{!showBatchActions && onTogglePinned && (
<ContextMenuItem
icon={isPinned ? PinOff : Pin}
label={isPinned ? t("unpin") : t("pin")}
onClick={() => handleAction(onTogglePinned)}
/>
)}
</>
);
const tagItem = (
<>
{/* Set tag submenu - the row's own tags, or the whole selection's (#1077) */}
{(!showBatchActions || onBatchToggleTag) && (
<ContextMenuSubMenu icon={Tag} label={t("tag")} testId="ctx-tag">
<div className="w-56 max-w-[18rem]">
{showBatchActions ? (
<TagPicker
selectedIds={batchTagIds ?? []}
partialIds={batchPartialTagIds}
onToggle={(tagId) => onBatchToggleTag?.(tagId)}
/>
) : (
<TagPicker
selectedIds={currentTagIds}
onToggle={(tagId) => onSetTag?.(tagId)}
/>
)}
</div>
</ContextMenuSubMenu>
)}
</>
);
const spamBlock = (
<>
{/* Spam - contextual based on folder; pointless on own outgoing mail */}
{spamApplicable && (
<>
<ContextMenuSeparator />
<ContextMenuItem
icon={isInJunkFolder ? ShieldCheck : ShieldAlert}
label={isInJunkFolder ? t("not_spam") : t("mark_as_spam")}
testId={isInJunkFolder ? "ctx-not-spam" : "ctx-spam"}
onClick={() =>
handleAction(
showBatchActions
? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)
: (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!)
)
}
disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)}
destructive={!isInJunkFolder && !mountainView}
/>
</>
)}
</>
);
const sender = email.from?.[0];
const findBlock = !showBatchActions && onFindFromSender && sender?.email ? (
<>
<ContextMenuSeparator />
<ContextMenuItem
icon={Search}
label={t("find_emails_from", { name: sender.name || sender.email })}
testId="ctx-find-from"
onClick={() => handleAction(onFindFromSender)}
/>
</>
) : null;
return (
<ContextMenu
ref={menuRef}
@@ -308,190 +546,43 @@ export function EmailContextMenu({
</>
)}
{/* Single email actions - Reply, Reply All, Forward */}
{!isScheduled && !showBatchActions && (
{mountainView ? (
<>
<ContextMenuItem
icon={Reply}
label={t("reply")}
onClick={() => handleAction(onReply!)}
disabled={!onReply}
/>
<ContextMenuItem
icon={ReplyAll}
label={t("reply_all")}
onClick={() => handleAction(onReplyAll!)}
disabled={!onReplyAll}
/>
<ContextMenuItem
icon={Forward}
label={t("forward")}
onClick={() => handleAction(onForward!)}
disabled={!onForward}
/>
<ContextMenuItem
icon={Paperclip}
label={tEmailViewer("forward_as_attachment")}
onClick={() => handleAction(onForwardAsAttachment!)}
disabled={!onForwardAsAttachment || !email.blobId}
/>
{replyBlock}
{archiveItem}
{deleteItem}
{readItem}
<ContextMenuSeparator />
{moveItem}
{tagItem}
{rulesItem}
{copyItem}
<ContextMenuSeparator />
{starItem}
{pinItem}
{spamBlock}
{findBlock}
<ContextMenuSeparator />
{permalinkBlock}
</>
) : (
<>
{replyBlock}
{permalinkBlock}
{archiveItem}
{deleteItem}
<ContextMenuSeparator />
{moveItem}
{copyItem}
{rulesItem}
{starItem}
{pinItem}
{tagItem}
{spamBlock}
<ContextMenuSeparator />
{readItem}
</>
)}
{/* Permalinks (#733). The conversation entry only appears when the
message actually belongs to a thread worth linking to. */}
{!showBatchActions && (
<>
<ContextMenuItem
icon={LinkIcon}
label={tDeepLink("copy_message")}
onClick={() => handleAction(() => {
void copyLink(buildMailPath({ mailboxId: null, emailId: email.id, threadId: null }));
})}
/>
{email.threadId && (
<ContextMenuItem
icon={MessagesSquare}
label={tDeepLink("copy_conversation")}
onClick={() => handleAction(() => {
void copyLink(buildMailPath({ mailboxId: null, emailId: null, threadId: email.threadId! }));
})}
/>
)}
<ContextMenuSeparator />
</>
)}
{/* Archive */}
<ContextMenuItem
icon={Archive}
label={t("archive")}
onClick={() =>
handleAction(showBatchActions ? onBatchArchive! : onArchive!)
}
disabled={showBatchActions ? !onBatchArchive : !onArchive}
/>
{/* Delete */}
<ContextMenuItem
icon={Trash2}
label={t("delete")}
testId="ctx-delete"
onClick={() =>
handleAction(showBatchActions ? onBatchDelete! : onDelete!)
}
disabled={showBatchActions ? !onBatchDelete : !onDelete}
destructive
/>
<ContextMenuSeparator />
{/* Move to submenu */}
{moveTree.length > 0 && (
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")} testId="ctx-move-to">
{renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) =>
showBatchActions
? onBatchMoveToMailbox?.(mailboxId)
: onMoveToMailbox?.(mailboxId)
)}
</ContextMenuSubMenu>
)}
{/* Copy to another connected account, keeping the originals */}
{onCopyToAccount && copyTrees.length > 0 && (
<ContextMenuSubMenu icon={Copy} label={t("copy_to_account")} testId="ctx-copy-to">
{copyTrees.map((target) => (
<div key={target.accountId}>
<ContextMenuHeader>{target.label}</ContextMenuHeader>
{renderMailboxNodes(target.tree, target.targetIds, `copy-to:${target.accountId}:`, (mailboxId) =>
onCopyToAccount(target.accountId, mailboxId)
)}
</div>
))}
</ContextMenuSubMenu>
)}
{/* Rules: filter rules made from the sender(s), saved as Sieve */}
<RulesContextSubMenu anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} />
{/* Star/Unstar - only for single email */}
{!showBatchActions && (
<ContextMenuItem
icon={Star}
label={isStarred ? t("unstar") : t("star")}
onClick={() => handleAction(onToggleStar!)}
disabled={!onToggleStar}
testId={isStarred ? "ctx-unstar" : "ctx-star"}
/>
)}
{/* Pin/Unpin - only for single email; pinned mails float to the top of the list */}
{!showBatchActions && onTogglePinned && (
<ContextMenuItem
icon={isPinned ? PinOff : Pin}
label={isPinned ? t("unpin") : t("pin")}
onClick={() => handleAction(onTogglePinned)}
/>
)}
{/* Set tag submenu - the row's own tags, or the whole selection's (#1077) */}
{(!showBatchActions || onBatchToggleTag) && (
<ContextMenuSubMenu icon={Tag} label={t("tag")} testId="ctx-tag">
<div className="w-56 max-w-[18rem]">
{showBatchActions ? (
<TagPicker
selectedIds={batchTagIds ?? []}
partialIds={batchPartialTagIds}
onToggle={(tagId) => onBatchToggleTag?.(tagId)}
/>
) : (
<TagPicker
selectedIds={currentTagIds}
onToggle={(tagId) => onSetTag?.(tagId)}
/>
)}
</div>
</ContextMenuSubMenu>
)}
{/* Spam - contextual based on folder; pointless on own outgoing mail */}
{spamApplicable && (
<>
<ContextMenuSeparator />
<ContextMenuItem
icon={isInJunkFolder ? ShieldCheck : ShieldAlert}
label={isInJunkFolder ? t("not_spam") : t("mark_as_spam")}
testId={isInJunkFolder ? "ctx-not-spam" : "ctx-spam"}
onClick={() =>
handleAction(
showBatchActions
? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)
: (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!)
)
}
disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)}
destructive={!isInJunkFolder}
/>
</>
)}
<ContextMenuSeparator />
{/* Mark as read/unread */}
<ContextMenuItem
icon={isUnread ? MailOpen : Mail}
label={isUnread ? t("mark_read") : t("mark_unread")}
testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"}
onClick={() =>
handleAction(() =>
showBatchActions
? onBatchMarkAsRead?.(isUnread)
: onMarkAsRead?.(isUnread)
)
}
/>
</>
)}
+6
View File
@@ -38,6 +38,8 @@ interface EmailListProps {
selectedEmailId?: string;
onEmailSelect?: (email: Email) => void;
onEmailDoubleClick?: (email: Email) => void;
/** Mountain View: search for the mail a sender has sent, from the row menu. */
onFindFromSender?: (sender: { name: string; email: string }) => void;
className?: string;
isLoading?: boolean;
hasMore?: boolean;
@@ -75,6 +77,7 @@ export function EmailList({
selectedEmailId,
onEmailSelect,
onEmailDoubleClick,
onFindFromSender,
className,
isLoading = false,
hasMore,
@@ -798,6 +801,9 @@ export function EmailList({
onReplyAll={() => onReplyAll?.(contextMenuEmail!)}
onForward={() => onForward?.(contextMenuEmail!)}
onForwardAsAttachment={() => onForwardAsAttachment?.(contextMenuEmail!)}
onFindFromSender={onFindFromSender && contextMenuEmail.from?.[0]?.email
? () => onFindFromSender({ name: contextMenuEmail!.from![0].name || '', email: contextMenuEmail!.from![0].email })
: undefined}
onMarkAsRead={(read) => onMarkAsRead?.(contextMenuEmail!, read)}
onToggleStar={() => onToggleStar?.(contextMenuEmail!)}
onTogglePinned={onTogglePinned ? () => onTogglePinned(contextMenuEmail!) : undefined}
+1
View File
@@ -4517,6 +4517,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
<ErrorBoundary fallback={EmailListErrorFallback}>
<EmailList
onFindFromSender={mvShell ? (sender) => void handleSelectContactSuggestion(sender, 'from') : undefined}
hideBatchToolbar={mvShell}
emails={activeEmails}
selectedEmailId={selectedEmail?.id}
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "لا توجد قواعد بعد",
"new_mail_only": "للرسائل الجديدة فقط",
"running": "جارٍ تشغيل «{name}» على {folder}…"
}
},
"find_emails_from": "البحث عن رسائل من {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "تمييز المجلد كمقروء",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Encara no hi ha regles",
"new_mail_only": "Només per a missatges nous",
"running": "S'està executant «{name}» a {folder}…"
}
},
"find_emails_from": "Cerca correus de {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Marca la carpeta com a llegida",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Zatím žádná pravidla",
"new_mail_only": "Pouze pro nové zprávy",
"running": "Spouštění „{name}“ ve složce {folder}…"
}
},
"find_emails_from": "Najít e-maily od {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Označit složku jako přečtenou",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Ingen regler endnu",
"new_mail_only": "Kun for nye beskeder",
"running": "Kører „{name}“ på {folder}…"
}
},
"find_emails_from": "Find e-mails fra {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Markér mappe som læst",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Noch keine Regeln",
"new_mail_only": "Nur für neue Nachrichten",
"running": "„{name}“ wird auf {folder} angewendet…"
}
},
"find_emails_from": "E-Mails von {name} finden"
},
"mailbox_context_menu": {
"mark_folder_read": "Ordner als gelesen markieren",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "No rules yet",
"new_mail_only": "Only for new messages",
"running": "Running \"{name}\" on {folder}…"
}
},
"find_emails_from": "Find emails from {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Mark folder as read",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Aún no hay reglas",
"new_mail_only": "Solo para mensajes nuevos",
"running": "Ejecutando «{name}» en {folder}…"
}
},
"find_emails_from": "Buscar correos de {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Marcar carpeta como leída",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "هنوز قانونی وجود ندارد",
"new_mail_only": "فقط برای پیام‌های جدید",
"running": "در حال اجرای «{name}» روی {folder}…"
}
},
"find_emails_from": "یافتن ایمیل‌های {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "علامت‌گذاری پوشه خوانده شده",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Aucune règle pour l'instant",
"new_mail_only": "Uniquement pour les nouveaux messages",
"running": "Exécution de « {name} » sur {folder}…"
}
},
"find_emails_from": "Rechercher les e-mails de {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Marquer le dossier comme lu",
+2 -1
View File
@@ -2483,7 +2483,8 @@
"none": "אין עדיין כללים",
"new_mail_only": "רק להודעות חדשות",
"running": "מריץ את „{name}“ על {folder}…"
}
},
"find_emails_from": "חיפוש הודעות מ{name}"
},
"shortcuts": {
"title": "קיצורי מקלדת",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Még nincsenek szabályok",
"new_mail_only": "Csak új üzenetekre",
"running": "„{name}” futtatása itt: {folder}…"
}
},
"find_emails_from": "{name} e-mailjeinek keresése"
},
"mailbox_context_menu": {
"mark_folder_read": "Mappa olvasottnak jelölése",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Nessuna regola",
"new_mail_only": "Solo per i nuovi messaggi",
"running": "Esecuzione di «{name}» su {folder}…"
}
},
"find_emails_from": "Trova email da {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Segna cartella come letta",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "ルールはまだありません",
"new_mail_only": "新着メッセージのみ",
"running": "{folder} で「{name}」を実行中…"
}
},
"find_emails_from": "{name} からのメールを検索"
},
"mailbox_context_menu": {
"mark_folder_read": "フォルダーを既読にする",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "아직 규칙이 없습니다",
"new_mail_only": "새 메시지에만 적용",
"running": "{folder}에서 \"{name}\" 실행 중…"
}
},
"find_emails_from": "{name}님이 보낸 이메일 찾기"
},
"mailbox_context_menu": {
"mark_folder_read": "폴더를 읽음으로 표시",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Vēl nav kārtulu",
"new_mail_only": "Tikai jaunām ziņām",
"running": "Izpilda “{name}” mapē {folder}…"
}
},
"find_emails_from": "Atrast e-pastus no {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Atzīmēt mapi kā lasītu",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Одоогоор дүрэм алга",
"new_mail_only": "Зөвхөн шинэ мессежүүдэд",
"running": "{folder} дээр \"{name}\"-г ажиллуулж байна…"
}
},
"find_emails_from": "{name}-с ирсэн имэйлүүдийг хайх"
},
"mailbox_context_menu": {
"mark_folder_read": "Хавтсыг уншсан гэж тэмдэглэх",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Ingen regler ennå",
"new_mail_only": "Bare for nye meldinger",
"running": "Kjører «{name}» på {folder}…"
}
},
"find_emails_from": "Finn e-poster fra {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Merk mappen som lest",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Nog geen regels",
"new_mail_only": "Alleen voor nieuwe berichten",
"running": "\"{name}\" wordt uitgevoerd op {folder}…"
}
},
"find_emails_from": "E-mails van {name} zoeken"
},
"mailbox_context_menu": {
"mark_folder_read": "Map markeren als gelezen",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Brak reguł",
"new_mail_only": "Tylko dla nowych wiadomości",
"running": "Uruchamianie „{name}” w folderze {folder}…"
}
},
"find_emails_from": "Znajdź e-maile od {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Oznacz folder jako przeczytany",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Ainda não há regras",
"new_mail_only": "Apenas para mensagens novas",
"running": "A executar \"{name}\" em {folder}…"
}
},
"find_emails_from": "Encontrar e-mails de {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Marcar pasta como lida",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Nicio regulă încă",
"new_mail_only": "Doar pentru mesajele noi",
"running": "Se rulează „{name}” în {folder}…"
}
},
"find_emails_from": "Găsește e-mailurile de la {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Marcați folderul ca citit",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Правил пока нет",
"new_mail_only": "Только для новых писем",
"running": "Выполняется «{name}» в папке {folder}…"
}
},
"find_emails_from": "Найти письма от {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Отметить папку как прочитанную",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Zatiaľ žiadne pravidlá",
"new_mail_only": "Len pre nové správy",
"running": "Spúšťa sa „{name}“ v priečinku {folder}…"
}
},
"find_emails_from": "Nájsť e-maily od {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Označiť priečinok ako prečítaný",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Henüz kural yok",
"new_mail_only": "Yalnızca yeni iletiler için",
"running": "\"{name}\" {folder} klasöründe çalıştırılıyor…"
}
},
"find_emails_from": "{name} adlı kişiden gelen e-postaları bul"
},
"mailbox_context_menu": {
"mark_folder_read": "Klasörü okundu olarak işaretle",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "Правил поки немає",
"new_mail_only": "Лише для нових листів",
"running": "Виконується «{name}» у теці {folder}…"
}
},
"find_emails_from": "Знайти листи від {name}"
},
"mailbox_context_menu": {
"mark_folder_read": "Позначити папку як прочитану",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "尚無規則",
"new_mail_only": "僅限新郵件",
"running": "正在 {folder} 中執行「{name}」…"
}
},
"find_emails_from": "尋找來自 {name} 的郵件"
},
"mailbox_context_menu": {
"mark_folder_read": "將信件匣標示為已讀",
+2 -1
View File
@@ -2517,7 +2517,8 @@
"none": "暂无规则",
"new_mail_only": "仅限新邮件",
"running": "正在 {folder} 中运行“{name}”…"
}
},
"find_emails_from": "查找来自 {name} 的邮件"
},
"mailbox_context_menu": {
"mark_folder_read": "将文件夹标记为已读",