Add email notifications language localization feature

This commit is contained in:
Omar Abid 2025-05-13 19:45:08 +01:00
parent a0b5c2e88c
commit 2ab9bd3172
6 changed files with 142 additions and 118 deletions

View file

@ -275,22 +275,18 @@ if (Meteor.isServer) {
url: FlowRouter.url('sign-up'), url: FlowRouter.url('sign-up'),
}; };
const lang = author.getLanguage(); const lang = author.getLanguage();
/* // Use EmailLocalization utility to handle email in the proper language
if (process.env.MAIL_SERVICE !== '') { if (typeof EmailLocalization !== 'undefined') {
let transporter = nodemailer.createTransport({ EmailLocalization.sendEmail({
service: process.env.MAIL_SERVICE,
auth: {
user: process.env.MAIL_SERVICE_USER,
pass: process.env.MAIL_SERVICE_PASSWORD
},
})
let info = transporter.sendMail({
to: icode.email, to: icode.email,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-register-subject', params, lang), subject: 'email-invite-register-subject',
text: TAPi18n.__('email-invite-register-text', params, lang), text: 'email-invite-register-text',
}) params: params,
language: lang
});
} else { } else {
// Fallback if EmailLocalization is not available
Email.send({ Email.send({
to: icode.email, to: icode.email,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
@ -298,13 +294,6 @@ if (Meteor.isServer) {
text: TAPi18n.__('email-invite-register-text', params, lang), text: TAPi18n.__('email-invite-register-text', params, lang),
}); });
} }
*/
Email.send({
to: icode.email,
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-register-subject', params, lang),
text: TAPi18n.__('email-invite-register-text', params, lang),
});
} catch (e) { } catch (e) {
InvitationCodes.remove(_id); InvitationCodes.remove(_id);
throw new Meteor.Error('email-fail', e.message); throw new Meteor.Error('email-fail', e.message);

View file

@ -1618,9 +1618,7 @@ if (Meteor.isServer) {
subBoard.addMember(user._id); subBoard.addMember(user._id);
user.addInvite(subBoard._id); user.addInvite(subBoard._id);
} }
} } try {
try {
const fullName = const fullName =
inviter.profile !== undefined && inviter.profile !== undefined &&
inviter.profile.fullname !== undefined inviter.profile.fullname !== undefined
@ -1642,38 +1640,29 @@ if (Meteor.isServer) {
board: board.title, board: board.title,
url: board.absoluteUrl(), url: board.absoluteUrl(),
}; };
// Get the recipient user's language preference for the email
const lang = user.getLanguage(); const lang = user.getLanguage();
/* // Add code to send invitation with EmailLocalization
if (process.env.MAIL_SERVICE !== '') { if (typeof EmailLocalization !== 'undefined') {
let transporter = nodemailer.createTransport({ EmailLocalization.sendEmail({
service: process.env.MAIL_SERVICE, to: user.emails[0].address,
auth: {
user: process.env.MAIL_SERVICE_USER,
pass: process.env.MAIL_SERVICE_PASSWORD
},
})
let info = transporter.sendMail({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-subject', params, lang), subject: 'email-invite-subject',
text: TAPi18n.__('email-invite-text', params, lang), text: 'email-invite-text',
}) params: params,
language: lang,
userId: user._id
});
} else { } else {
// Fallback if EmailLocalization is not available
Email.send({ Email.send({
to: user.emails[0].address.toLowerCase(), to: user.emails[0].address,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-subject', params, lang), subject: TAPi18n.__('email-invite-subject', params, lang),
text: TAPi18n.__('email-invite-text', params, lang), text: TAPi18n.__('email-invite-text', params, lang),
}); });
} }
*/
Email.send({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-subject', params, lang),
text: TAPi18n.__('email-invite-text', params, lang),
});
} catch (e) { } catch (e) {
throw new Meteor.Error('email-fail', e.message); throw new Meteor.Error('email-fail', e.message);
} }

View file

