remove date management by lang

This commit is contained in:
2026-09-30 15:08:37 +02:00
parent 0e3fe51b12
commit 151afd73a1
4 changed files with 108 additions and 10 deletions
+99 -3
View File
@@ -262,6 +262,98 @@ Notifications.filternoitif = (tribe, tpl, alias, channel = "push", topics = [])
return ok; return ok;
}; };
/**
* Language of a recipient: the language of its own app (persons.lg, stamped by
* Persons.managetopic on each topic subscription) wins over the language of
* whoever triggered the event, which is only a fallback. Anything outside
* fr/en is forced to "en", the same way checkHeaders forces xlang.
*/
Notifications.recipientlang = (person, fallback) =>
person && ["fr", "en"].includes(person.lg)
? person.lg
: ["fr", "en"].includes(fallback)
? fallback
: "en";
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
const localdate_re = /\{\{#localdate\}\}\s*([^\s}]+)\s*\{\{\/localdate\}\}/g;
/**
* A date is never formatted by the caller: it sends the raw value (ISO) and the
* template declares how to display it.
*
* seekertemplate_fr.js : tpl.dateformat = { bookdate: "DD/MM/YYYY" }
* template : ... le {{#localdate}}bookdate{{/localdate}} ...
*
* The text of the section is the NAME of the field to display, and it is not
* "{{bookdate}}": mustache hands the raw inner text of a lambda, it does not
* render it, so the value is read in the view (closure) and not through a
* render callback. tpl.dateformat gives the dayjs format of that field.
*
* Anything that is not an ISO date (missing, empty, ...) is returned untouched,
* which also prevents an "Invalid Date" from reaching a recipient.
*
* The value MUST be a factory (() => function (text)): mustache calls a function
* found in the data with no argument during the lookup
* (mustache.js Context.lookup), so a plain (text) => ... lambda is silently
* ignored and the section renders its own content.
*/
Notifications.localdate = (tpl, view) => () => (text) => {
const field = text.trim();
const raw = view ? view[field] : undefined;
const value = raw === undefined || raw === null ? "" : String(raw).trim();
const fmt = (tpl.dateformat || {})[field];
if (!fmt || !ISO_DATE_RE.test(value)) return value;
return dayjs(value).format(fmt);
};
/**
* Single rendering point for every notification template (email subject/html/
* text and push notiftitle/notifdescription).
*
* Notifications.render(tpl, view, ["subject", "html", "text"])
*
* `keys` is explicit on purpose: a template without tpl.text must keep failing
* instead of silently producing a mail without its text part. `meta` is only
* used to make the warnings actionable ({ template, lg }).
*
* Warns on both directions of a mismatch between tpl.dateformat and the
* template: a declared format never used means the raw ISO date is displayed,
* and a {{#localdate}} without declaration means the same thing silently.
*/
Notifications.render = (tpl, view, keys, meta = {}) => {
view.localdate = Notifications.localdate(tpl, view);
const rendered = {};
try {
for (const k of keys) rendered[k] = Mustache.render(tpl[k], view);
} finally {
// view is the caller object (req.body, emailsto entry, push recipient) and
// is persisted as is in persons.notifications[]: do not leak the factory.
delete view.localdate;
}
const sources = ["subject", "html", "text", "notiftitle", "notifdescription"]
.map((k) => (typeof tpl[k] === "string" ? tpl[k] : ""))
.join("\n");
const used = new Set([...sources.matchAll(localdate_re)].map((m) => m[1]));
const declared = Object.keys(tpl.dateformat || {});
const where = `${meta.template || "?"}_${meta.lg || "?"}`;
for (const field of declared) {
if (!used.has(field))
console.warn(
currentmod,
`dateformat "${field}" declared but never used in ${where}`
);
}
for (const field of used) {
if (!declared.includes(field))
console.warn(
currentmod,
`{{#localdate}}${field}} used without dateformat in ${where}, raw value displayed`
);
}
return rendered;
};
Notifications.manageemail = async ( Notifications.manageemail = async (
data, data,
templatename, templatename,
@@ -360,9 +452,13 @@ Notifications.manageemail = async (
emailtosend.to = emailtosend.to.slice(0, -1); emailtosend.to = emailtosend.to.slice(0, -1);
emailtosend.templatename = templatename; emailtosend.templatename = templatename;
emailtosend.lg = lg; emailtosend.lg = lg;
emailtosend.subject = Mustache.render(tplemail.subject, email); Object.assign(
emailtosend.html = Mustache.render(tplemail.html, email); emailtosend,
emailtosend.text = Mustache.render(tplemail.text, email); Notifications.render(tplemail, email, ["subject", "html", "text"], {
template: templatename,
lg,
})
);
if (email.cc) { if (email.cc) {
emailtosend.cc += emailtosend.cc && emailtosend.cc != "" ? "," : ""; emailtosend.cc += emailtosend.cc && emailtosend.cc != "" ? "," : "";
emailtosend.cc += email.cc.join(","); emailtosend.cc += email.cc.join(",");
+4 -3
View File
@@ -264,9 +264,10 @@ Pagans.sendmailkey = (data) => {
const maildata = { const maildata = {
from: tplemail.sender, from: tplemail.sender,
to: person.email, to: person.email,
subject: Mustache.render(tplemail.subject, person), ...Notifications.render(tplemail, person, ["subject", "html", "text"], {
html: Mustache.render(tplemail.html, person), template: "createidentity",
text: Mustache.render(tplemail.text, person), lg: data.lg,
}),
attachments: [ attachments: [
{ {
filename: `${person.alias}_privatekey.txt`, filename: `${person.alias}_privatekey.txt`,
+4 -3
View File
@@ -165,9 +165,10 @@ Pagans.sendmailkey = (data) => {
const maildata = { const maildata = {
from: tplemail.sender, from: tplemail.sender,
to: person.email, to: person.email,
subject: Mustache.render(tplemail.subject, person), ...Notifications.render(tplemail, person, ["subject", "html", "text"], {
html: Mustache.render(tplemail.html, person), template: "createidentity",
text: Mustache.render(tplemail.text, person), lg: data.lg,
}),
attachments: [ attachments: [
{ {
filename: `${person.alias}_privatekey.txt`, filename: `${person.alias}_privatekey.txt`,
+1 -1
View File
@@ -284,7 +284,7 @@ router.get("/checkemail/:email/:digit6", checkHeaders, async (req, res) => {
}, },
"checkemail", "checkemail",
"adminapi", "adminapi",
req.session.header.xlang || "fr", Notifications.recipientlang(null, req.session.header.xlang),
1, 1,
["anonymous"] ["anonymous"]
); );