Fix: localize reply/forward quote header incl. sender address

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 <email>" like every mail client (the reply line keeps the
bare name, which reads naturally in "On … wrote:").
This commit is contained in:
dealerweb
2026-05-30 14:09:23 +02:00
parent 7341e47a1b
commit 05e2837f6b
20 changed files with 185 additions and 11 deletions
+9 -1
View File
@@ -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
+19 -6
View File
@@ -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 ----------<br>From: ${fromStr}<br>Date: ${date}<br>Subject: ${replyTo.subject || ''}<br><br>`
: `On ${date}, ${fromStr} wrote:<br>`;
? `${tQuote('forwarded_separator')}<br>${tQuote('from_label')}: ${fromStrFull}<br>${tQuote('date_label')}: ${date}<br>${tQuote('subject_label')}: ${replyTo.subject || ''}<br><br>`
: `${tQuote('reply_line', { date, from: fromStr })}<br>`;
// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
if (mode === 'forward') {
return `${prefix}${signatureBlock}<br><br>---------- Forwarded message ----------<br>From: ${fromStr}<br>Date: ${date}<br>Subject: ${replyTo.subject || ''}<br><br>${escapedOriginal}`;
return `${prefix}${signatureBlock}<br><br>${tQuote('forwarded_separator')}<br>${tQuote('from_label')}: ${fromStrFull}<br>${tQuote('date_label')}: ${date}<br>${tQuote('subject_label')}: ${replyTo.subject || ''}<br><br>${escapedOriginal}`;
} else if (mode === 'reply' || mode === 'replyAll') {
return `${prefix}${signatureBlock}<br><br>On ${date}, ${fromStr} wrote:<br><blockquote style="margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex">${escapedOriginal}</blockquote>`;
return `${prefix}${signatureBlock}<br><br>${tQuote('reply_line', { date, from: fromStr })}<br><blockquote style="margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex">${escapedOriginal}</blockquote>`;
}
}
return prefix;
+38 -4
View File
@@ -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
// <email>"), 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 = `<div>---------- Forwarded message ----------<br>From: ${fromStr}<br>Date: ${date}<br>Subject: ${subject}<br><br></div>`;
const text = `${labels.forwardedSeparator}\n${labels.fromLabel}: ${fromStrFull}\n${labels.dateLabel}: ${date}\n${labels.subjectLabel}: ${subject}\n`;
const html = `<div>${labels.forwardedSeparator}<br>${labels.fromLabel}: ${fromStrFull}<br>${labels.dateLabel}: ${date}<br>${labels.subjectLabel}: ${subject}<br><br></div>`;
return { html, text, wrapInBlockquote: false };
}
const text = `On ${date}, ${fromStr} wrote:\n`;
const html = `<div>On ${date}, ${fromStr} wrote:<br></div>`;
const replyLine = labels.formatReplyLine({ date, from: fromStr });
const text = `${replyLine}\n`;
const html = `<div>${replyLine}<br></div>`;
return { html, text, wrapInBlockquote: true };
}
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -1,4 +1,11 @@
{
"quote_header": {
"reply_line": "{date}に{from}が書きました:",
"forwarded_separator": "---------- 転送メッセージ ----------",
"from_label": "差出人",
"date_label": "日付",
"subject_label": "件名"
},
"login": {
"title": "ウェブメール",
"username_label": "メールアドレス",
+7
View File
@@ -1,4 +1,11 @@
{
"quote_header": {
"reply_line": "{date}에 {from}님이 작성:",
"forwarded_separator": "---------- 전달된 메시지 ----------",
"from_label": "보낸 사람",
"date_label": "날짜",
"subject_label": "제목"
},
"login": {
"title": "웹메일",
"username_label": "이메일",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -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",
+7
View File
@@ -1,4 +1,11 @@
{
"quote_header": {
"reply_line": "{date}, {from} написал:",
"forwarded_separator": "---------- Пересланное сообщение ----------",
"from_label": "От",
"date_label": "Дата",
"subject_label": "Тема"
},
"login": {
"title": "Веб-почта",
"username_label": "Электронная почта",
+7
View File
@@ -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",
+7
View File
@@ -1,4 +1,11 @@
{
"quote_header": {
"reply_line": "{date}, {from} написав:",
"forwarded_separator": "---------- Переслане повідомлення ----------",
"from_label": "Від",
"date_label": "Дата",
"subject_label": "Тема"
},
"login": {
"title": "Веб-пошта",
"username_label": "Електронна пошта",
+7
View File
@@ -1,4 +1,11 @@
{
"quote_header": {
"reply_line": "在 {date}{from} 写道:",
"forwarded_separator": "---------- 转发邮件 ----------",
"from_label": "发件人",
"date_label": "日期",
"subject_label": "主题"
},
"login": {
"title": "网页邮箱",
"username_label": "邮箱地址",