From caed2d313a91cced1ac612e9240eccb63ac67b9b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:21:31 +0100 Subject: [PATCH] feat: open a message in a reader view with the app's font, spacing and colours --- .../email/__tests__/viewer-reading.test.tsx | 32 +++++++ components/email/email-viewer.tsx | 35 ++++++- components/email/reader-view-dialog.tsx | 93 +++++++++++++++++++ locales/ar/common.json | 3 +- locales/ca/common.json | 3 +- locales/cs/common.json | 3 +- locales/da/common.json | 3 +- locales/de/common.json | 3 +- locales/en/common.json | 3 +- locales/es/common.json | 3 +- locales/et/common.json | 3 +- locales/fa/common.json | 3 +- locales/fr/common.json | 3 +- locales/he/common.json | 3 +- locales/hu/common.json | 3 +- locales/it/common.json | 3 +- locales/ja/common.json | 3 +- locales/ko/common.json | 3 +- locales/lv/common.json | 3 +- locales/mn/common.json | 3 +- locales/nb/common.json | 3 +- locales/nl/common.json | 3 +- locales/pl/common.json | 3 +- locales/pt/common.json | 3 +- locales/ro/common.json | 3 +- locales/ru/common.json | 3 +- locales/sk/common.json | 3 +- locales/th/common.json | 3 +- locales/tr/common.json | 3 +- locales/uk/common.json | 3 +- locales/zh-TW/common.json | 3 +- locales/zh/common.json | 3 +- 32 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 components/email/reader-view-dialog.tsx diff --git a/components/email/__tests__/viewer-reading.test.tsx b/components/email/__tests__/viewer-reading.test.tsx index 6beb7d355..bc836a301 100644 --- a/components/email/__tests__/viewer-reading.test.tsx +++ b/components/email/__tests__/viewer-reading.test.tsx @@ -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: '

Fancy HTML version

', 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: '

Read this in peace.

' })); + 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); + }); +}); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index f33bf24a5..dcf28eeed 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -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(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}> + {readerViewOpen && ( + 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 && (
{t('shown_as_plain_text')} diff --git a/components/email/reader-view-dialog.tsx b/components/email/reader-view-dialog.tsx new file mode 100644 index 000000000..cde105f44 --- /dev/null +++ b/components/email/reader-view-dialog.tsx @@ -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(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 ( + +
+ {readAloud.supported && ( + readAloud.state === 'idle' ? ( +
+ +
+ ) : ( + + ) + )} +
+
+
+ ); +} diff --git a/locales/ar/common.json b/locales/ar/common.json index 96e592cf1..33a387c3c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -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": "خارجي", diff --git a/locales/ca/common.json b/locales/ca/common.json index a3121b834..2b3021495 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -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", diff --git a/locales/cs/common.json b/locales/cs/common.json index ed0380503..48d230bae 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -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í", diff --git a/locales/da/common.json b/locales/da/common.json index e4ac0f290..ea480d6ec 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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", diff --git a/locales/de/common.json b/locales/de/common.json index 2e4fc1cc1..f63ef2062 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index 3f0dd76f0..e0374cac0 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es/common.json b/locales/es/common.json index d718282b7..eebd5f73c 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -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", diff --git a/locales/et/common.json b/locales/et/common.json index 5bc8385fa..bc63b31ee 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -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", diff --git a/locales/fa/common.json b/locales/fa/common.json index c634f19f0..f51fd07d9 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -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": "خارجی", diff --git a/locales/fr/common.json b/locales/fr/common.json index dc57681ac..6f6179095 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -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", diff --git a/locales/he/common.json b/locales/he/common.json index 4b1eaea93..16aa516c1 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -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": "חיצוני", diff --git a/locales/hu/common.json b/locales/hu/common.json index d49f466c1..02838f42b 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -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ő", diff --git a/locales/it/common.json b/locales/it/common.json index 9883d13b8..debf81de4 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index b951895b2..adec59d42 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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": "外部", diff --git a/locales/ko/common.json b/locales/ko/common.json index 583212e5b..2dfe79b56 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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": "외부", diff --git a/locales/lv/common.json b/locales/lv/common.json index 65b4a97ba..212239a99 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -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", diff --git a/locales/mn/common.json b/locales/mn/common.json index 04102f4a7..3356ff3fe 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -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": "Гадаад", diff --git a/locales/nb/common.json b/locales/nb/common.json index dd51661cd..adff0184a 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -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", diff --git a/locales/nl/common.json b/locales/nl/common.json index 1b0362eae..e1dc5c5bc 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index b7d93be44..987b68291 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt/common.json b/locales/pt/common.json index 012059a38..d08858b7f 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -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", diff --git a/locales/ro/common.json b/locales/ro/common.json index 796465908..7c0be99bd 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index b2dd9928b..90c4def1a 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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": "Внешнее", diff --git a/locales/sk/common.json b/locales/sk/common.json index af3fe0da5..6f784feff 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -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é", diff --git a/locales/th/common.json b/locales/th/common.json index 11c46f06e..9f1583903 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -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": "ภายนอก", diff --git a/locales/tr/common.json b/locales/tr/common.json index 6f64a5cae..d1f4b61d8 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -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", diff --git a/locales/uk/common.json b/locales/uk/common.json index 62581f3dc..b52a1fba4 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -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": "Зовнішнє", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index e8a132779..dfb038405 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -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": "外部", diff --git a/locales/zh/common.json b/locales/zh/common.json index cb52b737f..2918888d2 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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": "外部",