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
@@ -0,0 +1,35 @@
exports.up = async function(knex) {
// Add language-specific columns to email_templates
await knex.schema.alterTable('email_templates', function(table) {
// Add English versions (rename existing columns for consistency)
table.renameColumn('subject', 'subject_en');
table.renameColumn('body_html', 'body_html_en');
table.renameColumn('body_text', 'body_text_en');
// Add German versions
table.string('subject_de');
table.text('body_html_de');
table.text('body_text_de');
});
// Copy existing values to German columns as defaults
await knex('email_templates').update({
subject_de: knex.raw('subject_en'),
body_html_de: knex.raw('body_html_en'),
body_text_de: knex.raw('body_text_en')
});
};
exports.down = async function(knex) {
await knex.schema.alterTable('email_templates', function(table) {
// Remove German columns
table.dropColumn('subject_de');
table.dropColumn('body_html_de');
table.dropColumn('body_text_de');
// Rename columns back
table.renameColumn('subject_en', 'subject');
table.renameColumn('body_html_en', 'body_html');
table.renameColumn('body_text_en', 'body_text');
});
};
@@ -0,0 +1,67 @@
exports.up = async function(knex) {
// Update gallery_created template with German content
await knex('email_templates')
.where('template_key', 'gallery_created')
.update({
subject_de: 'Ihre Fotogalerie ist bereit!',
body_html_de: `<h2>Galerie erfolgreich erstellt</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!</p>
<p><strong>Galerie-Details:</strong></p>
<ul>
<li>Veranstaltungsdatum: {{event_date}}</li>
<li>Galerie-Link: {{gallery_link}}</li>
<li>Passwort: {{gallery_password}}</li>
<li>Gültig bis: {{expiry_date}}</li>
</ul>
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit sie Fotos ansehen und herunterladen können.</p>`,
body_text_de: 'Galerie erfolgreich erstellt\n\nLiebe(r) {{host_name}},\n\nIhre Fotogalerie "{{event_name}}" wurde erfolgreich erstellt!'
});
// Update expiration_warning template with German content
await knex('email_templates')
.where('template_key', 'expiration_warning')
.update({
subject_de: 'Ihre Fotogalerie läuft bald ab',
body_html_de: `<h2>Galerie läuft bald ab</h2>
<p>Liebe(r) {{host_name}},</p>
<p>Ihre Fotogalerie "{{event_name}}" läuft in {{days_remaining}} Tagen ab.</p>
<p>Nach Ablauf wird die Galerie archiviert und ist für Gäste nicht mehr zugänglich.</p>
<p><a href="{{gallery_link}}">Galerie besuchen</a></p>`,
body_text_de: 'Galerie läuft bald ab\n\nLiebe(r) {{host_name}},\n\nIhre Fotogalerie "{{event_name}}" läuft in {{days_remaining}} Tagen ab.'
});
// Update gallery_expired template if it exists
await knex('email_templates')
.where('template_key', 'gallery_expired')
.update({
subject_de: 'Ihre Fotogalerie {{event_name}} ist abgelaufen',
body_html_de: `<h2>Galerie abgelaufen</h2>
<p>Ihre Fotogalerie für "{{event_name}}" ist abgelaufen und nicht mehr zugänglich.</p>
<p>Die Fotos wurden zur sicheren Aufbewahrung archiviert. Wenn Sie wieder Zugriff benötigen, wenden Sie sich bitte an den Administrator unter {{admin_email}}.</p>
<p>Vielen Dank für die Nutzung unseres Foto-Sharing-Services!</p>
<p>Mit freundlichen Grüßen,<br>Das Foto-Sharing-Team</p>`,
body_text_de: 'Ihre Fotogalerie für {{event_name}} ist abgelaufen und nicht mehr zugänglich.\n\nDie Fotos wurden archiviert. Bei Bedarf kontaktieren Sie bitte den Administrator unter {{admin_email}}.'
});
// Update archive_complete template if it exists
await knex('email_templates')
.where('template_key', 'archive_complete')
.update({
subject_de: 'Archivierung abgeschlossen: {{event_name}}',
body_html_de: `<h2>Archivierung abgeschlossen</h2>
<p>Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.</p>
<p>Archivgröße: {{archive_size}}</p>
<p>Das Archiv wird sicher aufbewahrt und kann bei Bedarf wiederhergestellt werden.</p>`,
body_text_de: 'Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.\n\nArchivgröße: {{archive_size}}'
});
};
exports.down = async function(knex) {
// Reset German fields to null
await knex('email_templates').update({
subject_de: null,
body_html_de: null,
body_text_de: null
});
};
@@ -0,0 +1,57 @@
exports.up = async function(knex) {
// Check if gallery_expired template exists
const galleryExpiredExists = await knex('email_templates')
.where('template_key', 'gallery_expired')
.first();
if (!galleryExpiredExists) {
await knex('email_templates').insert({
template_key: 'gallery_expired',
subject_en: 'Your {{event_name}} photo gallery has expired',
body_html_en: `<h2>Gallery Expired</h2>
<p>Your photo gallery for {{event_name}} has expired and is no longer accessible.</p>
<p>The photos have been archived for safekeeping. If you need access to them, please contact the event administrator at {{admin_email}}.</p>
<p>Thank you for using our photo sharing service!</p>
<p>Best regards,<br>The Photo Sharing Team</p>`,
body_text_en: 'Your photo gallery for {{event_name}} has expired and is no longer accessible.\n\nThe photos have been archived. Please contact the administrator at {{admin_email}} if you need access.',
subject_de: 'Ihre Fotogalerie {{event_name}} ist abgelaufen',
body_html_de: `<h2>Galerie abgelaufen</h2>
<p>Ihre Fotogalerie für "{{event_name}}" ist abgelaufen und nicht mehr zugänglich.</p>
<p>Die Fotos wurden zur sicheren Aufbewahrung archiviert. Wenn Sie wieder Zugriff benötigen, wenden Sie sich bitte an den Administrator unter {{admin_email}}.</p>
<p>Vielen Dank für die Nutzung unseres Foto-Sharing-Services!</p>
<p>Mit freundlichen Grüßen,<br>Das Foto-Sharing-Team</p>`,
body_text_de: 'Ihre Fotogalerie für {{event_name}} ist abgelaufen und nicht mehr zugänglich.\n\nDie Fotos wurden archiviert. Bei Bedarf kontaktieren Sie bitte den Administrator unter {{admin_email}}.',
variables: JSON.stringify(['event_name', 'admin_email'])
});
}
// Check if archive_complete template exists
const archiveCompleteExists = await knex('email_templates')
.where('template_key', 'archive_complete')
.first();
if (!archiveCompleteExists) {
await knex('email_templates').insert({
template_key: 'archive_complete',
subject_en: 'Archive Complete: {{event_name}}',
body_html_en: `<h2>Archive Complete</h2>
<p>The photo gallery "{{event_name}}" has been successfully archived.</p>
<p>Archive size: {{archive_size}}</p>
<p>The archive has been stored securely and can be restored if needed.</p>`,
body_text_en: 'The photo gallery "{{event_name}}" has been successfully archived.\n\nArchive size: {{archive_size}}',
subject_de: 'Archivierung abgeschlossen: {{event_name}}',
body_html_de: `<h2>Archivierung abgeschlossen</h2>
<p>Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.</p>
<p>Archivgröße: {{archive_size}}</p>
<p>Das Archiv wird sicher aufbewahrt und kann bei Bedarf wiederhergestellt werden.</p>`,
body_text_de: 'Die Fotogalerie "{{event_name}}" wurde erfolgreich archiviert.\n\nArchivgröße: {{archive_size}}',
variables: JSON.stringify(['event_name', 'archive_size'])
});
}
};
exports.down = async function(knex) {
await knex('email_templates')
.whereIn('template_key', ['gallery_expired', 'archive_complete'])
.del();
};