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:
@@ -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
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').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
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"quote_header": {
|
||||
"reply_line": "{date}に{from}が書きました:",
|
||||
"forwarded_separator": "---------- 転送メッセージ ----------",
|
||||
"from_label": "差出人",
|
||||
"date_label": "日付",
|
||||
"subject_label": "件名"
|
||||
},
|
||||
"login": {
|
||||
"title": "ウェブメール",
|
||||
"username_label": "メールアドレス",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"quote_header": {
|
||||
"reply_line": "{date}에 {from}님이 작성:",
|
||||
"forwarded_separator": "---------- 전달된 메시지 ----------",
|
||||
"from_label": "보낸 사람",
|
||||
"date_label": "날짜",
|
||||
"subject_label": "제목"
|
||||
},
|
||||
"login": {
|
||||
"title": "웹메일",
|
||||
"username_label": "이메일",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"quote_header": {
|
||||
"reply_line": "{date}, {from} написал:",
|
||||
"forwarded_separator": "---------- Пересланное сообщение ----------",
|
||||
"from_label": "От",
|
||||
"date_label": "Дата",
|
||||
"subject_label": "Тема"
|
||||
},
|
||||
"login": {
|
||||
"title": "Веб-почта",
|
||||
"username_label": "Электронная почта",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"quote_header": {
|
||||
"reply_line": "{date}, {from} написав:",
|
||||
"forwarded_separator": "---------- Переслане повідомлення ----------",
|
||||
"from_label": "Від",
|
||||
"date_label": "Дата",
|
||||
"subject_label": "Тема"
|
||||
},
|
||||
"login": {
|
||||
"title": "Веб-пошта",
|
||||
"username_label": "Електронна пошта",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"quote_header": {
|
||||
"reply_line": "在 {date},{from} 写道:",
|
||||
"forwarded_separator": "---------- 转发邮件 ----------",
|
||||
"from_label": "发件人",
|
||||
"date_label": "日期",
|
||||
"subject_label": "主题"
|
||||
},
|
||||
"login": {
|
||||
"title": "网页邮箱",
|
||||
"username_label": "邮箱地址",
|
||||
|
||||
Reference in New Issue
Block a user