feat: add batch forward-as-attachment for selected emails

Multi-select emails in the list, right-click, and pick "Forward as
attachments" to open one composer carrying every selected message as
its own message/rfc822 (.eml) attachment - the batch sibling of the
existing single-message action.

- lib/forward-as-attachment.ts: buildBatchForwardAsAttachmentPayload
  builds one payload from many emails. The subject is count-based
  (there is no single original subject), with the count localized via
  an injected label so the subject never claims more attachments than
  the composer carries. Emails without a blobId are skipped; null when
  none has one, matching the single flow's silent no-op.
- mail-app.tsx: handleBatchForwardAsAttachment resolves the selection
  through useEmailStore.getState() (fresh state, list-ordered
  attachments, stale ids drop out), runs the plugin transform and veto
  hooks per email - one veto cancels the whole compose - and opens the
  composer with the attachments seeded via replyTo. Clears the
  selection only on success.
- email-context-menu.tsx / email-list.tsx: new batch menu item shown
  when the right-clicked email belongs to a multi-selection; hidden in
  the scheduled view like the single item.
- locales: context_menu.forward_as_attachments and
  email_composer.forward_batch_count in all 27 locales.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
daluchen
2026-10-15 14:50:31 +08:00
co-authored by Claude Code
parent e2e73913b9
commit 5dde5e00e1
33 changed files with 378 additions and 2 deletions
@@ -0,0 +1,101 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import type { Email, Mailbox } from '@/lib/jmap/types';
// The menu's only far-reaching child - the plugin host iframe slot. It is
// never what these tests are about, so stub it out.
vi.mock('@/components/plugins/plugin-slot', () => ({
PluginSlot: () => null,
}));
// vitest.setup.ts mocks next-intl's useTranslations to return the key itself,
// so labels render as raw keys ("forward_as_attachments" etc.).
import { EmailContextMenu } from '../email-context-menu';
const email: Email = {
id: 'e1',
blobId: 'b1',
threadId: 't1',
mailboxIds: { inbox: true },
keywords: { $seen: true },
from: [{ name: 'Alice', email: 'alice@example.com' }],
subject: 'Hello',
receivedAt: '2026-01-01T00:00:00Z',
hasAttachment: false,
size: 100,
} as unknown as Email;
const mailboxes: Mailbox[] = [
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
] as unknown as Mailbox[];
type MenuProps = React.ComponentProps<typeof EmailContextMenu>;
function renderMenu(props: Partial<MenuProps> = {}) {
const onClose = vi.fn();
render(
<EmailContextMenu
email={email}
position={{ x: 10, y: 10 }}
isOpen
onClose={onClose}
menuRef={{ current: null }}
mailboxes={mailboxes}
selectedMailbox="inbox"
currentMailboxRole="inbox"
{...props}
/>,
);
return { onClose };
}
describe('EmailContextMenu batch forward as attachment', () => {
it('shows the batch header and batch forward item when the right-clicked email is in a multi-selection', () => {
renderMenu({ isMultiSelect: true, selectedCount: 3, onBatchForwardAsAttachment: vi.fn() });
expect(screen.getByTestId('ctx-batch-forward-as-attachment')).toBeInTheDocument();
expect(screen.getByText('items_selected')).toBeInTheDocument();
});
it('clicking the batch item invokes onBatchForwardAsAttachment and closes the menu', () => {
const onBatchForwardAsAttachment = vi.fn();
const { onClose } = renderMenu({ isMultiSelect: true, selectedCount: 3, onBatchForwardAsAttachment });
fireEvent.click(screen.getByTestId('ctx-batch-forward-as-attachment'));
expect(onBatchForwardAsAttachment).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1);
});
it('hides the single-message forward-as-attachment item in batch mode', () => {
renderMenu({ isMultiSelect: true, selectedCount: 3, onBatchForwardAsAttachment: vi.fn() });
// vitest's next-intl mock renders labels as raw keys; the singular key
// must be absent while the plural batch key is present.
expect(screen.queryByText('forward_as_attachment')).toBeNull();
expect(screen.getByText('forward_as_attachments')).toBeInTheDocument();
});
it('hides the batch item when only one email is selected', () => {
renderMenu({ isMultiSelect: true, selectedCount: 1, onBatchForwardAsAttachment: vi.fn() });
expect(screen.queryByTestId('ctx-batch-forward-as-attachment')).toBeNull();
});
it('hides the batch item when the right-clicked email is outside the selection', () => {
renderMenu({ isMultiSelect: false, selectedCount: 3, onBatchForwardAsAttachment: vi.fn() });
expect(screen.queryByTestId('ctx-batch-forward-as-attachment')).toBeNull();
});
it('hides the batch item for scheduled emails', () => {
const scheduled = { ...email, isScheduled: true } as unknown as Email;
renderMenu({
email: scheduled,
isMultiSelect: true,
selectedCount: 3,
onBatchForwardAsAttachment: vi.fn(),
});
expect(screen.queryByTestId('ctx-batch-forward-as-attachment')).toBeNull();
});
it('disables the batch item when no handler is passed', () => {
renderMenu({ isMultiSelect: true, selectedCount: 3 });
expect((screen.getByTestId('ctx-batch-forward-as-attachment') as HTMLButtonElement).disabled).toBe(true);
});
});
+18
View File
@@ -117,6 +117,7 @@ interface EmailContextMenuProps {
*/
copyTargets?: CopyTargetAccount[];
onCopyToAccount?: (accountId: string, mailboxId: string) => void;
onBatchForwardAsAttachment?: () => void;
}
// Get mailbox icon based on role
@@ -172,6 +173,7 @@ export function EmailContextMenu({
onBatchMoveToMailbox,
onBatchMarkAsSpam,
onBatchUndoSpam,
onBatchForwardAsAttachment,
onEditDraft,
onCancelScheduledForEdit,
onRescheduleScheduled,
@@ -552,6 +554,22 @@ export function EmailContextMenu({
{!isScheduled && (
<>
{/* Batch forward - multi-select sibling of the single-message item below.
First action in batch mode so compose-style actions stay above the
archive/delete group, mirroring the single menu's ordering. */}
{showBatchActions && (
<>
<ContextMenuItem
icon={Paperclip}
label={t("forward_as_attachments")}
testId="ctx-batch-forward-as-attachment"
onClick={() => handleAction(onBatchForwardAsAttachment!)}
disabled={!onBatchForwardAsAttachment}
/>
<ContextMenuSeparator />
</>
)}
{/* Edit Draft - only for single draft emails */}
{!isScheduled && !showBatchActions && isDraft && onEditDraft && (
<>
+3
View File
@@ -56,6 +56,7 @@ interface EmailListProps {
onReplyAll?: (email: Email) => void;
onForward?: (email: Email) => void;
onForwardAsAttachment?: (email: Email) => void;
onBatchForwardAsAttachment?: () => void;
onMarkAsRead?: (email: Email, read: boolean) => void;
onToggleStar?: (email: Email) => void;
onTogglePinned?: (email: Email) => void;
@@ -98,6 +99,7 @@ export function EmailList({
onReplyAll,
onForward,
onForwardAsAttachment,
onBatchForwardAsAttachment,
onMarkAsRead,
onToggleStar,
onTogglePinned,
@@ -960,6 +962,7 @@ export function EmailList({
onFindFromSender={onFindFromSender && contextMenuEmail.from?.[0]?.email
? () => onFindFromSender({ name: contextMenuEmail!.from![0].name || '', email: contextMenuEmail!.from![0].email })
: undefined}
onBatchForwardAsAttachment={onBatchForwardAsAttachment}
onMarkAsRead={(read) => onMarkAsRead?.(contextMenuEmail!, read)}
onToggleStar={() => onToggleStar?.(contextMenuEmail!)}
onTogglePinned={onTogglePinned ? () => onTogglePinned(contextMenuEmail!) : undefined}
+83 -1
View File
@@ -133,7 +133,7 @@ import { composeOptionsFor, runBeforeCompose, type ComposePrefill, type Composer
import { formatMailto } from "@/lib/protocol-handlers/mailto";
import { buildQuoteHeader } from "@/lib/quote-header";
import { buildComposeTabTitle, buildReplySubject } from "@/lib/subject-prefix";
import { buildForwardAsAttachmentPayload } from "@/lib/forward-as-attachment";
import { buildForwardAsAttachmentPayload, buildBatchForwardAsAttachmentPayload } from "@/lib/forward-as-attachment";
import { getEffectiveLocale } from '@/i18n/detect-locale';
import {
SCHEDULED_MAILBOX_ID,
@@ -2572,6 +2572,85 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
if (isMobile) setActiveView('viewer');
};
// Batch sibling of handleForwardAsAttachment above: opens ONE composer with
// every selected message attached as message/rfc822. Resolution goes through
// useEmailStore.getState() - always fresh, immune to the closure-staleness
// noted above - and filters the current list so attachment order follows the
// list, not checkbox-click order. Stale selection ids (folder switched under
// the selection) simply drop out of the filter rather than forwarding
// something the user can no longer see.
const handleBatchForwardAsAttachment = async () => {
const state = useEmailStore.getState();
const ids = state.selectedEmailIds;
if (ids.size < 2) return; // menu only offers this for >1; guards future wiring
// Unreachable from the menu in the scheduled view (the whole actions block
// is isScheduled-gated); resolve the right pool anyway so a future shortcut
// can't silently read the wrong list.
const pool = state.isScheduledView ? state.scheduledEmails : state.emails;
const selected = pool.filter((e) => ids.has(e.id));
if (selected.length === 0) return;
const {
emailDownloadTemplate,
filenameSpaceReplacement,
filenameLowercase,
filenameStripDiacritics,
filenameCollapseSeparators,
} = useSettingsStore.getState();
const transformed: Email[] = [];
for (const e of selected) {
transformed.push(await emailHooks.onBeforeComposeOpenToForwardAsAttachment.transform(e));
}
const payload = buildBatchForwardAsAttachmentPayload(
transformed,
t('email_composer.prefix.forward'),
(count) => t('email_composer.forward_batch_count', { count }),
{
template: emailDownloadTemplate,
spaceReplacement: filenameSpaceReplacement,
lowercase: filenameLowercase,
stripDiacritics: filenameStripDiacritics,
collapseSeparators: filenameCollapseSeparators,
},
);
if (!payload) return; // nothing attachable - same silent no-op as the single flow
// A veto on any one original cancels the whole batch compose: opening a
// composer missing a message a plugin refused would forward a partial set
// the user never chose. (ReplyContext is single-email; no batch variant.)
for (const e of transformed) {
const ok = await emailHooks.onBeforeForward.intercept({
originalEmailId: e.id,
originalEmail: emailToReadView(e),
mode: 'forward' as const,
});
if (!ok) return;
}
startFreshComposerSession();
setPendingDraft({
to: "",
cc: "",
bcc: "",
subject: payload.subject,
body: "",
showCc: false,
showBcc: false,
selectedIdentityId: null,
subAddressTag: "",
mode: "forward",
draftId: null,
// No single original subject to quote - the count-based subject above
// is the whole story, and the composer seeds attachments from this array.
replyTo: { attachments: payload.attachments },
});
setComposerMode('forward');
setShowComposer(true);
if (isMobile) setActiveView('viewer');
clearSelection(); // only on success - a vetoed/no-op attempt keeps the selection
};
const handleDelete = async (emailToDelete: Email | null = selectedEmail) => {
if (!client || !emailToDelete) return;
@@ -4922,6 +5001,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
selectEmail(email);
handleForwardAsAttachment(email);
}}
onBatchForwardAsAttachment={() => {
void handleBatchForwardAsAttachment();
}}
onMarkAsRead={async (email, read) => {
if (client) {
await markAsRead(client, email.id, read);
+79 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { buildForwardAsAttachmentPayload } from '@/lib/forward-as-attachment';
import { buildForwardAsAttachmentPayload, buildBatchForwardAsAttachmentPayload } from '@/lib/forward-as-attachment';
import type { Email } from '@/lib/jmap/types';
// Pin TZ so the local-time date rendering in the filename test is deterministic,
@@ -93,3 +93,81 @@ describe('buildForwardAsAttachmentPayload', () => {
expect(payload?.attachment.name).not.toContain('Bobby');
});
});
describe('buildBatchForwardAsAttachmentPayload', () => {
const countLabel = (n: number) => `${n} email${n === 1 ? '' : 's'}`;
it('returns null when no email has a blobId', () => {
const emails = [makeEmail({ id: 'e1', blobId: undefined }), makeEmail({ id: 'e2', blobId: undefined })];
expect(buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel)).toBeNull();
});
it('skips emails without a blobId and attaches the rest', () => {
const emails = [
makeEmail({ id: 'e1', blobId: 'blob-1' }),
makeEmail({ id: 'e2', blobId: undefined }),
makeEmail({ id: 'e3', blobId: 'blob-3' }),
];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel);
expect(payload?.attachments.map((a) => a.blobId)).toEqual(['blob-1', 'blob-3']);
});
it('builds the subject from the attachable count, not the input length', () => {
const emails = [
makeEmail({ id: 'e1', blobId: 'blob-1' }),
makeEmail({ id: 'e2', blobId: 'blob-2' }),
makeEmail({ id: 'e3', blobId: undefined }),
];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel);
expect(payload?.subject).toBe('Fwd: 2 emails');
});
it('prefixes the count phrase using the given forward prefix', () => {
const emails = [makeEmail({ id: 'e1', blobId: 'b1' }), makeEmail({ id: 'e2', blobId: 'b2' })];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'WG:', countLabel);
expect(payload?.subject).toBe('WG: 2 emails');
});
it('preserves the input list order of the attachments', () => {
const emails = [
makeEmail({ id: 'e1', blobId: 'b1', subject: 'First' }),
makeEmail({ id: 'e2', blobId: 'b2', subject: 'Second' }),
makeEmail({ id: 'e3', blobId: 'b3', subject: 'Third' }),
];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel);
expect(payload?.attachments.map((a) => a.name)).toEqual([
expect.stringMatching(/first/i),
expect.stringMatching(/second/i),
expect.stringMatching(/third/i),
]);
});
it('references each email\'s own blobId and size as message/rfc822 .eml entries', () => {
const emails = [
makeEmail({ id: 'e1', blobId: 'b1', size: 100 }),
makeEmail({ id: 'e2', blobId: 'b2', size: 200 }),
];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel);
expect(payload?.attachments).toEqual([
{ blobId: 'b1', name: expect.stringMatching(/\.eml$/), type: 'message/rfc822', size: 100 },
{ blobId: 'b2', name: expect.stringMatching(/\.eml$/), type: 'message/rfc822', size: 200 },
]);
});
it('applies user space/case transforms but ignores a custom filename template, unlike "Export as .eml"', () => {
const emails = [makeEmail({ id: 'e1', blobId: 'b1', subject: 'Missed spam example' })];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel, {
template: 'custom-{subject}',
lowercase: true,
spaceReplacement: 'dash',
});
expect(payload?.attachments[0].name).toBe('2026-07-26-22.25.22-missed-spam-example.eml');
});
it('supports a single email, producing a "one" count label', () => {
const emails = [makeEmail({ id: 'e1', blobId: 'b1' })];
const payload = buildBatchForwardAsAttachmentPayload(emails, 'Fwd:', countLabel);
expect(payload?.subject).toBe('Fwd: 1 email');
expect(payload?.attachments).toHaveLength(1);
});
});
+40
View File
@@ -14,6 +14,11 @@ export interface ForwardAsAttachmentPayload {
attachment: ForwardAsAttachmentEntry;
}
export interface BatchForwardAsAttachmentPayload {
subject: string;
attachments: ForwardAsAttachmentEntry[];
}
/**
* Build the subject and synthetic attachment entry for forwarding a
* message as a message/rfc822 attachment instead of inline-quoted text
@@ -56,3 +61,38 @@ export function buildForwardAsAttachmentPayload(
},
};
}
/**
* Batch sibling of buildForwardAsAttachmentPayload: one composer carrying
* every selected message as its own message/rfc822 attachment. The subject is
* count-based (there is no single original subject): `countLabel(n)` returns
* the localized phrase for n messages (e.g. "3 emails") and is called with
* the number actually attached, so the subject never claims more attachments
* than the composer carries. Emails without a blobId are skipped; returns
* null when none of them has one (same silent no-op as the single flow).
*
* Attachments keep the input list order so the composer shows them in the
* same order the list did. Per-message "{date}-{subject}" filenames would
* collide only when two messages share subject AND receivedAt-to-the-second
* - rare enough to tolerate (JMAP accepts duplicate part names), so no
* dedupe suffix.
*/
export function buildBatchForwardAsAttachmentPayload(
emails: Email[],
forwardPrefix: string,
countLabel: (count: number) => string,
filenameOptions?: EmailFilenameOptions,
): BatchForwardAsAttachmentPayload | null {
const attachable = emails.filter((email) => email.blobId);
if (attachable.length === 0) return null;
return {
subject: buildForwardSubject(countLabel(attachable.length), forwardPrefix),
attachments: attachable.map((email) => ({
blobId: email.blobId!,
name: emailExportFilename(email, { ...filenameOptions, template: "{date}-{subject}" }),
type: "message/rfc822" as const,
size: email.size,
})),
};
}
+2
View File
@@ -675,6 +675,7 @@
"forward": "إعادة توجيه:",
"reply": "رد:"
},
"forward_batch_count": "{count, plural, one {رسالة واحدة} other {# رسائل}}",
"no_subject": "(بلا موضوع)",
"unknown_sender": "غير معروف",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "ليس مزعجًا",
"tag": "وسم",
"items_selected": "{count} رسالة محددة",
"forward_as_attachments": "إعادة التوجيه كمرفقات",
"edit_draft": "تعديل المسودة",
"cancel_scheduled_send": "إلغاء الإرسال",
"reschedule_send": "إعادة الجدولة",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Reenv.:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 correu} other {# correus}}",
"no_subject": "(Sense assumpte)",
"unknown_sender": "Desconegut",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "No és brossa",
"tag": "Etiqueta",
"items_selected": "{count} correus seleccionats",
"forward_as_attachments": "Reenvia com a fitxers adjunts",
"edit_draft": "Edita l'esborrany",
"cancel_scheduled_send": "Cancel·la l'enviament",
"reschedule_send": "Reprograma",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 zpráva} few {# zprávy} other {# zpráv}}",
"no_subject": "(Bez předmětu)",
"unknown_sender": "Neznámý",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Není spam",
"tag": "Štítek",
"items_selected": "{count} vybraných zpráv",
"forward_as_attachments": "Přeposlat jako přílohy",
"edit_draft": "Upravit koncept",
"cancel_scheduled_send": "Zrušit odeslání",
"reschedule_send": "Přeplánovat",
+2
View File
@@ -674,6 +674,7 @@
"forward": "Vs:",
"reply": "Sv:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} other {# e-mails}}",
"no_subject": "(Intet emne)",
"unknown_sender": "Ukendt",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Ikke spam",
"tag": "Tag",
"items_selected": "{count} e-mails valgt",
"forward_as_attachments": "Videresend som vedhæftede filer",
"edit_draft": "Redigér kladde",
"cancel_scheduled_send": "Annuller afsendelse",
"reschedule_send": "Planlæg om",
+2
View File
@@ -672,6 +672,7 @@
"forward": "WG:",
"reply": "AW:"
},
"forward_batch_count": "{count, plural, one {1 E-Mail} other {# E-Mails}}",
"no_subject": "(Kein Betreff)",
"unknown_sender": "Unbekannt",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Kein Spam",
"tag": "Label",
"items_selected": "{count} E-Mails ausgewählt",
"forward_as_attachments": "Als Anhänge weiterleiten",
"edit_draft": "Entwurf bearbeiten",
"cancel_scheduled_send": "Senden abbrechen",
"reschedule_send": "Neu planen",
+2
View File
@@ -676,6 +676,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 email} other {# emails}}",
"no_subject": "(No Subject)",
"unknown_sender": "Unknown",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Not spam",
"tag": "Tag",
"items_selected": "{count} emails selected",
"forward_as_attachments": "Forward as attachments",
"edit_draft": "Edit Draft",
"cancel_scheduled_send": "Cancel send",
"reschedule_send": "Reschedule",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Rvf:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 correo} other {# correos}}",
"no_subject": "(Sin Asunto)",
"unknown_sender": "Desconocido",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "No es spam",
"tag": "Etiqueta",
"items_selected": "{count} correos seleccionados",
"forward_as_attachments": "Reenviar como archivos adjuntos",
"edit_draft": "Editar borrador",
"cancel_scheduled_send": "Cancelar envío",
"reschedule_send": "Reprogramar",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count} ایمیل",
"no_subject": "(بدون موضوع)",
"unknown_sender": "ناشناس",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "هرزنامه نیست",
"tag": "برچسب",
"items_selected": "{count} ایمیل انتخاب شده",
"forward_as_attachments": "ارسال به‌عنوان پیوست‌ها",
"edit_draft": "ویرایش پیش‌نویس",
"cancel_scheduled_send": "لغو ارسال",
"reschedule_send": "باززمان‌بندی",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Tr:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} other {# e-mails}}",
"no_subject": "(Sans objet)",
"unknown_sender": "Inconnu",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Pas un spam",
"tag": "Étiquette",
"items_selected": "{count} emails sélectionnés",
"forward_as_attachments": "Transférer en pièces jointes",
"edit_draft": "Modifier le brouillon",
"cancel_scheduled_send": "Annuler l’envoi",
"reschedule_send": "Replanifier",
+2
View File
@@ -629,6 +629,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {הודעת דוא״ל אחת} other {# הודעות דוא״ל}}",
"no_subject": "(ללא נושא)",
"unknown_sender": "לא ידוע",
"quote": {
@@ -2474,6 +2475,7 @@
"not_spam": "לא ספאם",
"tag": "תווית",
"items_selected": "נבחרו הודעות דוא\"ל מסוג{count}",
"forward_as_attachments": "העבר כקבצים מצורפים",
"edit_draft": "ערוך טיוטה",
"cancel_scheduled_send": "ביטול שליחה",
"reschedule_send": "תזמן מחדש",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Továbbítás:",
"reply": "Válasz:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} other {# e-mail}}",
"no_subject": "(Nincs tárgy)",
"unknown_sender": "Ismeretlen",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Nem spam",
"tag": "Címke",
"items_selected": "{count} e-mail kijelölve",
"forward_as_attachments": "Továbbítás mellékletként",
"edit_draft": "Piszkozat szerkesztése",
"cancel_scheduled_send": "Küldés megszakítása",
"reschedule_send": "Átütemezés",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Inol:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 email} other {# email}}",
"no_subject": "(Nessun oggetto)",
"unknown_sender": "Sconosciuto",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Non spam",
"tag": "Etichetta",
"items_selected": "{count} messaggi selezionati",
"forward_as_attachments": "Inoltra come allegati",
"edit_draft": "Modifica bozza",
"cancel_scheduled_send": "Annulla invio",
"reschedule_send": "Riprogramma",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count}件のメール",
"no_subject": "(件名なし)",
"unknown_sender": "不明",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "迷惑メールでない",
"tag": "ラベル",
"items_selected": "{count}件のメールを選択",
"forward_as_attachments": "添付ファイルとして転送",
"edit_draft": "下書きを編集",
"cancel_scheduled_send": "送信をキャンセル",
"reschedule_send": "再予約",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count}개의 메일",
"no_subject": "(제목 없음)",
"unknown_sender": "알 수 없음",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "정상 메일",
"tag": "태그",
"items_selected": "{count}개의 메일 선택됨",
"forward_as_attachments": "첨부 파일로 전달",
"edit_draft": "임시보관 메일 수정",
"cancel_scheduled_send": "보내기 취소",
"reschedule_send": "다시 예약",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 vēstule} other {# vēstules}}",
"no_subject": "(nav temata)",
"unknown_sender": "Nezināms",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Nav mēstule",
"tag": "Tags",
"items_selected": "{count} vēstules atlasītas",
"forward_as_attachments": "Pārsūtīt kā pielikumiem",
"edit_draft": "Rediģēt melnrakstu",
"cancel_scheduled_send": "Atcelt sūtīšanu",
"reschedule_send": "Pārplānot",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Дахин:",
"reply": "Дахин:"
},
"forward_batch_count": "{count, plural, one {1 имэйл} other {# имэйл}}",
"no_subject": "(Сэдэв байхгүй)",
"unknown_sender": "Үл мэдэгдэх",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Спам биш",
"tag": "Таг",
"items_selected": "{count} имэйл сонгосон",
"forward_as_attachments": "Хавсралт болгон дамжуулах",
"edit_draft": "Ноорог засах",
"cancel_scheduled_send": "Илгээхийг цуцлах",
"reschedule_send": "Хуваарь солих",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-post} other {# e-poster}}",
"no_subject": "(Uten emne)",
"unknown_sender": "Ukjent",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Ikke søppelpost",
"tag": "Etikett",
"items_selected": "Valgt: {count}",
"forward_as_attachments": "Videresend som vedlegg",
"edit_draft": "Rediger utkast",
"cancel_scheduled_send": "Avbryt sending",
"reschedule_send": "Planlegg på nytt",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} other {# e-mails}}",
"no_subject": "(Geen onderwerp)",
"unknown_sender": "Onbekend",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Geen spam",
"tag": "Label",
"items_selected": "{count} e-mails geselecteerd",
"forward_as_attachments": "Doorsturen als bijlagen",
"edit_draft": "Concept bewerken",
"cancel_scheduled_send": "Verzenden annuleren",
"reschedule_send": "Opnieuw plannen",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} few {# e-maile} many {# e-maili} other {# e-maili}}",
"no_subject": "(Bez tematu)",
"unknown_sender": "Nieznany",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "To nie spam",
"tag": "Etykieta",
"items_selected": "{count} zaznaczonych wiadomości",
"forward_as_attachments": "Prześlij dalej jako załączniki",
"edit_draft": "Edytuj szkic",
"cancel_scheduled_send": "Anuluj wysyłkę",
"reschedule_send": "Przełóż",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Enc:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} other {# e-mails}}",
"no_subject": "(Sem Assunto)",
"unknown_sender": "Desconhecido",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Não é spam",
"tag": "Etiqueta",
"items_selected": "{count} e-mails selecionados",
"forward_as_attachments": "Encaminhar como anexos",
"edit_draft": "Editar rascunho",
"cancel_scheduled_send": "Cancelar envio",
"reschedule_send": "Reagendar",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Redirecționare:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} few {# e-mailuri} other {# de e-mailuri}}",
"no_subject": "(Fără subiect)",
"unknown_sender": "Necunoscut",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Nu este spam",
"tag": "Etichetă",
"items_selected": "{count} e-mailuri selectate",
"forward_as_attachments": "Redirecționează ca atașamente",
"edit_draft": "Editează schița",
"cancel_scheduled_send": "Anulează trimiterea",
"reschedule_send": "Reprogramare",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Пересл:",
"reply": "Ответ:"
},
"forward_batch_count": "{count, plural, one {1 письмо} few {# письма} many {# писем} other {# писем}}",
"no_subject": "(Без темы)",
"unknown_sender": "Неизвестный",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Не спам",
"tag": "Тег",
"items_selected": "{count} писем выбрано",
"forward_as_attachments": "Переслать как вложения",
"edit_draft": "Редактировать черновик",
"cancel_scheduled_send": "Отменить отправку",
"reschedule_send": "Перенести",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 e-mail} few {# e-maily} other {# e-mailov}}",
"no_subject": "(Bez predmetu)",
"unknown_sender": "Neznámy",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Nie je spam",
"tag": "Štítok",
"items_selected": "{count} vybraných e-mailov",
"forward_as_attachments": "Preposlať ako prílohy",
"edit_draft": "Upraviť koncept",
"cancel_scheduled_send": "Zrušiť odoslanie",
"reschedule_send": "Prenaplánovať",
+2
View File
@@ -675,6 +675,7 @@
"forward": "İlt:",
"reply": "Yn:"
},
"forward_batch_count": "{count, plural, one {1 e-posta} other {# e-posta}}",
"no_subject": "(Konu Yok)",
"unknown_sender": "Bilinmeyen",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Spam değil",
"tag": "Etiket",
"items_selected": "{count} e-posta seçildi",
"forward_as_attachments": "Ek olarak ilet",
"edit_draft": "Taslağı Düzenle",
"cancel_scheduled_send": "Göndermeyi iptal et",
"reschedule_send": "Yeniden zamanla",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count, plural, one {1 лист} few {# листи} many {# листів} other {# листів}}",
"no_subject": "(без теми)",
"unknown_sender": "Невідомий",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "Не спам",
"tag": "Мітка",
"items_selected": "Вибрано електронних листів: {count}",
"forward_as_attachments": "Переслати як вкладення",
"edit_draft": "Редагувати чернетку",
"cancel_scheduled_send": "Скасувати надсилання",
"reschedule_send": "Перенести",
+2
View File
@@ -675,6 +675,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count} 封郵件",
"no_subject": "(無主題)",
"unknown_sender": "未知",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "非垃圾郵件",
"tag": "標籤",
"items_selected": "已選取 {count} 封郵件",
"forward_as_attachments": "以附件形式轉寄",
"edit_draft": "編輯草稿",
"cancel_scheduled_send": "取消傳送",
"reschedule_send": "重新排程",
+2
View File
@@ -672,6 +672,7 @@
"forward": "Fwd:",
"reply": "Re:"
},
"forward_batch_count": "{count} 封邮件",
"no_subject": "(无主题)",
"unknown_sender": "未知",
"quote": {
@@ -2508,6 +2509,7 @@
"not_spam": "不是垃圾邮件",
"tag": "标签",
"items_selected": "已选择 {count} 封邮件",
"forward_as_attachments": "作为附件转发",
"edit_draft": "编辑草稿",
"cancel_scheduled_send": "取消发送",
"reschedule_send": "重新计划",