mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
fix: show Not spam in Spam and Restore and Delete forever in Trash on the reader toolbar
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { render, act, fireEvent, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
|
||||
// Spam and Trash showed the Inbox's toolbar. Spam now has "Not spam" in
|
||||
// Archive's place and no quick reply; Trash has "Restore" there and its
|
||||
// Delete says "Delete forever".
|
||||
|
||||
vi.mock('@/hooks/use-media-query', () => ({
|
||||
useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
vi.mock('@/components/tour/tour-provider', () => ({
|
||||
useTour: () => ({ activeTour: null, startTour: vi.fn(), endTour: vi.fn(), registerStep: vi.fn() }),
|
||||
}));
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({ usePluginSlotOffers: () => [] }));
|
||||
|
||||
class NoopResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', NoopResizeObserver);
|
||||
|
||||
const { EmailViewer } = await import('../email-viewer');
|
||||
|
||||
const mailboxes = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
|
||||
{ id: 'junk', name: 'Junk', role: 'junk', parentId: null, myRights: { mayAddItems: true } },
|
||||
{ id: 'trash', name: 'Trash', role: 'trash', parentId: null, myRights: { mayAddItems: true } },
|
||||
] as unknown as Mailbox[];
|
||||
|
||||
const message = (folder: string) => ({
|
||||
id: 'm1', blobId: 'b1', threadId: 't1', mailboxIds: { [folder]: true }, keywords: { $seen: true },
|
||||
from: [{ name: 'Dave', email: 'dave@example.net' }], to: [{ name: 'Alice', email: 'alice@example.org' }],
|
||||
subject: 'Hello', receivedAt: '2026-01-01T00:00:00Z', preview: 'Hello', hasAttachment: false, size: 100,
|
||||
textBody: [{ partId: '1', type: 'text/plain' }], htmlBody: [{ partId: '1', type: 'text/plain' }], attachments: [],
|
||||
bodyValues: { '1': { value: 'Plain body' } },
|
||||
}) as unknown as Email;
|
||||
|
||||
const handlers = () => ({
|
||||
onArchive: vi.fn(),
|
||||
onDelete: vi.fn(),
|
||||
onMarkAsSpam: vi.fn(),
|
||||
onUndoSpam: vi.fn(),
|
||||
onMoveToMailbox: vi.fn(),
|
||||
onQuickReply: vi.fn(async () => true),
|
||||
});
|
||||
|
||||
async function viewer(folder: string, h = handlers()) {
|
||||
render(<EmailViewer email={message(folder)} isLoading={false} mailboxes={mailboxes} selectedMailbox={folder} currentMailboxRole={folder} {...h} />);
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 10)); });
|
||||
return h;
|
||||
}
|
||||
|
||||
const archiveButton = () => document.querySelector('button[title="tooltips.archive"]');
|
||||
|
||||
beforeEach(() => {
|
||||
useSettingsStore.setState({ deleteAction: 'trash', permanentlyDeleteJunk: false, quickReplyEnabled: true });
|
||||
});
|
||||
|
||||
describe('reader toolbar in Spam', () => {
|
||||
it('offers Not spam in place of Archive', async () => {
|
||||
const h = await viewer('junk');
|
||||
expect(archiveButton()).toBeNull();
|
||||
fireEvent.click(screen.getByTestId('reader-not-spam'));
|
||||
expect(h.onUndoSpam).toHaveBeenCalled();
|
||||
// Only once: the old spam button no longer says it as well.
|
||||
expect(document.querySelectorAll('button[title="spam.not_spam_title"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('has no quick reply', async () => {
|
||||
await viewer('junk');
|
||||
expect(screen.queryByPlaceholderText('quick_reply_placeholder')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reader toolbar in Trash', () => {
|
||||
it('offers Restore in place of Archive, back to the Inbox', async () => {
|
||||
const h = await viewer('trash');
|
||||
expect(archiveButton()).toBeNull();
|
||||
fireEvent.click(screen.getByTestId('reader-restore'));
|
||||
expect(h.onMoveToMailbox).toHaveBeenCalledWith('inbox');
|
||||
});
|
||||
|
||||
it('says Delete forever', async () => {
|
||||
await viewer('trash');
|
||||
expect(screen.getByTestId('reader-delete').textContent).toContain('delete_forever');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reader toolbar in the Inbox', () => {
|
||||
it('keeps Archive, Delete and quick reply', async () => {
|
||||
await viewer('inbox');
|
||||
expect(archiveButton()).not.toBeNull();
|
||||
expect(screen.queryByTestId('reader-restore')).toBeNull();
|
||||
expect(screen.queryByTestId('reader-not-spam')).toBeNull();
|
||||
expect(screen.getByTestId('reader-delete').textContent).not.toContain('delete_forever');
|
||||
expect(screen.getByPlaceholderText('quick_reply_placeholder')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
Minus,
|
||||
ShieldCheck,
|
||||
ShieldAlert,
|
||||
RotateCcw,
|
||||
Copy,
|
||||
Brain,
|
||||
Keyboard,
|
||||
@@ -140,7 +141,9 @@ import { useDeviceDetection } from "@/hooks/use-media-query";
|
||||
import { useIsPaneScoped } from "@/hooks/use-pane-context";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
import { useEmailStore, resolveEmailBlobContext, emailWriteSource, type EmailBlobContext } from "@/stores/email-store";
|
||||
import { useEmailStore, resolveEmailBlobContext, emailWriteSource, bulkDeleteIsPermanent, type EmailBlobContext } from "@/stores/email-store";
|
||||
import { trashRestoreTarget } from "@/lib/trash-restore-target";
|
||||
import { localizeMailboxName } from "@/lib/mailbox-label";
|
||||
import { useListSelection } from "@/lib/plugin-sandbox/assistant/b.list-selection";
|
||||
import { readability } from "@/lib/plugin-sandbox/assistant/b.projection";
|
||||
import { useThemeStore } from "@/stores/theme-store";
|
||||
@@ -1463,6 +1466,15 @@ export function EmailViewer({
|
||||
|
||||
// Detect if current mailbox is Junk folder
|
||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||
// Trash offers Restore in Archive's place; there and wherever a delete
|
||||
// destroys the mail, Delete says "Delete forever".
|
||||
const isInTrashFolder = currentMailboxRole === 'trash';
|
||||
const tActions = useTranslations('reader_actions');
|
||||
const tSidebarNames = useTranslations('sidebar');
|
||||
// Read for the re-render: bulkDeleteIsPermanent reads them from the store.
|
||||
useSettingsStore((state) => state.deleteAction);
|
||||
useSettingsStore((state) => state.permanentlyDeleteJunk);
|
||||
const deleteIsPermanent = bulkDeleteIsPermanent({ role: currentMailboxRole });
|
||||
// Marking your own outgoing mail as spam makes no sense - hide the action
|
||||
// in Sent, Drafts and Scheduled.
|
||||
const spamApplicable = !['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || '');
|
||||
@@ -1999,6 +2011,8 @@ export function EmailViewer({
|
||||
[mailboxes, selectedMailbox, moveOwnerAccountId, email],
|
||||
);
|
||||
|
||||
const restoreTarget = isInTrashFolder && email ? trashRestoreTarget(email, mailboxes, identities, moveOwnerAccountId) : undefined;
|
||||
|
||||
// Close dropdown menus on click outside
|
||||
useEffect(() => {
|
||||
if (!moreMenuOpen) return;
|
||||
@@ -3469,7 +3483,8 @@ export function EmailViewer({
|
||||
const [iframeReadyFor, setIframeReadyFor] = useState<string | null>(null);
|
||||
const latestSrcDocRef = useRef<string | null>(null);
|
||||
const iframeReady = iframeReadyFor !== null && iframeReadyFor === emailIframeSrcDoc;
|
||||
const showQuickReply = !!email && quickReplyEnabled && !mvReplyRow && !isDraft && !isScheduled && !isBodyLoading
|
||||
// Not on spam: a reply confirms the address to the sender.
|
||||
const showQuickReply = !!email && quickReplyEnabled && !mvReplyRow && !isDraft && !isScheduled && !isBodyLoading && !isInJunkFolder
|
||||
&& (!effectiveEmailContent.isHtml || iframeReady);
|
||||
// The plain-text body, where reader-above-quote mounts between text and quote.
|
||||
const [plainBodyEl, setPlainBodyEl] = useState<HTMLDivElement | null>(null);
|
||||
@@ -4250,7 +4265,10 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
const toolbarShownActions = (): ReadonlySet<MessageActionKey> => {
|
||||
const shown = new Set<MessageActionKey>(['delete', 'edit_draft']);
|
||||
toolbarRef.current?.querySelectorAll<HTMLElement>('[data-overflow-item]').forEach((item) => {
|
||||
const key = TOOLBAR_PRIORITY_ACTIONS[item.dataset.overflowPriority ?? ''];
|
||||
// A button standing in for another (Not spam in Archive's place) says which entry it is.
|
||||
const key = item.dataset.overflowAction !== undefined
|
||||
? item.dataset.overflowAction as MessageActionKey
|
||||
: TOOLBAR_PRIORITY_ACTIONS[item.dataset.overflowPriority ?? ''];
|
||||
if (key && item.getClientRects().length > 0) shown.add(key);
|
||||
});
|
||||
if (isMobile && !mvMobileViewer) {
|
||||
@@ -4467,8 +4485,40 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
{/* Right: Organize actions - order: archive, delete, move, tag, spam, read state, print, view source */}
|
||||
{!isScheduled && (
|
||||
<div className="flex items-center gap-0 sm:gap-0.5">
|
||||
{/* Archive */}
|
||||
{archiveActionEnabled && (
|
||||
{/* Archive. In Spam "Not spam" takes its place, in Trash "Restore". */}
|
||||
{isInJunkFolder && onUndoSpam && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onUndoSpam}
|
||||
data-overflow-item
|
||||
data-overflow-priority="4"
|
||||
data-overflow-action="spam"
|
||||
data-testid="reader-not-spam"
|
||||
className={cn("flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0", mvViewer ? "order-1" : "hover:bg-emerald-50 dark:hover:bg-emerald-950/30")}
|
||||
title={t('spam.not_spam_title')}
|
||||
>
|
||||
<ShieldCheck className={cn("h-4 w-4", !mvViewer && "text-emerald-600 dark:text-emerald-400")} />
|
||||
{showToolbarLabels && <span data-toolbar-label="" className="text-[0.625rem] leading-tight sm:hidden sm:text-sm sm:whitespace-nowrap sm:@[30rem]/reader-toolbar:inline">{t('spam.not_spam_title')}</span>}
|
||||
</Button>
|
||||
)}
|
||||
{isInTrashFolder && restoreTarget && onMoveToMailbox && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveToMailbox(restoreTarget.id)}
|
||||
data-overflow-item
|
||||
data-overflow-priority="4"
|
||||
data-overflow-action=""
|
||||
data-testid="reader-restore"
|
||||
className={cn("flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0", mvViewer && "order-1")}
|
||||
title={tActions('restore_to', { folder: localizeMailboxName(restoreTarget.role, restoreTarget.name, (key) => tSidebarNames(`mailboxes.${key}`)) })}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
{showToolbarLabels && <span data-toolbar-label="" className="text-[0.625rem] leading-tight sm:hidden sm:text-sm sm:whitespace-nowrap sm:@[30rem]/reader-toolbar:inline">{tActions('restore')}</span>}
|
||||
</Button>
|
||||
)}
|
||||
{archiveActionEnabled && !isInJunkFolder && !isInTrashFolder && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -4488,10 +4538,11 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
className={cn("flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0", mvViewer && "order-3")}
|
||||
title={t('tooltips.delete')}
|
||||
title={deleteIsPermanent ? tActions('delete_forever') : t('tooltips.delete')}
|
||||
data-testid="reader-delete"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{showToolbarLabels && <span data-toolbar-label="" className="text-[0.625rem] leading-tight sm:hidden sm:text-sm sm:whitespace-nowrap sm:@[30rem]/reader-toolbar:inline">{t('delete')}</span>}
|
||||
{showToolbarLabels && <span data-toolbar-label="" className="text-[0.625rem] leading-tight sm:hidden sm:text-sm sm:whitespace-nowrap sm:@[30rem]/reader-toolbar:inline">{deleteIsPermanent ? tActions('delete_forever') : t('delete')}</span>}
|
||||
</Button>
|
||||
{/* Move to folder */}
|
||||
{moveTree.length > 0 && onMoveToMailbox && (
|
||||
@@ -4558,7 +4609,7 @@ ${printWatermark ? printWatermark.html : ''}
|
||||
</div>
|
||||
|
||||
{/* Spam */}
|
||||
{spamApplicable && (onMarkAsSpam || onUndoSpam) && (
|
||||
{spamApplicable && !isInJunkFolder && onMarkAsSpam && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { trashRestoreTarget } from '../trash-restore-target';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
|
||||
const mailboxes = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox' },
|
||||
{ id: 'sent', name: 'Sent', role: 'sent' },
|
||||
{ id: 'drafts', name: 'Drafts', role: 'drafts' },
|
||||
{ id: 'trash', name: 'Trash', role: 'trash' },
|
||||
{ id: 'b:inbox', name: 'Inbox', role: 'inbox', accountId: 'b', isShared: true },
|
||||
] as unknown as Mailbox[];
|
||||
const identities = [{ email: 'Robin@example.org' }];
|
||||
const mail = (from: string, keywords: Record<string, boolean> = {}) =>
|
||||
({ from: [{ email: from }], keywords }) as unknown as Email;
|
||||
|
||||
describe('trashRestoreTarget', () => {
|
||||
it('puts received mail back in the Inbox', () => {
|
||||
expect(trashRestoreTarget(mail('dave@example.net'), mailboxes, identities)?.id).toBe('inbox');
|
||||
});
|
||||
|
||||
it('puts your own mail back in Sent and drafts back in Drafts', () => {
|
||||
expect(trashRestoreTarget(mail('robin@example.org'), mailboxes, identities)?.id).toBe('sent');
|
||||
expect(trashRestoreTarget(mail('robin@example.org', { $draft: true }), mailboxes, identities)?.id).toBe('drafts');
|
||||
});
|
||||
|
||||
it("uses the message's own account", () => {
|
||||
expect(trashRestoreTarget(mail('dave@example.net'), mailboxes, identities, 'b')?.id).toBe('b:inbox');
|
||||
});
|
||||
|
||||
it('falls back to the Inbox when the folder is missing', () => {
|
||||
const noSent = mailboxes.filter((m) => m.role !== 'sent');
|
||||
expect(trashRestoreTarget(mail('robin@example.org'), noSent, identities)?.id).toBe('inbox');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Email, Identity, Mailbox } from "@/lib/jmap/types";
|
||||
import { isAuthenticationSpoofed } from "@/lib/email-headers";
|
||||
|
||||
/**
|
||||
* The folder Restore puts a message in Trash back into. The server keeps no
|
||||
* record of the folder a message was deleted from, so this goes by what the
|
||||
* message is: a draft back to Drafts, mail you sent back to Sent, anything
|
||||
* else to the Inbox. In a list that mixes accounts, the message's own
|
||||
* account's folder. Undefined when there's no such folder.
|
||||
*/
|
||||
export function trashRestoreTarget(
|
||||
email: Pick<Email, "keywords" | "from" | "authenticationResults">,
|
||||
mailboxes: Mailbox[],
|
||||
identities: Pick<Identity, "email">[],
|
||||
ownerAccountId?: string,
|
||||
): Mailbox | undefined {
|
||||
const from = email.from?.[0]?.email?.toLowerCase();
|
||||
const own = !!from
|
||||
&& !isAuthenticationSpoofed(email.authenticationResults)
|
||||
&& identities.some((identity) => identity.email?.toLowerCase() === from);
|
||||
const role = email.keywords?.["$draft"] === true ? "drafts" : own ? "sent" : "inbox";
|
||||
const pick = (wanted: string) => {
|
||||
const candidates = mailboxes.filter((m) => m.role === wanted);
|
||||
return candidates.find((m) => !!ownerAccountId && m.accountId === ownerAccountId)
|
||||
?? candidates.find((m) => !m.isShared)
|
||||
?? candidates[0];
|
||||
};
|
||||
return pick(role) ?? (role === "inbox" ? undefined : pick("inbox"));
|
||||
}
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "إيقاف القراءة",
|
||||
"reader_view": "عرض القراءة"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "استعادة",
|
||||
"restore_to": "إعادة النقل إلى {folder}",
|
||||
"delete_forever": "حذف نهائي"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "خارجي",
|
||||
"sender_tooltip": "من {domain}، من خارج مؤسستك",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Atura la lectura",
|
||||
"reader_view": "Vista de lectura"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restaura",
|
||||
"restore_to": "Torna a moure a {folder}",
|
||||
"delete_forever": "Suprimeix per sempre"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
"sender_tooltip": "De {domain}, fora de la vostra organització",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Ukončit předčítání",
|
||||
"reader_view": "Zobrazení pro čtení"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Obnovit",
|
||||
"restore_to": "Přesunout zpět do složky {folder}",
|
||||
"delete_forever": "Smazat natrvalo"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externí",
|
||||
"sender_tooltip": "Z {domain}, mimo vaši organizaci",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Stop oplæsning",
|
||||
"reader_view": "Læsevisning"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Gendan",
|
||||
"restore_to": "Flyt tilbage til {folder}",
|
||||
"delete_forever": "Slet permanent"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ekstern",
|
||||
"sender_tooltip": "Fra {domain}, uden for din organisation",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Vorlesen beenden",
|
||||
"reader_view": "Leseansicht"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Wiederherstellen",
|
||||
"restore_to": "Zurück nach {folder} verschieben",
|
||||
"delete_forever": "Endgültig löschen"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
"sender_tooltip": "Von {domain}, außerhalb Ihrer Organisation",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Stop reading",
|
||||
"reader_view": "Reader view"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restore",
|
||||
"restore_to": "Move back to {folder}",
|
||||
"delete_forever": "Delete forever"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "External",
|
||||
"sender_tooltip": "From {domain}, outside your organisation",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Detener la lectura",
|
||||
"reader_view": "Vista de lectura"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restaurar",
|
||||
"restore_to": "Volver a mover a {folder}",
|
||||
"delete_forever": "Eliminar para siempre"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externo",
|
||||
"sender_tooltip": "De {domain}, fuera de tu organización",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Lõpeta ettelugemine",
|
||||
"reader_view": "Lugemisvaade"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Taasta",
|
||||
"restore_to": "Teisalda tagasi kausta {folder}",
|
||||
"delete_forever": "Kustuta jäädavalt"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Väline",
|
||||
"sender_tooltip": "Domeenilt {domain}, väljastpoolt sinu organisatsiooni",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "پایان خواندن",
|
||||
"reader_view": "نمای مطالعه"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "بازیابی",
|
||||
"restore_to": "انتقال دوباره به {folder}",
|
||||
"delete_forever": "حذف برای همیشه"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "خارجی",
|
||||
"sender_tooltip": "از {domain}، خارج از سازمان شما",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Arrêter la lecture",
|
||||
"reader_view": "Mode lecture"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restaurer",
|
||||
"restore_to": "Remettre dans {folder}",
|
||||
"delete_forever": "Supprimer définitivement"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externe",
|
||||
"sender_tooltip": "De {domain}, hors de votre organisation",
|
||||
|
||||
@@ -836,6 +836,11 @@
|
||||
"read_aloud_stop": "עצירת ההקראה",
|
||||
"reader_view": "תצוגת קריאה"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "שחזור",
|
||||
"restore_to": "החזרה אל {folder}",
|
||||
"delete_forever": "מחיקה לצמיתות"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "חיצוני",
|
||||
"sender_tooltip": "מ־{domain}, מחוץ לארגון שלך",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Felolvasás leállítása",
|
||||
"reader_view": "Olvasó nézet"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Visszaállítás",
|
||||
"restore_to": "Visszahelyezés ide: {folder}",
|
||||
"delete_forever": "Végleges törlés"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Külső",
|
||||
"sender_tooltip": "Innen: {domain}, a szervezeteden kívülről",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Interrompi la lettura",
|
||||
"reader_view": "Modalità lettura"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Ripristina",
|
||||
"restore_to": "Riporta in {folder}",
|
||||
"delete_forever": "Elimina definitivamente"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Esterno",
|
||||
"sender_tooltip": "Da {domain}, fuori dalla tua organizzazione",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "読み上げを停止",
|
||||
"reader_view": "リーダー表示"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "元に戻す",
|
||||
"restore_to": "{folder} に戻す",
|
||||
"delete_forever": "完全に削除"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
"sender_tooltip": "{domain} から(組織外)",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "읽기 중지",
|
||||
"reader_view": "읽기 보기"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "복원",
|
||||
"restore_to": "{folder}(으)로 되돌리기",
|
||||
"delete_forever": "영구 삭제"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "외부",
|
||||
"sender_tooltip": "{domain}에서 보냄, 조직 외부",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Beigt nolasīšanu",
|
||||
"reader_view": "Lasīšanas skats"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Atjaunot",
|
||||
"restore_to": "Pārvietot atpakaļ uz {folder}",
|
||||
"delete_forever": "Dzēst neatgriezeniski"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ārējs",
|
||||
"sender_tooltip": "No {domain}, ārpus jūsu organizācijas",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Уншихыг зогсоох",
|
||||
"reader_view": "Унших харагдац"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Сэргээх",
|
||||
"restore_to": "{folder} руу буцаах",
|
||||
"delete_forever": "Бүрмөсөн устгах"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Гадаад",
|
||||
"sender_tooltip": "{domain}-аас, таны байгууллагаас гадуур",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Stopp opplesning",
|
||||
"reader_view": "Lesevisning"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Gjenopprett",
|
||||
"restore_to": "Flytt tilbake til {folder}",
|
||||
"delete_forever": "Slett for godt"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ekstern",
|
||||
"sender_tooltip": "Fra {domain}, utenfor organisasjonen din",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Voorlezen stoppen",
|
||||
"reader_view": "Leesweergave"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Terugzetten",
|
||||
"restore_to": "Terugzetten naar {folder}",
|
||||
"delete_forever": "Definitief verwijderen"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
"sender_tooltip": "Van {domain}, buiten je organisatie",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Zakończ czytanie",
|
||||
"reader_view": "Widok do czytania"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Przywróć",
|
||||
"restore_to": "Przenieś z powrotem do folderu {folder}",
|
||||
"delete_forever": "Usuń na zawsze"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Zewnętrzny",
|
||||
"sender_tooltip": "Z domeny {domain}, spoza Twojej organizacji",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Parar a leitura",
|
||||
"reader_view": "Vista de leitura"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restaurar",
|
||||
"restore_to": "Mover de volta para {folder}",
|
||||
"delete_forever": "Eliminar para sempre"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externo",
|
||||
"sender_tooltip": "De {domain}, fora da sua organização",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Opriți citirea",
|
||||
"reader_view": "Vizualizare pentru citire"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Restaurează",
|
||||
"restore_to": "Mută înapoi în {folder}",
|
||||
"delete_forever": "Șterge definitiv"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
"sender_tooltip": "De la {domain}, din afara organizației dvs.",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Остановить чтение",
|
||||
"reader_view": "Режим чтения"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Восстановить",
|
||||
"restore_to": "Вернуть в папку {folder}",
|
||||
"delete_forever": "Удалить навсегда"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Внешнее",
|
||||
"sender_tooltip": "С домена {domain}, вне вашей организации",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Zastaviť čítanie",
|
||||
"reader_view": "Zobrazenie na čítanie"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Obnoviť",
|
||||
"restore_to": "Presunúť späť do priečinka {folder}",
|
||||
"delete_forever": "Odstrániť natrvalo"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externé",
|
||||
"sender_tooltip": "Z {domain}, mimo vašej organizácie",
|
||||
|
||||
@@ -1989,6 +1989,11 @@
|
||||
"read_aloud_stop": "หยุดอ่าน",
|
||||
"reader_view": "มุมมองการอ่าน"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "กู้คืน",
|
||||
"restore_to": "ย้ายกลับไปที่ {folder}",
|
||||
"delete_forever": "ลบถาวร"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "ภายนอก",
|
||||
"sender_tooltip": "จาก {domain} ซึ่งอยู่นอกองค์กรของคุณ",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Okumayı durdur",
|
||||
"reader_view": "Okuma görünümü"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Geri yükle",
|
||||
"restore_to": "{folder} klasörüne geri taşı",
|
||||
"delete_forever": "Kalıcı olarak sil"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Harici",
|
||||
"sender_tooltip": "{domain} adresinden, kuruluşunuzun dışından",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Зупинити читання",
|
||||
"reader_view": "Режим читання"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Відновити",
|
||||
"restore_to": "Повернути до теки {folder}",
|
||||
"delete_forever": "Видалити назавжди"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Зовнішнє",
|
||||
"sender_tooltip": "З домену {domain}, поза вашою організацією",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "Dừng đọc",
|
||||
"reader_view": "Chế độ đọc"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "Khôi phục",
|
||||
"restore_to": "Chuyển lại vào {folder}",
|
||||
"delete_forever": "Xóa vĩnh viễn"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Bên ngoài",
|
||||
"sender_tooltip": "Từ {domain}, bên ngoài tổ chức của bạn",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "停止朗讀",
|
||||
"reader_view": "閱讀檢視"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "復原",
|
||||
"restore_to": "移回 {folder}",
|
||||
"delete_forever": "永久刪除"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
"sender_tooltip": "來自 {domain},位於您的組織之外",
|
||||
|
||||
@@ -893,6 +893,11 @@
|
||||
"read_aloud_stop": "停止朗读",
|
||||
"reader_view": "阅读视图"
|
||||
},
|
||||
"reader_actions": {
|
||||
"restore": "恢复",
|
||||
"restore_to": "移回 {folder}",
|
||||
"delete_forever": "永久删除"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
"sender_tooltip": "来自 {domain},位于您的组织之外",
|
||||
|
||||
Reference in New Issue
Block a user