mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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": "إعادة الجدولة",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "باززمانبندی",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "תזמן מחדש",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "再予約",
|
||||
|
||||
@@ -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": "다시 예약",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Хуваарь солих",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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łóż",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Перенести",
|
||||
|
||||
@@ -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ť",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Перенести",
|
||||
|
||||
@@ -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": "重新排程",
|
||||
|
||||
@@ -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": "重新计划",
|
||||
|
||||
Reference in New Issue
Block a user