Files
SRCmail/lib/quote-header.ts
T
dealerweb 05e2837f6b 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:").
2026-05-30 14:09:23 +02:00

117 lines
4.3 KiB
TypeScript

// Builds the default reply/forward quote header and runs it through the
// emailHooks.onBuildQuoteHeader transform so plugins can replace it (e.g.
// with an Outlook-style From/Sent/To/Cc/Subject block).
//
// This module is the single source of truth for the default header strings -
// the composer keeps the same defaults inline as a fallback, but production
// flow goes through here.
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: {
from?: { email?: string; name?: string }[];
to?: { email?: string; name?: string }[];
cc?: { email?: string; name?: string }[];
subject?: string;
receivedAt?: string;
};
newTo: string[];
newCc: string[];
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",
year: "numeric",
month: "short",
day: "numeric",
})
: "";
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 = `${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 replyLine = labels.formatReplyLine({ date, from: fromStr });
const text = `${replyLine}\n`;
const html = `<div>${replyLine}<br></div>`;
return { html, text, wrapInBlockquote: true };
}
export async function buildQuoteHeader(args: BuildArgs): Promise<QuoteHeader> {
const def = defaultHeader(args);
const ctx: QuoteHeaderContext = {
mode: args.mode,
newTo: args.newTo,
newCc: args.newCc,
from: args.email.from?.[0]?.email
? { name: args.email.from[0].name, email: args.email.from[0].email }
: null,
to: (args.email.to ?? [])
.filter((r): r is { email: string; name?: string } => !!r.email)
.map((r) => ({ name: r.name, email: r.email })),
cc: (args.email.cc ?? [])
.filter((r): r is { email: string; name?: string } => !!r.email)
.map((r) => ({ name: r.name, email: r.email })),
subject: args.email.subject ?? "",
date: args.email.receivedAt
? formatDateTime(args.email.receivedAt, args.timeFormat, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})
: "",
receivedAt: args.email.receivedAt,
locale: args.locale,
};
return emailHooks.onBuildQuoteHeader.transform<QuoteHeader>(def, ctx);
}