From 05e2837f6b3c20937e002e059ba4ab6ac4cd25f2 Mon Sep 17 00:00:00 2001 From: dealerweb Date: Fri, 29 May 2026 13:02:12 +0200 Subject: [PATCH] Fix: localize reply/forward quote header incl. sender address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reply/forward quote header was always emitted in English ("On {date}, {from} wrote:", "---------- Forwarded message ----------", From/Date/Subject) regardless of UI language, in both the main path (lib/quote-header.ts) and the composer's inline fallback. quote-header.ts now takes an optional localized QuoteHeaderLabels set (English defaults preserved for back-compat); page.tsx builds it from a new quote_header message namespace, and the composer fallback uses the same keys. Added the quote_header namespace to all 17 locales. Also folds in the forward-sender-address fix: the forward "From:" line now shows the full "Name " like every mail client (the reply line keeps the bare name, which reads naturally in "On … wrote:"). --- app/(main)/[locale]/page.tsx | 10 ++++++- components/email/email-composer.tsx | 25 ++++++++++++----- lib/quote-header.ts | 42 ++++++++++++++++++++++++++--- locales/cs/common.json | 7 +++++ locales/da/common.json | 7 +++++ locales/de/common.json | 7 +++++ locales/en/common.json | 7 +++++ locales/es/common.json | 7 +++++ locales/fr/common.json | 7 +++++ locales/it/common.json | 7 +++++ locales/ja/common.json | 7 +++++ locales/ko/common.json | 7 +++++ locales/lv/common.json | 7 +++++ locales/nl/common.json | 7 +++++ locales/pl/common.json | 7 +++++ locales/pt/common.json | 7 +++++ locales/ru/common.json | 7 +++++ locales/tr/common.json | 7 +++++ locales/uk/common.json | 7 +++++ locales/zh/common.json | 7 +++++ 20 files changed, 185 insertions(+), 11 deletions(-) diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index ae57e8ec..e951b6c9 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -81,6 +81,7 @@ const SCHEDULED_MAILBOX_ID = '__scheduled__'; export default function Home() { const t = useTranslations(); const tCommon = useTranslations('common'); + const tQuote = useTranslations('quote_header'); const { appName } = useConfig(); const mailLayout = useSettingsStore((state) => state.mailLayout); const [showComposer, setShowComposer] = useState(false); @@ -1212,13 +1213,20 @@ export default function Home() { locale: useLocaleStore.getState().locale, timeFormat: useSettingsStore.getState().timeFormat, unknownLabel: tCommon('unknown'), + labels: { + formatReplyLine: (vars) => tQuote('reply_line', vars), + forwardedSeparator: tQuote('forwarded_separator'), + fromLabel: tQuote('from_label'), + dateLabel: tQuote('date_label'), + subjectLabel: tQuote('subject_label'), + }, }); setComposerQuoteHeader(header); } catch (err) { console.warn('[quote-header] plugin transform failed; using default', err); setComposerQuoteHeader(null); } - }, [tCommon]); + }, [tCommon, tQuote]); // Force a clean composer remount on every fresh entry point so prior // compose state can't bleed into the new session (#329 C). The composer is diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 64f9c9d8..2b9566b1 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -196,6 +196,7 @@ export function EmailComposer({ }: EmailComposerProps) { const t = useTranslations('email_composer'); const tCommon = useTranslations('common'); + const tQuote = useTranslations('quote_header'); const timeFormat = useSettingsStore((state) => state.timeFormat); const plainTextMode = useSettingsStore((state) => state.plainTextMode); const subAddressDelimiter = useSettingsStore((state) => state.subAddressDelimiter); @@ -291,6 +292,13 @@ export function EmailComposer({ const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ""; const from = replyTo.from?.[0]; const fromStr = from ? `${from.name || from.email}` : tCommon('unknown'); + // Forward "From:" shows the full sender incl. address; reply line keeps + // the bare name (reads naturally in the localized "On … wrote:" line). + const fromStrFull = from + ? (from.name && from.email && from.name !== from.email + ? `${from.name} <${from.email}>` + : (from.email || from.name || tCommon('unknown'))) + : tCommon('unknown'); const originalText = replyTo.body || (replyTo.htmlBody ? htmlToPlainText(replyTo.htmlBody) : ''); const quotedText = originalText.split('\n').map(line => `> ${line}`).join('\n'); @@ -311,9 +319,9 @@ export function EmailComposer({ } if (mode === 'forward') { - return `${prefix}${signatureBlock}\n\n---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ''}\n\n${originalText}`; + return `${prefix}${signatureBlock}\n\n${tQuote('forwarded_separator')}\n${tQuote('from_label')}: ${fromStrFull}\n${tQuote('date_label')}: ${date}\n${tQuote('subject_label')}: ${replyTo.subject || ''}\n\n${originalText}`; } else if (mode === 'reply' || mode === 'replyAll') { - return `${prefix}${signatureBlock}\n\nOn ${date}, ${fromStr} wrote:\n${quotedText}`; + return `${prefix}${signatureBlock}\n\n${tQuote('reply_line', { date, from: fromStr })}\n${quotedText}`; } return prefix; } @@ -336,6 +344,11 @@ export function EmailComposer({ const date = replyTo.receivedAt ? formatDateTime(replyTo.receivedAt, timeFormat, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) : ""; const from = replyTo.from?.[0]; const fromStr = from ? `${from.name || from.email}` : tCommon('unknown'); + const fromStrFull = from + ? (from.name && from.email && from.name !== from.email + ? `${from.name} <${from.email}>` + : (from.email || from.name || tCommon('unknown'))) + : tCommon('unknown'); const signatureBlock = buildEmbeddedSignatureHtml(initialSignatureIdentity, { embed: shouldEmbedSignatureAboveQuote, @@ -359,8 +372,8 @@ export function EmailComposer({ // Build quoted content as HTML if (replyTo.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) { const quoteHeader = mode === 'forward' - ? `---------- Forwarded message ----------
From: ${fromStr}
Date: ${date}
Subject: ${replyTo.subject || ''}

` - : `On ${date}, ${fromStr} wrote:
`; + ? `${tQuote('forwarded_separator')}
${tQuote('from_label')}: ${fromStrFull}
${tQuote('date_label')}: ${date}
${tQuote('subject_label')}: ${replyTo.subject || ''}

` + : `${tQuote('reply_line', { date, from: fromStr })}
`; // cid: image refs are rewritten so they render in the editor (browsers // can't fetch cid: URLs); see useEffect below for the data-URL backfill. const quotedHtml = rewriteCidImagesForEditor(replyTo.htmlBody); @@ -370,9 +383,9 @@ export function EmailComposer({ if (replyTo.body) { const escapedOriginal = replyTo.body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
'); if (mode === 'forward') { - return `${prefix}${signatureBlock}

---------- Forwarded message ----------
From: ${fromStr}
Date: ${date}
Subject: ${replyTo.subject || ''}

${escapedOriginal}`; + return `${prefix}${signatureBlock}

${tQuote('forwarded_separator')}
${tQuote('from_label')}: ${fromStrFull}
${tQuote('date_label')}: ${date}
${tQuote('subject_label')}: ${replyTo.subject || ''}

${escapedOriginal}`; } else if (mode === 'reply' || mode === 'replyAll') { - return `${prefix}${signatureBlock}

On ${date}, ${fromStr} wrote:
${escapedOriginal}
`; + return `${prefix}${signatureBlock}

${tQuote('reply_line', { date, from: fromStr })}
${escapedOriginal}
`; } } return prefix; diff --git a/lib/quote-header.ts b/lib/quote-header.ts index 786abfc8..c55eb046 100644 --- a/lib/quote-header.ts +++ b/lib/quote-header.ts @@ -10,6 +10,17 @@ import { formatDateTime } from "@/lib/utils"; import { emailHooks } from "@/lib/plugin-hooks"; import type { QuoteHeader, QuoteHeaderContext } from "@/lib/plugin-types"; +// Localized label set the caller passes in. Labels live on the client where +// useTranslations is available; this module stays framework-agnostic. +export interface QuoteHeaderLabels { + /** ICU-formatted reply line, e.g. "On {date}, {from} wrote:" with placeholders already substituted. */ + formatReplyLine: (vars: { date: string; from: string }) => string; + forwardedSeparator: string; + fromLabel: string; + dateLabel: string; + subjectLabel: string; +} + interface BuildArgs { mode: "reply" | "replyAll" | "forward"; email: { @@ -24,10 +35,24 @@ interface BuildArgs { locale: string; timeFormat: "12h" | "24h"; unknownLabel: string; + /** + * Localized labels. Optional for backward compatibility; falls back to + * English (matching the original hardcoded behaviour) when not supplied. + */ + labels?: QuoteHeaderLabels; } +const DEFAULT_LABELS: QuoteHeaderLabels = { + formatReplyLine: ({ date, from }) => `On ${date}, ${from} wrote:`, + forwardedSeparator: "---------- Forwarded message ----------", + fromLabel: "From", + dateLabel: "Date", + subjectLabel: "Subject", +}; + function defaultHeader(args: BuildArgs): QuoteHeader { const { mode, email, timeFormat, unknownLabel } = args; + const labels = args.labels ?? DEFAULT_LABELS; const date = email.receivedAt ? formatDateTime(email.receivedAt, timeFormat, { weekday: "short", @@ -38,16 +63,25 @@ function defaultHeader(args: BuildArgs): QuoteHeader { : ""; const from = email.from?.[0]; const fromStr = from ? `${from.name || from.email}` : unknownLabel; + // Forward header "From:" shows the full sender incl. address ("Name + // "), like every mail client. The reply line keeps the bare name + // (reads more naturally in "On … wrote:"). + const fromStrFull = from + ? (from.name && from.email && from.name !== from.email + ? `${from.name} <${from.email}>` + : (from.email || from.name || unknownLabel)) + : unknownLabel; const subject = email.subject || ""; if (mode === "forward") { - const text = `---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${subject}\n`; - const html = `
---------- Forwarded message ----------
From: ${fromStr}
Date: ${date}
Subject: ${subject}

`; + const text = `${labels.forwardedSeparator}\n${labels.fromLabel}: ${fromStrFull}\n${labels.dateLabel}: ${date}\n${labels.subjectLabel}: ${subject}\n`; + const html = `
${labels.forwardedSeparator}
${labels.fromLabel}: ${fromStrFull}
${labels.dateLabel}: ${date}
${labels.subjectLabel}: ${subject}

`; return { html, text, wrapInBlockquote: false }; } - const text = `On ${date}, ${fromStr} wrote:\n`; - const html = `
On ${date}, ${fromStr} wrote:
`; + const replyLine = labels.formatReplyLine({ date, from: fromStr }); + const text = `${replyLine}\n`; + const html = `
${replyLine}
`; return { html, text, wrapInBlockquote: true }; } diff --git a/locales/cs/common.json b/locales/cs/common.json index 6620a55b..3ce40153 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Dne {date} napsal(a) {from}:", + "forwarded_separator": "---------- Přeposlaná zpráva ----------", + "from_label": "Od", + "date_label": "Datum", + "subject_label": "Předmět" + }, "login": { "title": "Webmail", "username_label": "E-mail", diff --git a/locales/da/common.json b/locales/da/common.json index 2f455b42..26d72337 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Den {date} skrev {from}:", + "forwarded_separator": "---------- Videresendt besked ----------", + "from_label": "Fra", + "date_label": "Dato", + "subject_label": "Emne" + }, "login": { "title": "Webmail", "username_label": "Email", diff --git a/locales/de/common.json b/locales/de/common.json index fa621a99..180258ae 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Am {date} schrieb {from}:", + "forwarded_separator": "---------- Weitergeleitete Nachricht ----------", + "from_label": "Von", + "date_label": "Datum", + "subject_label": "Betreff" + }, "login": { "title": "Webmail", "username_label": "E-Mail", diff --git a/locales/en/common.json b/locales/en/common.json index 1dbba593..b24adfd3 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "On {date}, {from} wrote:", + "forwarded_separator": "---------- Forwarded message ----------", + "from_label": "From", + "date_label": "Date", + "subject_label": "Subject" + }, "login": { "title": "Webmail", "username_label": "Email", diff --git a/locales/es/common.json b/locales/es/common.json index 7b83d66a..615ebeb1 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "El {date}, {from} escribió:", + "forwarded_separator": "---------- Mensaje reenviado ----------", + "from_label": "De", + "date_label": "Fecha", + "subject_label": "Asunto" + }, "login": { "title": "Correo Web", "username_label": "Correo electrónico", diff --git a/locales/fr/common.json b/locales/fr/common.json index f9cb00ad..0ac4ee61 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Le {date}, {from} a écrit :", + "forwarded_separator": "---------- Message transféré ----------", + "from_label": "De", + "date_label": "Date", + "subject_label": "Sujet" + }, "login": { "title": "Webmail", "username_label": "Email", diff --git a/locales/it/common.json b/locales/it/common.json index 3c96458a..6da32b5e 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Il {date}, {from} ha scritto:", + "forwarded_separator": "---------- Messaggio inoltrato ----------", + "from_label": "Da", + "date_label": "Data", + "subject_label": "Oggetto" + }, "login": { "title": "Webmail", "username_label": "Email", diff --git a/locales/ja/common.json b/locales/ja/common.json index aa366688..ff4153fe 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date}に{from}が書きました:", + "forwarded_separator": "---------- 転送メッセージ ----------", + "from_label": "差出人", + "date_label": "日付", + "subject_label": "件名" + }, "login": { "title": "ウェブメール", "username_label": "メールアドレス", diff --git a/locales/ko/common.json b/locales/ko/common.json index 79a238d7..1545df90 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date}에 {from}님이 작성:", + "forwarded_separator": "---------- 전달된 메시지 ----------", + "from_label": "보낸 사람", + "date_label": "날짜", + "subject_label": "제목" + }, "login": { "title": "웹메일", "username_label": "이메일", diff --git a/locales/lv/common.json b/locales/lv/common.json index 62a09509..2dd8d7b0 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date} {from} rakstīja:", + "forwarded_separator": "---------- Pārsūtītā ziņa ----------", + "from_label": "No", + "date_label": "Datums", + "subject_label": "Tēma" + }, "login": { "title": "Tīmekļa pasts", "username_label": "E-pasta adrese", diff --git a/locales/nl/common.json b/locales/nl/common.json index d1208b36..2901d918 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Op {date} schreef {from}:", + "forwarded_separator": "---------- Doorgestuurd bericht ----------", + "from_label": "Van", + "date_label": "Datum", + "subject_label": "Onderwerp" + }, "login": { "title": "Webmail", "username_label": "E-mail", diff --git a/locales/pl/common.json b/locales/pl/common.json index ee550e28..5cf29463 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date}, {from} napisał(a):", + "forwarded_separator": "---------- Wiadomość przekazana ----------", + "from_label": "Od", + "date_label": "Data", + "subject_label": "Temat" + }, "login": { "title": "Webmail", "username_label": "E-mail", diff --git a/locales/pt/common.json b/locales/pt/common.json index 3f7ef129..47b5d68c 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "Em {date}, {from} escreveu:", + "forwarded_separator": "---------- Mensagem encaminhada ----------", + "from_label": "De", + "date_label": "Data", + "subject_label": "Assunto" + }, "login": { "title": "Webmail", "username_label": "E-mail", diff --git a/locales/ru/common.json b/locales/ru/common.json index 747b269c..b46e2970 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date}, {from} написал:", + "forwarded_separator": "---------- Пересланное сообщение ----------", + "from_label": "От", + "date_label": "Дата", + "subject_label": "Тема" + }, "login": { "title": "Веб-почта", "username_label": "Электронная почта", diff --git a/locales/tr/common.json b/locales/tr/common.json index 287522ab..b590d642 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date} tarihinde {from} şunu yazdı:", + "forwarded_separator": "---------- İletilen mesaj ----------", + "from_label": "Kimden", + "date_label": "Tarih", + "subject_label": "Konu" + }, "login": { "title": "Webmail", "username_label": "E-posta", diff --git a/locales/uk/common.json b/locales/uk/common.json index e08ee372..c2aed60b 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "{date}, {from} написав:", + "forwarded_separator": "---------- Переслане повідомлення ----------", + "from_label": "Від", + "date_label": "Дата", + "subject_label": "Тема" + }, "login": { "title": "Веб-пошта", "username_label": "Електронна пошта", diff --git a/locales/zh/common.json b/locales/zh/common.json index 00a7634f..dc473be0 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1,4 +1,11 @@ { + "quote_header": { + "reply_line": "在 {date},{from} 写道:", + "forwarded_separator": "---------- 转发邮件 ----------", + "from_label": "发件人", + "date_label": "日期", + "subject_label": "主题" + }, "login": { "title": "网页邮箱", "username_label": "邮箱地址",