316 lines
12 KiB
JavaScript
316 lines
12 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.DEFAULT_NOTIFICATION_LOCALE = exports.SUPPORTED_NOTIFICATION_LOCALES = void 0;
|
|
exports.coerceNotificationLocale = coerceNotificationLocale;
|
|
exports.getNotificationTextDirection = getNotificationTextDirection;
|
|
exports.formatLocalizedDate = formatLocalizedDate;
|
|
exports.formatLocalizedCurrency = formatLocalizedCurrency;
|
|
exports.localizeSubscriptionStatus = localizeSubscriptionStatus;
|
|
exports.localizeInvoiceStatus = localizeInvoiceStatus;
|
|
exports.localizeBillingPeriod = localizeBillingPeriod;
|
|
exports.localizePlanName = localizePlanName;
|
|
exports.resolveNotificationLocale = resolveNotificationLocale;
|
|
exports.resolveNotificationTemplate = resolveNotificationTemplate;
|
|
exports.renderNotificationText = renderNotificationText;
|
|
exports.renderLocalizedEmailHtml = renderLocalizedEmailHtml;
|
|
const prisma_1 = require("../lib/prisma");
|
|
exports.SUPPORTED_NOTIFICATION_LOCALES = ['en', 'fr', 'ar'];
|
|
exports.DEFAULT_NOTIFICATION_LOCALE = 'en';
|
|
const localeConfig = {
|
|
en: { intl: 'en-US', direction: 'ltr' },
|
|
fr: { intl: 'fr-FR', direction: 'ltr' },
|
|
ar: { intl: 'ar-MA', direction: 'rtl' },
|
|
};
|
|
const subscriptionStatusLabels = {
|
|
en: {
|
|
trialing: 'Trial active',
|
|
active: 'Active',
|
|
payment_pending: 'Payment pending',
|
|
past_due: 'Payment overdue',
|
|
suspended: 'Suspended',
|
|
cancelled: 'Canceled',
|
|
canceled: 'Canceled',
|
|
expired: 'Expired',
|
|
},
|
|
fr: {
|
|
trialing: 'Essai actif',
|
|
active: 'Actif',
|
|
payment_pending: 'Paiement en attente',
|
|
past_due: 'Paiement en retard',
|
|
suspended: 'Suspendu',
|
|
cancelled: 'Annule',
|
|
canceled: 'Annule',
|
|
expired: 'Expire',
|
|
},
|
|
ar: {
|
|
trialing: 'الفترة التجريبية نشطة',
|
|
active: 'نشط',
|
|
payment_pending: 'الدفع قيد الانتظار',
|
|
past_due: 'الدفع متأخر',
|
|
suspended: 'معلّق',
|
|
cancelled: 'ملغى',
|
|
canceled: 'ملغى',
|
|
expired: 'منتهي',
|
|
},
|
|
};
|
|
const invoiceStatusLabels = {
|
|
en: {
|
|
draft: 'Preparing',
|
|
open: 'Due',
|
|
payment_pending: 'Payment processing',
|
|
paid: 'Paid',
|
|
partially_paid: 'Partially paid',
|
|
past_due: 'Past due',
|
|
void: 'Canceled',
|
|
uncollectible: 'Contact support',
|
|
refunded: 'Refunded',
|
|
partially_refunded: 'Partially refunded',
|
|
},
|
|
fr: {
|
|
draft: 'En preparation',
|
|
open: 'A payer',
|
|
payment_pending: 'Paiement en cours',
|
|
paid: 'Payee',
|
|
partially_paid: 'Partiellement payee',
|
|
past_due: 'En retard',
|
|
void: 'Annulee',
|
|
uncollectible: 'Contacter le support',
|
|
refunded: 'Remboursee',
|
|
partially_refunded: 'Partiellement remboursee',
|
|
},
|
|
ar: {
|
|
draft: 'قيد الإعداد',
|
|
open: 'مستحقة الدفع',
|
|
payment_pending: 'الدفع قيد المعالجة',
|
|
paid: 'مدفوعة',
|
|
partially_paid: 'مدفوعة جزئياً',
|
|
past_due: 'متأخرة',
|
|
void: 'ملغاة',
|
|
uncollectible: 'تواصل مع الدعم',
|
|
refunded: 'مستردة',
|
|
partially_refunded: 'مستردة جزئياً',
|
|
},
|
|
};
|
|
const billingPeriodLabels = {
|
|
en: {
|
|
monthly: 'monthly',
|
|
annual: 'annual',
|
|
},
|
|
fr: {
|
|
monthly: 'mensuel',
|
|
annual: 'annuel',
|
|
},
|
|
ar: {
|
|
monthly: 'شهري',
|
|
annual: 'سنوي',
|
|
},
|
|
};
|
|
const planLabels = {
|
|
en: {
|
|
starter: 'Starter',
|
|
growth: 'Growth',
|
|
pro: 'Pro',
|
|
},
|
|
fr: {
|
|
starter: 'Starter',
|
|
growth: 'Growth',
|
|
pro: 'Pro',
|
|
},
|
|
ar: {
|
|
starter: 'Starter',
|
|
growth: 'Growth',
|
|
pro: 'Pro',
|
|
},
|
|
};
|
|
function coerceNotificationLocale(locale) {
|
|
if (locale && exports.SUPPORTED_NOTIFICATION_LOCALES.includes(locale)) {
|
|
return locale;
|
|
}
|
|
return exports.DEFAULT_NOTIFICATION_LOCALE;
|
|
}
|
|
function getNotificationTextDirection(locale) {
|
|
return localeConfig[locale].direction;
|
|
}
|
|
function formatLocalizedDate(value, locale, options) {
|
|
const date = value instanceof Date ? value : new Date(value);
|
|
return new Intl.DateTimeFormat(localeConfig[locale].intl, options ?? {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
}).format(date);
|
|
}
|
|
function formatLocalizedCurrency(amountMinor, currency, locale) {
|
|
return new Intl.NumberFormat(localeConfig[locale].intl, {
|
|
style: 'currency',
|
|
currency,
|
|
}).format(amountMinor / 100);
|
|
}
|
|
function localizeSubscriptionStatus(status, locale) {
|
|
const normalized = status.toLowerCase();
|
|
return subscriptionStatusLabels[locale][normalized] ?? status;
|
|
}
|
|
function localizeInvoiceStatus(status, locale) {
|
|
const normalized = status.toLowerCase();
|
|
return invoiceStatusLabels[locale][normalized] ?? status;
|
|
}
|
|
function localizeBillingPeriod(period, locale) {
|
|
const normalized = period.toLowerCase();
|
|
return billingPeriodLabels[locale][normalized] ?? period;
|
|
}
|
|
function localizePlanName(plan, locale) {
|
|
const normalized = plan.toLowerCase();
|
|
return planLabels[locale][normalized] ?? plan;
|
|
}
|
|
function isTemplateDateValue(value) {
|
|
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'date';
|
|
}
|
|
function isTemplateCurrencyValue(value) {
|
|
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'currency';
|
|
}
|
|
function isTemplateSubscriptionStatusValue(value) {
|
|
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'subscription_status';
|
|
}
|
|
function isTemplateInvoiceStatusValue(value) {
|
|
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'invoice_status';
|
|
}
|
|
function formatTemplateValue(value, locale) {
|
|
if (value instanceof Date)
|
|
return formatLocalizedDate(value, locale);
|
|
if (isTemplateDateValue(value))
|
|
return formatLocalizedDate(value.value, locale, value.options);
|
|
if (isTemplateCurrencyValue(value))
|
|
return formatLocalizedCurrency(value.amountMinor, value.currency, locale);
|
|
if (isTemplateSubscriptionStatusValue(value))
|
|
return localizeSubscriptionStatus(value.status, locale);
|
|
if (isTemplateInvoiceStatusValue(value))
|
|
return localizeInvoiceStatus(value.status, locale);
|
|
if (value === null || value === undefined)
|
|
return '';
|
|
return String(value);
|
|
}
|
|
function normalizeTemplateVariableList(value) {
|
|
if (!Array.isArray(value))
|
|
return [];
|
|
return value.filter((item) => typeof item === 'string');
|
|
}
|
|
function escapeHtml(value) {
|
|
return value
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
async function findTemplate(db, templateKey, channel, locale) {
|
|
return db.notificationTemplate.findFirst({
|
|
where: { templateKey, channel, locale, isActive: true },
|
|
orderBy: { version: 'desc' },
|
|
select: {
|
|
templateKey: true,
|
|
channel: true,
|
|
locale: true,
|
|
subject: true,
|
|
body: true,
|
|
requiredVariables: true,
|
|
optionalVariables: true,
|
|
version: true,
|
|
},
|
|
});
|
|
}
|
|
async function resolveNotificationLocale(input, db = prisma_1.prisma) {
|
|
if (input.locale)
|
|
return coerceNotificationLocale(input.locale);
|
|
const employeePromise = input.employeeId
|
|
? db.employee.findUnique({
|
|
where: { id: input.employeeId },
|
|
select: { preferredLanguage: true },
|
|
})
|
|
: Promise.resolve(null);
|
|
const renterPromise = input.renterId
|
|
? db.renter.findUnique({
|
|
where: { id: input.renterId },
|
|
select: { preferredLocale: true },
|
|
})
|
|
: Promise.resolve(null);
|
|
const workspacePromise = input.companyId
|
|
? db.brandSettings.findUnique({
|
|
where: { companyId: input.companyId },
|
|
select: { defaultLocale: true },
|
|
})
|
|
: Promise.resolve(null);
|
|
const billingPromise = input.billingAccountId
|
|
? db.billingAccount.findUnique({
|
|
where: { id: input.billingAccountId },
|
|
select: { preferredLanguage: true },
|
|
})
|
|
: input.companyId
|
|
? db.billingAccount.findFirst({
|
|
where: { companyId: input.companyId, isPrimary: true },
|
|
select: { preferredLanguage: true },
|
|
orderBy: { createdAt: 'asc' },
|
|
})
|
|
: Promise.resolve(null);
|
|
const [employee, renter, workspace, billingAccount] = await Promise.all([
|
|
employeePromise,
|
|
renterPromise,
|
|
workspacePromise,
|
|
billingPromise,
|
|
]);
|
|
return coerceNotificationLocale(employee?.preferredLanguage ??
|
|
renter?.preferredLocale ??
|
|
workspace?.defaultLocale ??
|
|
billingAccount?.preferredLanguage ??
|
|
exports.DEFAULT_NOTIFICATION_LOCALE);
|
|
}
|
|
async function resolveNotificationTemplate(input, db = prisma_1.prisma) {
|
|
const requested = await findTemplate(db, input.templateKey, input.channel, input.locale);
|
|
if (requested) {
|
|
return {
|
|
locale: coerceNotificationLocale(requested.locale),
|
|
subject: renderNotificationText(requested.subject, input.variables, normalizeTemplateVariableList(requested.requiredVariables), input.locale),
|
|
body: renderNotificationText(requested.body, input.variables, normalizeTemplateVariableList(requested.requiredVariables), input.locale),
|
|
templateKey: requested.templateKey,
|
|
usedFallback: false,
|
|
};
|
|
}
|
|
if (input.locale !== exports.DEFAULT_NOTIFICATION_LOCALE) {
|
|
console.warn(`[Notifications] Missing ${input.locale} template for ${input.templateKey}/${input.channel}; falling back to ${exports.DEFAULT_NOTIFICATION_LOCALE}.`);
|
|
}
|
|
const fallback = await findTemplate(db, input.templateKey, input.channel, exports.DEFAULT_NOTIFICATION_LOCALE);
|
|
if (!fallback) {
|
|
throw new Error(`Missing notification template for ${input.templateKey}/${input.channel} in ${input.locale} and ${exports.DEFAULT_NOTIFICATION_LOCALE}`);
|
|
}
|
|
return {
|
|
locale: exports.DEFAULT_NOTIFICATION_LOCALE,
|
|
subject: renderNotificationText(fallback.subject, input.variables, normalizeTemplateVariableList(fallback.requiredVariables), exports.DEFAULT_NOTIFICATION_LOCALE),
|
|
body: renderNotificationText(fallback.body, input.variables, normalizeTemplateVariableList(fallback.requiredVariables), exports.DEFAULT_NOTIFICATION_LOCALE),
|
|
templateKey: fallback.templateKey,
|
|
usedFallback: true,
|
|
};
|
|
}
|
|
function renderNotificationText(template, variables = {}, requiredVariables = [], locale) {
|
|
if (!template)
|
|
return null;
|
|
for (const variableName of requiredVariables) {
|
|
if (variables[variableName] === undefined || variables[variableName] === null) {
|
|
throw new Error(`Missing notification template variable: ${variableName}`);
|
|
}
|
|
}
|
|
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, variableName) => formatTemplateValue(variables[variableName], locale));
|
|
}
|
|
function renderLocalizedEmailHtml(body, locale) {
|
|
const direction = getNotificationTextDirection(locale);
|
|
const textAlign = direction === 'rtl' ? 'right' : 'left';
|
|
const paragraphs = body
|
|
.split(/\n{2,}/)
|
|
.map((paragraph) => `<p style="margin:0 0 16px;text-align:${textAlign};">${escapeHtml(paragraph).replace(/\n/g, '<br />')}</p>`)
|
|
.join('');
|
|
return [
|
|
`<html lang="${locale}" dir="${direction}">`,
|
|
`<body style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:32px;color:#1c1917;direction:${direction};text-align:${textAlign};">`,
|
|
paragraphs,
|
|
'</body>',
|
|
'</html>',
|
|
].join('');
|
|
}
|
|
//# sourceMappingURL=notificationLocalizationService.js.map
|