fix: show Not spam in Spam and Restore and Delete forever in Trash on the reader toolbar

This commit is contained in:
Linus Rath
2026-11-04 06:37:20 +01:00
parent e9931e28f8
commit f828b6dffe
34 changed files with 377 additions and 8 deletions
@@ -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();
});
});
+59 -8
View File
@@ -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');
});
});
+29
View File
@@ -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"));
}
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "إيقاف القراءة",
"reader_view": "عرض القراءة"
},
"reader_actions": {
"restore": "استعادة",
"restore_to": "إعادة النقل إلى {folder}",
"delete_forever": "حذف نهائي"
},
"external_mail": {
"sender_label": "خارجي",
"sender_tooltip": "من {domain}، من خارج مؤسستك",
+5
View File
@@ -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ó",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "پایان خواندن",
"reader_view": "نمای مطالعه"
},
"reader_actions": {
"restore": "بازیابی",
"restore_to": "انتقال دوباره به {folder}",
"delete_forever": "حذف برای همیشه"
},
"external_mail": {
"sender_label": "خارجی",
"sender_tooltip": "از {domain}، خارج از سازمان شما",
+5
View File
@@ -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",
+5
View File
@@ -836,6 +836,11 @@
"read_aloud_stop": "עצירת ההקראה",
"reader_view": "תצוגת קריאה"
},
"reader_actions": {
"restore": "שחזור",
"restore_to": "החזרה אל {folder}",
"delete_forever": "מחיקה לצמיתות"
},
"external_mail": {
"sender_label": "חיצוני",
"sender_tooltip": "מ־{domain}, מחוץ לארגון שלך",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "読み上げを停止",
"reader_view": "リーダー表示"
},
"reader_actions": {
"restore": "元に戻す",
"restore_to": "{folder} に戻す",
"delete_forever": "完全に削除"
},
"external_mail": {
"sender_label": "外部",
"sender_tooltip": "{domain} から(組織外)",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "읽기 중지",
"reader_view": "읽기 보기"
},
"reader_actions": {
"restore": "복원",
"restore_to": "{folder}(으)로 되돌리기",
"delete_forever": "영구 삭제"
},
"external_mail": {
"sender_label": "외부",
"sender_tooltip": "{domain}에서 보냄, 조직 외부",
+5
View File
@@ -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",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "Уншихыг зогсоох",
"reader_view": "Унших харагдац"
},
"reader_actions": {
"restore": "Сэргээх",
"restore_to": "{folder} руу буцаах",
"delete_forever": "Бүрмөсөн устгах"
},
"external_mail": {
"sender_label": "Гадаад",
"sender_tooltip": "{domain}-аас, таны байгууллагаас гадуур",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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",
+5
View File
@@ -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.",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "Остановить чтение",
"reader_view": "Режим чтения"
},
"reader_actions": {
"restore": "Восстановить",
"restore_to": "Вернуть в папку {folder}",
"delete_forever": "Удалить навсегда"
},
"external_mail": {
"sender_label": "Внешнее",
"sender_tooltip": "С домена {domain}, вне вашей организации",
+5
View File
@@ -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",
+5
View File
@@ -1989,6 +1989,11 @@
"read_aloud_stop": "หยุดอ่าน",
"reader_view": "มุมมองการอ่าน"
},
"reader_actions": {
"restore": "กู้คืน",
"restore_to": "ย้ายกลับไปที่ {folder}",
"delete_forever": "ลบถาวร"
},
"external_mail": {
"sender_label": "ภายนอก",
"sender_tooltip": "จาก {domain} ซึ่งอยู่นอกองค์กรของคุณ",
+5
View File
@@ -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",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "Зупинити читання",
"reader_view": "Режим читання"
},
"reader_actions": {
"restore": "Відновити",
"restore_to": "Повернути до теки {folder}",
"delete_forever": "Видалити назавжди"
},
"external_mail": {
"sender_label": "Зовнішнє",
"sender_tooltip": "З домену {domain}, поза вашою організацією",
+5
View File
@@ -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",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "停止朗讀",
"reader_view": "閱讀檢視"
},
"reader_actions": {
"restore": "復原",
"restore_to": "移回 {folder}",
"delete_forever": "永久刪除"
},
"external_mail": {
"sender_label": "外部",
"sender_tooltip": "來自 {domain},位於您的組織之外",
+5
View File
@@ -893,6 +893,11 @@
"read_aloud_stop": "停止朗读",
"reader_view": "阅读视图"
},
"reader_actions": {
"restore": "恢复",
"restore_to": "移回 {folder}",
"delete_forever": "永久删除"
},
"external_mail": {
"sender_label": "外部",
"sender_tooltip": "来自 {domain},位于您的组织之外",