@ -0,0 +1,58 @@
// emailLocalization.js
// Utility functions to handle email localization in Wekan
import { TAPi18n } from '/imports/i18n';
import { ReactiveCache } from '/imports/reactiveCache';
// Main object for email localization utilities
EmailLocalization = {
/**
* Send an email using the recipient's preferred language
* @param {Object} options - Standard email sending options plus language options
* @param {String} options.to - Recipient email address
* @param {String} options.from - Sender email address
* @param {String} options.subject - Email subject i18n key
* @param {String} options.text - Email text i18n key
* @param {Object} options.params - Parameters for i18n translation
* @param {String} options.language - Language code to use (if not provided, will try to detect)
* @param {String} options.userId - User ID to determine language (if not provided with language)
*/
sendEmail(options) {
// Determine the language to use
let lang = options.language;
// If no language is specified but we have a userId, try to get the user's language
if (!lang && options.userId) {
const user = ReactiveCache.getUser(options.userId);
if (user) {
lang = user.getLanguage();
}
}
// If no language could be determined, use the site default
if (!lang) {
lang = TAPi18n.getLanguage() || 'en';
}
// Translate subject and text using the determined language
const subject = TAPi18n.__(options.subject, options.params || {}, lang);
let text = options.text;
// If text is an i18n key, translate it
if (typeof text === 'string' && text.startsWith('email-')) {
text = TAPi18n.__(text, options.params || {}, lang);
}
// Send the email with translated content
return Email.send({
to: options.to,
from: options.from || Accounts.emailTemplates.from,
subject: subject,
text: text,
html: options.html
});
}
};
// Add module.exports to make it accessible from other files
module.exports = EmailLocalization;

4
server/lib/importer.js Normal file
View file

@ -0,0 +1,4 @@
// This file ensures the EmailLocalization utility is imported
// and available throughout the application
import './emailLocalization';

View file

@ -2,6 +2,8 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
//var nodemailer = require('nodemailer'); //var nodemailer = require('nodemailer');
import EmailLocalization from '../lib/emailLocalization';
// buffer each user's email text in a queue, then flush them in single email // buffer each user's email text in a queue, then flush them in single email
Meteor.startup(() => { Meteor.startup(() => {
Notifications.subscribe('email', (user, title, description, params) => { Notifications.subscribe('email', (user, title, description, params) => {
@ -14,6 +16,7 @@ Meteor.startup(() => {
quoteParams[key] = quoteParams[key] ? `${params[key]}` : ''; quoteParams[key] = quoteParams[key] ? `${params[key]}` : '';
}); });
// Get user's preferred language
const lan = user.getLanguage(); const lan = user.getLanguage();
const subject = TAPi18n.__(title, params, lan); // the original function has a fault, i believe the title should be used according to original author const subject = TAPi18n.__(title, params, lan); // the original function has a fault, i believe the title should be used according to original author
const existing = user.getEmailBuffer().length > 0; const existing = user.getEmailBuffer().length > 0;
@ -42,35 +45,14 @@ Meteor.startup(() => {
const html = texts.join('<br/>\n\n'); const html = texts.join('<br/>\n\n');
user.clearEmailBuffer(); user.clearEmailBuffer();
try { try {
/* // Use EmailLocalization utility to ensure the correct language is used
if (process.env.MAIL_SERVICE !== '') { EmailLocalization.sendEmail({
let transporter = nodemailer.createTransport({
service: process.env.MAIL_SERVICE,
auth: {
user: process.env.MAIL_SERVICE_USER,
pass: process.env.MAIL_SERVICE_PASSWORD
},
})
let info = transporter.sendMail({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from,
subject,
html,
})
} else {
Email.send({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from,
subject,
html,
});
}
*/
Email.send({
to: user.emails[0].address.toLowerCase(), to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject, subject,
html, html,
language: user.getLanguage(),
userId: user._id
}); });
} catch (e) { } catch (e) {
return; return;

View file

@ -125,22 +125,31 @@ RulesHelper = {
const text = action.emailMsg || ''; const text = action.emailMsg || '';
const subject = action.emailSubject || ''; const subject = action.emailSubject || '';
try { try {
/* // Try to detect the recipient's language preference if it's a Wekan user
if (process.env.MAIL_SERVICE !== '') { // Otherwise, use the default language for the rule-triggered emails
let transporter = nodemailer.createTransport({ let recipientUser = null;
service: process.env.MAIL_SERVICE, let recipientLang = TAPi18n.getLanguage() || 'en';
auth: {
user: process.env.MAIL_SERVICE_USER, // Check if recipient is a Wekan user to get their language
pass: process.env.MAIL_SERVICE_PASSWORD if (to && to.includes('@')) {
}, recipientUser = ReactiveCache.getUser({ 'emails.address': to.toLowerCase() });
}) if (recipientUser && typeof recipientUser.getLanguage === 'function') {
let info = transporter.sendMail({ recipientLang = recipientUser.getLanguage();
}
}
// Use EmailLocalization if available
if (typeof EmailLocalization !== 'undefined') {
EmailLocalization.sendEmail({
to, to,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject, subject,
text, text,
}) language: recipientLang,
userId: recipientUser ? recipientUser._id : null
});
} else { } else {
// Fallback to standard Email.send
Email.send({ Email.send({
to, to,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
@ -148,13 +157,6 @@ RulesHelper = {
text, text,
}); });
} }
*/
Email.send({
to,
from: Accounts.emailTemplates.from,
subject,
text,
});
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(e); console.error(e);