Files
wekan/server/notifications/email.js
T
Lauri OjansivuandClaude Opus 4.8 22f996903b fix: notification emails ignored the user's language and were sent in English
On the server only the default (English) translation bundle is loaded at
startup. Translating a notification to a user's language called
TAPi18n.__(key, params, lang) with lng=lang, but that resource bundle was
never added on the server, so i18next fell back to English — every
notification email went out in English regardless of the user's profile
language. (The client works because switching language calls setLanguage,
which loads the bundle.)

Add TAPi18n.ensureLanguageLoaded(language), which loads the bundle on
demand if i18next does not already have it, and await it before translating
in the email notification, EmailLocalization.sendEmail and outgoing webhook
paths.

Closes #5875

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:37:56 +03:00

84 lines
3.0 KiB
JavaScript

import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
//var nodemailer = require('nodemailer');
import EmailLocalization from '../lib/emailLocalization';
import { Notifications } from '/server/notifications/notifications';
// buffer each user's email text in a queue, then flush them in single email
Meteor.startup(() => {
Notifications.subscribe('email', async (user, title, description, params) => {
try {
if (
!user ||
typeof user.getLanguage !== 'function' ||
typeof user.getEmailBuffer !== 'function' ||
typeof user.addEmailBuffer !== 'function'
) {
console.error('Invalid user helper surface for email notification:', user?._id);
return;
}
// #5875: the server only loads the English bundle at startup, so make sure
// the recipient's language is loaded before translating the subject/body —
// otherwise notification emails silently fall back to English.
await TAPi18n.ensureLanguageLoaded(user.getLanguage());
// add quote to make titles easier to read in email text
const quoteParams = { ...params };
['card', 'list', 'oldList', 'board', 'comment'].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
});
['timeValue', 'timeOldValue'].forEach(key => {
quoteParams[key] = quoteParams[key] ? `${params[key]}` : '';
});
const lan = user.getLanguage();
const subject = TAPi18n.__(title, params, lan);
const existing = user.getEmailBuffer().length > 0;
const htmlEnabled =
Meteor.settings.public &&
Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false;
const actorName = params.user || '';
const text = `${existing ? `\n${subject}\n` : ''}${
actorName
} ${TAPi18n.__(description, quoteParams, lan)}\n${params.url}`;
user.addEmailBuffer(htmlEnabled ? text.replace(/\n/g, '<br/>') : text);
const userId = user._id;
Meteor.setTimeout(async () => {
const user = await ReactiveCache.getUser(userId);
if (
!user ||
typeof user.getEmailBuffer !== 'function' ||
typeof user.clearEmailBuffer !== 'function' ||
typeof user.getLanguage !== 'function'
) {
return;
}
const texts = user.getEmailBuffer();
if (texts.length === 0) return;
const html = texts.join('<br/>\n\n');
user.clearEmailBuffer();
try {
await EmailLocalization.sendEmail({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from,
subject,
html,
language: user.getLanguage(),
userId: user._id
});
} catch (e) {
return;
}
}, process.env.EMAIL_NOTIFICATION_TIMEOUT || 30000);
} catch (error) {
console.error('Error preparing email notification:', error);
}
});
});