feat: Ability to overwrite locale on contact level with locale key on data
This commit is contained in:
@@ -214,11 +214,21 @@ export class Contacts {
|
|||||||
// Fetch project to get language preference
|
// Fetch project to get language preference
|
||||||
const project = await ContactService.getProjectByContactId(contactId);
|
const project = await ContactService.getProjectByContactId(contactId);
|
||||||
|
|
||||||
|
// Get contact-level locale (overrides project language)
|
||||||
|
const contactLocale =
|
||||||
|
contact.data &&
|
||||||
|
typeof contact.data === 'object' &&
|
||||||
|
!Array.isArray(contact.data) &&
|
||||||
|
'locale' in contact.data &&
|
||||||
|
typeof contact.data.locale === 'string'
|
||||||
|
? contact.data.locale
|
||||||
|
: null;
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
email: contact.email,
|
email: contact.email,
|
||||||
subscribed: contact.subscribed,
|
subscribed: contact.subscribed,
|
||||||
language: project?.language || 'en',
|
language: contactLocale || project?.language || 'en',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {type Contact, Prisma} from '@plunk/db';
|
import {type Contact, Prisma} from '@plunk/db';
|
||||||
|
import {isValidLanguageCode} from '@plunk/shared';
|
||||||
import type {FilterCondition, FilterGroup} from '@plunk/types';
|
import type {FilterCondition, FilterGroup} from '@plunk/types';
|
||||||
|
|
||||||
import {prisma} from '../database/prisma.js';
|
import {prisma} from '../database/prisma.js';
|
||||||
@@ -229,6 +230,20 @@ export class ContactService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate locale field
|
||||||
|
if (key === 'locale') {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
if (!isValidLanguageCode(value)) {
|
||||||
|
throw new HttpException(
|
||||||
|
400,
|
||||||
|
`Invalid locale code: ${value}. Must be one of: en, nl, fr, hi, de`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (value !== null && value !== undefined) {
|
||||||
|
throw new HttpException(400, 'Locale must be a string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle non-persistent data format: { value: "...", persistent: false }
|
// Handle non-persistent data format: { value: "...", persistent: false }
|
||||||
if (
|
if (
|
||||||
typeof value === 'object' &&
|
typeof value === 'object' &&
|
||||||
@@ -297,6 +312,12 @@ export class ContactService {
|
|||||||
Object.assign(mergedData, contact.data);
|
Object.assign(mergedData, contact.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly expose locale as a predefined field (available in templates)
|
||||||
|
// This ensures locale is always accessible even if not in contact.data
|
||||||
|
if (mergedData.locale === undefined) {
|
||||||
|
mergedData.locale = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Add temporary (non-persistent) data
|
// Add temporary (non-persistent) data
|
||||||
if (temporaryData) {
|
if (temporaryData) {
|
||||||
for (const [key, value] of Object.entries(temporaryData)) {
|
for (const [key, value] of Object.entries(temporaryData)) {
|
||||||
|
|||||||
@@ -604,8 +604,18 @@ export class EmailService {
|
|||||||
|
|
||||||
const unsubscribeHtml = includeUnsubscribe
|
const unsubscribeHtml = includeUnsubscribe
|
||||||
? (() => {
|
? (() => {
|
||||||
// Get translator for project's language
|
// Get contact-level locale (overrides project language)
|
||||||
const translator = createTranslatorSync(project.language || 'en');
|
const contactLocale =
|
||||||
|
contact.data &&
|
||||||
|
typeof contact.data === 'object' &&
|
||||||
|
!Array.isArray(contact.data) &&
|
||||||
|
'locale' in contact.data &&
|
||||||
|
typeof contact.data.locale === 'string'
|
||||||
|
? contact.data.locale
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Get translator for contact's locale or project's language
|
||||||
|
const translator = createTranslatorSync(contactLocale || project.language || 'en');
|
||||||
const unsubscribeText = translator.t('email.footer.unsubscribeText', {
|
const unsubscribeText = translator.t('email.footer.unsubscribeText', {
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const VariableMention = Mention.configure({
|
|||||||
|
|
||||||
items: ({query}) => {
|
items: ({query}) => {
|
||||||
const safeVariables = Array.isArray(availableVariables) ? availableVariables : [];
|
const safeVariables = Array.isArray(availableVariables) ? availableVariables : [];
|
||||||
const allVariables = ['email', 'unsubscribeUrl', 'subscribeUrl', 'manageUrl', ...safeVariables];
|
const allVariables = ['email', 'unsubscribeUrl', 'subscribeUrl', 'manageUrl', 'locale', ...safeVariables];
|
||||||
const uniqueVariables = Array.from(new Set(allVariables)).filter(v => typeof v === 'string');
|
const uniqueVariables = Array.from(new Set(allVariables)).filter(v => typeof v === 'string');
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
|||||||
Reference in New Issue
Block a user