mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: open a message in a reader view with the app's font, spacing and colours
This commit is contained in:
@@ -274,3 +274,35 @@ describe('pausing animated images', () => {
|
||||
expect(await frameKey()).not.toContain('/p');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reader toolbar actions', () => {
|
||||
async function openMenu() {
|
||||
await act(async () => { screen.getByRole('button', { name: 'more_actions' }).click(); });
|
||||
}
|
||||
const item = (name: string) => screen.getAllByRole('menuitem').find((el) => el.textContent?.includes(name));
|
||||
|
||||
it('switches one HTML message to plain text and back', async () => {
|
||||
const { container } = await show(message({ html: '<p>Fancy <b>HTML</b> version</p>', text: 'The plain version' }));
|
||||
await openMenu();
|
||||
await act(async () => { item('show_as_plain_text')!.click(); });
|
||||
expect(container.querySelector('iframe')).toBeNull();
|
||||
expect(screen.getByText('The plain version')).toBeInTheDocument();
|
||||
await openMenu();
|
||||
expect(item('show_formatted')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('opens the reader view as a dialog that Escape closes, with focus back', async () => {
|
||||
await show(message({ html: '<p>Read <a href="https://ex.com/r">this</a> in peace.</p>' }));
|
||||
const more = screen.getByRole('button', { name: 'more_actions' });
|
||||
await openMenu();
|
||||
await act(async () => { item('reader_view')!.click(); });
|
||||
const dialog = screen.getByRole('dialog', { name: 'Bonjour' });
|
||||
expect(dialog).toHaveAttribute('aria-modal', 'true');
|
||||
expect(screen.getByTestId('reader-view-body').textContent).toContain('Read this (https://ex.com/r) in peace.');
|
||||
expect(dialog.contains(document.activeElement)).toBe(true);
|
||||
await act(async () => { document.activeElement!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); });
|
||||
expect(screen.queryByRole('dialog', { name: 'Bonjour' })).toBeNull();
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
|
||||
expect(document.activeElement === more || more.contains(document.activeElement)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading";
|
||||
import { useMailReading } from "@/hooks/use-mail-reading";
|
||||
import { useReadAloud } from "@/hooks/use-read-aloud";
|
||||
import { ReadAloudBar } from "./read-aloud-bar";
|
||||
import { ReaderViewDialog } from "./reader-view-dialog";
|
||||
import { collectReferencedCids, isEmbeddedInBody, normalizeCid, rewriteCidReferences } from "@/lib/attachment-visibility";
|
||||
import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collapse";
|
||||
import { DARK_PENDING_CLASS, applyEmailDarkMode, emailColorScheme, emailDarkModeCss, emailHasNativeDarkMode as htmlHasNativeDarkMode, resolveColorSchemeMedia } from "@/lib/email-dark-mode";
|
||||
@@ -108,6 +109,7 @@ import {
|
||||
Minimize2,
|
||||
Type,
|
||||
Volume2,
|
||||
BookOpen,
|
||||
ArrowLeft,
|
||||
Pencil,
|
||||
} from "@/components/icons";
|
||||
@@ -2224,6 +2226,7 @@ export function EmailViewer({
|
||||
setShowSourceModal(false);
|
||||
setEmailViewDarkOverride(null);
|
||||
setPlainViewOverride(null);
|
||||
setReaderViewOpen(false);
|
||||
setPluginRenderedHtml(null);
|
||||
setPluginRenderedText(null);
|
||||
setPluginRenderedAttachments(NO_POSTAL_ATTACHMENTS);
|
||||
@@ -2860,8 +2863,8 @@ export function EmailViewer({
|
||||
const [plainViewOverride, setPlainViewOverride] = useState<boolean | null>(null);
|
||||
const plainView = plainViewOverride ?? mailViewMode === 'plain';
|
||||
const hasHtmlBody = decodedEmailContent.isHtml;
|
||||
const effectiveEmailContent = useMemo(() => {
|
||||
if (!plainView || !decodedEmailContent.isHtml) return decodedEmailContent;
|
||||
// The decoded body as plain text: the text part, else the HTML converted.
|
||||
const readableText = useCallback(() => {
|
||||
let text: string | null = null;
|
||||
if (decodedEmailContent === emailContent && email) {
|
||||
const textPart = email.textBody?.[0];
|
||||
@@ -2869,14 +2872,23 @@ export function EmailViewer({
|
||||
text = email.bodyValues?.[textPart.partId]?.value ?? null;
|
||||
}
|
||||
}
|
||||
if (!text?.trim()) text = htmlToReadableText(decodedEmailContent.html);
|
||||
return text?.trim() ? text : htmlToReadableText(decodedEmailContent.html);
|
||||
}, [decodedEmailContent, emailContent, email]);
|
||||
const effectiveEmailContent = useMemo(() => {
|
||||
if (!plainView || !decodedEmailContent.isHtml) return decodedEmailContent;
|
||||
return {
|
||||
html: collapsePlainTextQuotes(plainTextToSafeHtml(text), { show: t('show_quoted_text'), hide: t('hide_quoted_text') }),
|
||||
html: collapsePlainTextQuotes(plainTextToSafeHtml(readableText()), { show: t('show_quoted_text'), hide: t('hide_quoted_text') }),
|
||||
isHtml: false,
|
||||
hasStyleTag: false,
|
||||
externalBlocked: false,
|
||||
};
|
||||
}, [plainView, decodedEmailContent, emailContent, email, t]);
|
||||
}, [plainView, decodedEmailContent, readableText, t]);
|
||||
// Reader view: the message full screen in the app's own look.
|
||||
const [readerViewOpen, setReaderViewOpen] = useState(false);
|
||||
const readerViewHtml = useMemo(() => {
|
||||
if (!readerViewOpen) return '';
|
||||
return decodedEmailContent.isHtml ? plainTextToSafeHtml(readableText()) : decodedEmailContent.html;
|
||||
}, [readerViewOpen, decodedEmailContent, readableText]);
|
||||
|
||||
const resolveAttachmentName = useCallback(
|
||||
(attachment: EffectiveAttachment) => {
|
||||
@@ -4168,6 +4180,9 @@ export function EmailViewer({
|
||||
...(readAloud.supported && !isBodyLoading
|
||||
? [{ kind: 'item' as const, key: 'extra:read_aloud' as const, icon: Volume2, label: t('read_aloud'), run: readAloud.start }]
|
||||
: []),
|
||||
...(!isBodyLoading
|
||||
? [{ kind: 'item' as const, key: 'extra:reader_view' as const, icon: BookOpen, label: t('reader_view'), run: () => { readAloud.stop(); setReaderViewOpen(true); } }]
|
||||
: []),
|
||||
...(hasHtmlBody
|
||||
? [{ kind: 'item' as const, key: 'extra:plain_text' as const, icon: plainView ? Type : FileText, label: plainView ? t('show_formatted') : t('show_as_plain_text'), run: () => setPlainViewOverride(!plainView) }]
|
||||
: []),
|
||||
@@ -6232,6 +6247,16 @@ export function EmailViewer({
|
||||
)}
|
||||
style={isDark ? { backgroundColor: '#121212' } : undefined}>
|
||||
<ReadAloudBar state={readAloud.state} onPause={readAloud.pause} onResume={readAloud.resume} onStop={readAloud.stop} />
|
||||
{readerViewOpen && (
|
||||
<ReaderViewDialog
|
||||
isOpen
|
||||
onClose={() => setReaderViewOpen(false)}
|
||||
title={email?.subject || t('no_subject')}
|
||||
description={frameSender ? (frameSender.name ? `${frameSender.name} <${frameSender.email}>` : frameSender.email) : undefined}
|
||||
html={readerViewHtml}
|
||||
lang={bodyLanguage}
|
||||
/>
|
||||
)}
|
||||
{!isBodyLoading && plainView && hasHtmlBody && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 px-6 pt-3 text-sm text-muted-foreground" data-testid="plain-view-note">
|
||||
<span>{t('shown_as_plain_text')}</span>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Volume2 } from "@/components/icons";
|
||||
import { useMailReading } from "@/hooks/use-mail-reading";
|
||||
import { useReadAloud } from "@/hooks/use-read-aloud";
|
||||
import { sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization";
|
||||
import { ReadAloudBar } from "./read-aloud-bar";
|
||||
|
||||
interface ReaderViewDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** The subject, as the dialog's title. */
|
||||
title: string;
|
||||
/** Who sent it, under the title. */
|
||||
description?: string;
|
||||
/** The body as safe HTML from plain text (plainTextToSafeHtml). */
|
||||
html: string;
|
||||
/** The body's language, for screen readers and the speech voice. */
|
||||
lang?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reader view: one message, large, in the app's font, text spacing, reading
|
||||
* width and colours instead of the sender's layout, with read aloud built in.
|
||||
* A proper dialog: the focus moves in and stays there, Escape closes it and
|
||||
* the focus goes back to where it was.
|
||||
*/
|
||||
export function ReaderViewDialog({ isOpen, onClose, title, description, html, lang }: ReaderViewDialogProps) {
|
||||
const t = useTranslations('email_viewer');
|
||||
const reading = useMailReading();
|
||||
const articleRef = useRef<HTMLDivElement>(null);
|
||||
const getRoot = useCallback(() => articleRef.current, []);
|
||||
const readAloud = useReadAloud({ getRoot, lang, resetKey: isOpen ? html : null });
|
||||
const body = useMemo(() => ({ __html: sanitizePlainTextRenderedHtml(html) }), [html]);
|
||||
|
||||
const close = () => {
|
||||
readAloud.stop();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
title={title}
|
||||
description={description}
|
||||
className="max-w-none h-full"
|
||||
bodyClassName="flex-1"
|
||||
testId="reader-view"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
{readAloud.supported && (
|
||||
readAloud.state === 'idle' ? (
|
||||
<div>
|
||||
<Button variant="outline" size="sm" onClick={readAloud.start}>
|
||||
<Volume2 className="w-4 h-4 me-1.5" aria-hidden="true" />
|
||||
{t('read_aloud')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ReadAloudBar
|
||||
state={readAloud.state}
|
||||
onPause={readAloud.pause}
|
||||
onResume={readAloud.resume}
|
||||
onStop={readAloud.stop}
|
||||
className="rounded-md border"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
ref={articleRef}
|
||||
lang={lang ?? undefined}
|
||||
dir="auto"
|
||||
className="text-foreground [&_a]:text-primary [&_a]:underline"
|
||||
style={{
|
||||
fontSize: reading.plainFontSize(18),
|
||||
lineHeight: 1.7,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
textAlign: 'start',
|
||||
...(reading.comfortableWidth && { maxWidth: '80ch' }),
|
||||
}}
|
||||
data-testid="reader-view-body"
|
||||
dangerouslySetInnerHTML={body}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "القراءة متوقفة مؤقتًا",
|
||||
"read_aloud_pause": "إيقاف مؤقت",
|
||||
"read_aloud_resume": "استئناف",
|
||||
"read_aloud_stop": "إيقاف القراءة"
|
||||
"read_aloud_stop": "إيقاف القراءة",
|
||||
"reader_view": "عرض القراءة"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "خارجي",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Lectura en pausa",
|
||||
"read_aloud_pause": "Pausa",
|
||||
"read_aloud_resume": "Reprèn",
|
||||
"read_aloud_stop": "Atura la lectura"
|
||||
"read_aloud_stop": "Atura la lectura",
|
||||
"reader_view": "Vista de lectura"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Předčítání pozastaveno",
|
||||
"read_aloud_pause": "Pozastavit",
|
||||
"read_aloud_resume": "Pokračovat",
|
||||
"read_aloud_stop": "Ukončit předčítání"
|
||||
"read_aloud_stop": "Ukončit předčítání",
|
||||
"reader_view": "Zobrazení pro čtení"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externí",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Oplæsning sat på pause",
|
||||
"read_aloud_pause": "Pause",
|
||||
"read_aloud_resume": "Fortsæt",
|
||||
"read_aloud_stop": "Stop oplæsning"
|
||||
"read_aloud_stop": "Stop oplæsning",
|
||||
"reader_view": "Læsevisning"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ekstern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Vorlesen angehalten",
|
||||
"read_aloud_pause": "Anhalten",
|
||||
"read_aloud_resume": "Fortsetzen",
|
||||
"read_aloud_stop": "Vorlesen beenden"
|
||||
"read_aloud_stop": "Vorlesen beenden",
|
||||
"reader_view": "Leseansicht"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Reading paused",
|
||||
"read_aloud_pause": "Pause",
|
||||
"read_aloud_resume": "Resume",
|
||||
"read_aloud_stop": "Stop reading"
|
||||
"read_aloud_stop": "Stop reading",
|
||||
"reader_view": "Reader view"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "External",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Lectura en pausa",
|
||||
"read_aloud_pause": "Pausar",
|
||||
"read_aloud_resume": "Reanudar",
|
||||
"read_aloud_stop": "Detener la lectura"
|
||||
"read_aloud_stop": "Detener la lectura",
|
||||
"reader_view": "Vista de lectura"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externo",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Ettelugemine on peatatud",
|
||||
"read_aloud_pause": "Peata",
|
||||
"read_aloud_resume": "Jätka",
|
||||
"read_aloud_stop": "Lõpeta ettelugemine"
|
||||
"read_aloud_stop": "Lõpeta ettelugemine",
|
||||
"reader_view": "Lugemisvaade"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Väline",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "خواندن متوقف شد",
|
||||
"read_aloud_pause": "توقف",
|
||||
"read_aloud_resume": "ادامه",
|
||||
"read_aloud_stop": "پایان خواندن"
|
||||
"read_aloud_stop": "پایان خواندن",
|
||||
"reader_view": "نمای مطالعه"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "خارجی",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Lecture en pause",
|
||||
"read_aloud_pause": "Pause",
|
||||
"read_aloud_resume": "Reprendre",
|
||||
"read_aloud_stop": "Arrêter la lecture"
|
||||
"read_aloud_stop": "Arrêter la lecture",
|
||||
"reader_view": "Mode lecture"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externe",
|
||||
|
||||
@@ -747,7 +747,8 @@
|
||||
"read_aloud_paused": "ההקראה מושהית",
|
||||
"read_aloud_pause": "השהיה",
|
||||
"read_aloud_resume": "המשך",
|
||||
"read_aloud_stop": "עצירת ההקראה"
|
||||
"read_aloud_stop": "עצירת ההקראה",
|
||||
"reader_view": "תצוגת קריאה"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "חיצוני",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Felolvasás szüneteltetve",
|
||||
"read_aloud_pause": "Szünet",
|
||||
"read_aloud_resume": "Folytatás",
|
||||
"read_aloud_stop": "Felolvasás leállítása"
|
||||
"read_aloud_stop": "Felolvasás leállítása",
|
||||
"reader_view": "Olvasó nézet"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Külső",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Lettura in pausa",
|
||||
"read_aloud_pause": "Pausa",
|
||||
"read_aloud_resume": "Riprendi",
|
||||
"read_aloud_stop": "Interrompi la lettura"
|
||||
"read_aloud_stop": "Interrompi la lettura",
|
||||
"reader_view": "Modalità lettura"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Esterno",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "読み上げを一時停止中",
|
||||
"read_aloud_pause": "一時停止",
|
||||
"read_aloud_resume": "再開",
|
||||
"read_aloud_stop": "読み上げを停止"
|
||||
"read_aloud_stop": "読み上げを停止",
|
||||
"reader_view": "リーダー表示"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "읽기 일시 중지됨",
|
||||
"read_aloud_pause": "일시 중지",
|
||||
"read_aloud_resume": "다시 시작",
|
||||
"read_aloud_stop": "읽기 중지"
|
||||
"read_aloud_stop": "읽기 중지",
|
||||
"reader_view": "읽기 보기"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "외부",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Nolasīšana pauzēta",
|
||||
"read_aloud_pause": "Pauzēt",
|
||||
"read_aloud_resume": "Turpināt",
|
||||
"read_aloud_stop": "Beigt nolasīšanu"
|
||||
"read_aloud_stop": "Beigt nolasīšanu",
|
||||
"reader_view": "Lasīšanas skats"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ārējs",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Уншихыг түр зогсоосон",
|
||||
"read_aloud_pause": "Түр зогсоох",
|
||||
"read_aloud_resume": "Үргэлжлүүлэх",
|
||||
"read_aloud_stop": "Уншихыг зогсоох"
|
||||
"read_aloud_stop": "Уншихыг зогсоох",
|
||||
"reader_view": "Унших харагдац"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Гадаад",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Opplesning satt på pause",
|
||||
"read_aloud_pause": "Pause",
|
||||
"read_aloud_resume": "Fortsett",
|
||||
"read_aloud_stop": "Stopp opplesning"
|
||||
"read_aloud_stop": "Stopp opplesning",
|
||||
"reader_view": "Lesevisning"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Ekstern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Voorlezen gepauzeerd",
|
||||
"read_aloud_pause": "Pauzeren",
|
||||
"read_aloud_resume": "Hervatten",
|
||||
"read_aloud_stop": "Voorlezen stoppen"
|
||||
"read_aloud_stop": "Voorlezen stoppen",
|
||||
"reader_view": "Leesweergave"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Czytanie wstrzymane",
|
||||
"read_aloud_pause": "Wstrzymaj",
|
||||
"read_aloud_resume": "Wznów",
|
||||
"read_aloud_stop": "Zakończ czytanie"
|
||||
"read_aloud_stop": "Zakończ czytanie",
|
||||
"reader_view": "Widok do czytania"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Zewnętrzny",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Leitura em pausa",
|
||||
"read_aloud_pause": "Pausar",
|
||||
"read_aloud_resume": "Retomar",
|
||||
"read_aloud_stop": "Parar a leitura"
|
||||
"read_aloud_stop": "Parar a leitura",
|
||||
"reader_view": "Vista de leitura"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externo",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Citire întreruptă",
|
||||
"read_aloud_pause": "Pauză",
|
||||
"read_aloud_resume": "Reluați",
|
||||
"read_aloud_stop": "Opriți citirea"
|
||||
"read_aloud_stop": "Opriți citirea",
|
||||
"reader_view": "Vizualizare pentru citire"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Extern",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Чтение приостановлено",
|
||||
"read_aloud_pause": "Пауза",
|
||||
"read_aloud_resume": "Продолжить",
|
||||
"read_aloud_stop": "Остановить чтение"
|
||||
"read_aloud_stop": "Остановить чтение",
|
||||
"reader_view": "Режим чтения"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Внешнее",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Čítanie pozastavené",
|
||||
"read_aloud_pause": "Pozastaviť",
|
||||
"read_aloud_resume": "Pokračovať",
|
||||
"read_aloud_stop": "Zastaviť čítanie"
|
||||
"read_aloud_stop": "Zastaviť čítanie",
|
||||
"reader_view": "Zobrazenie na čítanie"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Externé",
|
||||
|
||||
@@ -1712,7 +1712,8 @@
|
||||
"read_aloud_paused": "หยุดการอ่านชั่วคราว",
|
||||
"read_aloud_pause": "หยุดชั่วคราว",
|
||||
"read_aloud_resume": "อ่านต่อ",
|
||||
"read_aloud_stop": "หยุดอ่าน"
|
||||
"read_aloud_stop": "หยุดอ่าน",
|
||||
"reader_view": "มุมมองการอ่าน"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "ภายนอก",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Okuma duraklatıldı",
|
||||
"read_aloud_pause": "Duraklat",
|
||||
"read_aloud_resume": "Devam et",
|
||||
"read_aloud_stop": "Okumayı durdur"
|
||||
"read_aloud_stop": "Okumayı durdur",
|
||||
"reader_view": "Okuma görünümü"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Harici",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "Читання призупинено",
|
||||
"read_aloud_pause": "Пауза",
|
||||
"read_aloud_resume": "Продовжити",
|
||||
"read_aloud_stop": "Зупинити читання"
|
||||
"read_aloud_stop": "Зупинити читання",
|
||||
"reader_view": "Режим читання"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "Зовнішнє",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "朗讀已暫停",
|
||||
"read_aloud_pause": "暫停",
|
||||
"read_aloud_resume": "繼續",
|
||||
"read_aloud_stop": "停止朗讀"
|
||||
"read_aloud_stop": "停止朗讀",
|
||||
"reader_view": "閱讀檢視"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
|
||||
@@ -782,7 +782,8 @@
|
||||
"read_aloud_paused": "朗读已暂停",
|
||||
"read_aloud_pause": "暂停",
|
||||
"read_aloud_resume": "继续",
|
||||
"read_aloud_stop": "停止朗读"
|
||||
"read_aloud_stop": "停止朗读",
|
||||
"reader_view": "阅读视图"
|
||||
},
|
||||
"external_mail": {
|
||||
"sender_label": "外部",
|
||||
|
||||
Reference in New Issue
Block a user