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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user