feat: multilingual email templates with translations table

Replace column-based email template languages (subject_en/subject_de) with
a normalized email_template_translations table where each language is a row.
This allows adding new languages without schema changes.

- Add migration 075 to create email_template_translations table, migrate
  existing EN/DE data, and seed NL/PT/RU for customer-facing templates
- Update processTemplate() to query translations table with fallback chain
  (requested lang -> en -> first available), with legacy column fallback
- Restructure admin email API to return/accept translations object format
- Update frontend EmailConfigPage with dynamic 5-language tabs, translation
  count badges, and copy-from-language feature for empty translations
- Add Dutch to default language dropdown in general settings
- Add Dutch to clientAccessI18n and password security messages in emails
- Expand email domain detection for NL/BE/BR/PT/RU domains
- Add email i18n keys (copyFrom, noTranslation, etc.) across all 5 locales
This commit is contained in:
Paul Nothaft
2026-04-04 17:43:01 +02:00
parent e32da68cbd
commit f50d7c0c51
11 changed files with 687 additions and 255 deletions
+11 -13
View File
@@ -11,19 +11,17 @@ export interface EmailConfig {
tls_reject_unauthorized: boolean;
}
export interface EmailTemplateTranslation {
subject: string;
body_html: string;
body_text?: string;
}
export interface EmailTemplate {
id: number;
template_key: string;
subject: string; // For backward compatibility
body_html: string; // For backward compatibility
body_text?: string; // For backward compatibility
subject_en: string;
subject_de: string;
body_html_en: string;
body_html_de: string;
body_text_en?: string;
body_text_de?: string;
variables: string[];
translations: Record<string, EmailTemplateTranslation>;
updated_at: string;
}
@@ -63,16 +61,16 @@ export const emailService = {
},
// Update email template
async updateTemplate(key: string, template: Partial<EmailTemplate>): Promise<void> {
await api.put(`/admin/email/templates/${key}`, template);
async updateTemplate(key: string, data: { translations: Record<string, EmailTemplateTranslation> }): Promise<void> {
await api.put(`/admin/email/templates/${key}`, data);
},
// Preview email template
async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> {
async previewTemplate(key: string, previewData: Record<string, string>, language: string = 'en'): Promise<EmailPreview> {
const response = await api.post<EmailPreview>(
`/admin/email/templates/${key}/preview`,
{ preview_data: previewData, language }
);
return response.data;
}
};
};