Files
alrahma_sunday_school_api/apps/api/dist/services/notificationLocalizationService.js
T
2026-06-11 03:22:12 -04:00

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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