Implement multi-language email templates

- Add language columns to email_templates table (subject_en/de, body_html_en/de, body_text_en/de)
- Update adminEmail.js routes to support language-specific templates
- Create EmailProcessor service to handle language selection based on recipient
- Update EmailConfigPage component with language tabs similar to CMS pages
- Add German translations for all email templates
- Update all email queue usage to use proper template keys
- Add missing email templates (gallery_expired, archive_complete)
- Integrate email processor service into main server startup

The system now automatically selects the appropriate language (English/German) based on the recipient's email domain or preferences.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-08 17:57:26 +02:00
parent 1bc9b547c7
commit 69b56ed582
13 changed files with 564 additions and 102 deletions
+58 -13
View File
@@ -88,6 +88,7 @@ export const EmailConfigPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'smtp' | 'templates'>('smtp');
const [selectedTemplateKey, setSelectedTemplateKey] = useState<string>('gallery_created');
const [editedTemplate, setEditedTemplate] = useState<Partial<EmailTemplate>>({});
const [editingLang, setEditingLang] = useState<'en' | 'de'>('en');
const [showPassword, setShowPassword] = useState(false);
const [testEmail, setTestEmail] = useState('');
const [showPreview, setShowPreview] = useState(false);
@@ -203,13 +204,19 @@ export const EmailConfigPage: React.FC = () => {
const handleSaveTemplate = () => {
if (selectedTemplateKey && editedTemplate) {
const templateData: Partial<EmailTemplate> = {};
// Include both language versions
if (editedTemplate.subject_en !== undefined) templateData.subject_en = editedTemplate.subject_en;
if (editedTemplate.subject_de !== undefined) templateData.subject_de = editedTemplate.subject_de;
if (editedTemplate.body_html_en !== undefined) templateData.body_html_en = editedTemplate.body_html_en;
if (editedTemplate.body_html_de !== undefined) templateData.body_html_de = editedTemplate.body_html_de;
if (editedTemplate.body_text_en !== undefined) templateData.body_text_en = editedTemplate.body_text_en;
if (editedTemplate.body_text_de !== undefined) templateData.body_text_de = editedTemplate.body_text_de;
saveTemplateMutation.mutate({
key: selectedTemplateKey,
template: {
subject: editedTemplate.subject,
body_html: editedTemplate.body_html,
body_text: editedTemplate.body_text
}
template: templateData
});
}
};
@@ -231,7 +238,7 @@ export const EmailConfigPage: React.FC = () => {
};
try {
const preview = await emailService.previewTemplate(selectedTemplateKey, sampleData);
const preview = await emailService.previewTemplate(selectedTemplateKey, sampleData, editingLang);
setPreviewData({
subject: preview.subject,
htmlContent: preview.body_html,
@@ -506,7 +513,9 @@ export const EmailConfigPage: React.FC = () => {
<p className="font-medium text-neutral-900">
{templateInfo?.name || template.template_key}
</p>
<p className="text-sm text-neutral-500 mt-1 truncate">{template.subject}</p>
<p className="text-sm text-neutral-500 mt-1 truncate">
{template.subject_en || template.subject}
</p>
</button>
);
})}
@@ -518,6 +527,28 @@ export const EmailConfigPage: React.FC = () => {
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900">{t('email.editTemplate')}</h3>
<div className="flex gap-2">
<div className="flex gap-1 mr-4">
<button
onClick={() => setEditingLang('en')}
className={`px-3 py-1 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'en'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
}`}
>
🇬🇧 English
</button>
<button
onClick={() => setEditingLang('de')}
className={`px-3 py-1 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'de'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
}`}
>
🇩🇪 Deutsch
</button>
</div>
<Button
variant="outline"
size="sm"
@@ -553,23 +584,37 @@ export const EmailConfigPage: React.FC = () => {
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1">
{t('email.subjectLine')}
{t('email.subjectLine')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<Input
type="text"
value={editedTemplate.subject || ''}
onChange={(e) => setEditedTemplate(prev => ({ ...prev, subject: e.target.value }))}
value={
editingLang === 'en'
? (editedTemplate.subject_en || editedTemplate.subject || '')
: (editedTemplate.subject_de || '')
}
onChange={(e) => setEditedTemplate(prev => ({
...prev,
[editingLang === 'en' ? 'subject_en' : 'subject_de']: e.target.value
}))}
placeholder="Email subject"
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1">
{t('email.emailBody')}
{t('email.emailBody')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<textarea
value={editedTemplate.body_html || ''}
onChange={(e) => setEditedTemplate(prev => ({ ...prev, body_html: e.target.value }))}
value={
editingLang === 'en'
? (editedTemplate.body_html_en || editedTemplate.body_html || '')
: (editedTemplate.body_html_de || '')
}
onChange={(e) => setEditedTemplate(prev => ({
...prev,
[editingLang === 'en' ? 'body_html_en' : 'body_html_de']: e.target.value
}))}
rows={15}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono text-sm"
/>
+11 -5
View File
@@ -13,9 +13,15 @@ export interface EmailConfig {
export interface EmailTemplate {
id: number;
template_key: string;
subject: string;
body_html: string;
body_text?: 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[];
updated_at: string;
}
@@ -61,10 +67,10 @@ export const emailService = {
},
// Preview email template
async previewTemplate(key: string, previewData: Record<string, string>): Promise<EmailPreview> {
async previewTemplate(key: string, previewData: Record<string, string>, language: 'en' | 'de' = 'en'): Promise<EmailPreview> {
const response = await api.post<EmailPreview>(
`/api/admin/email/templates/${key}/preview`,
{ preview_data: previewData }
{ preview_data: previewData, language }
);
return response.data;
}