From 358f7ee99e2941ad179d5859178b35a80886542f Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 11 May 2026 20:42:28 +0200 Subject: [PATCH] feat(email-templates): seed missing nl/pt/ru/fr translations --- ...eed_missing_email_template_translations.js | 646 ++++++++++++++++++ 1 file changed, 646 insertions(+) create mode 100644 backend/migrations/core/099_seed_missing_email_template_translations.js diff --git a/backend/migrations/core/099_seed_missing_email_template_translations.js b/backend/migrations/core/099_seed_missing_email_template_translations.js new file mode 100644 index 00000000..c8631eee --- /dev/null +++ b/backend/migrations/core/099_seed_missing_email_template_translations.js @@ -0,0 +1,646 @@ +/** + * Migration: Auto-fill missing email-template translations for nl / pt / + * ru / fr. + * + * Coverage going in: + * - gallery_created / expiration_warning / gallery_expired / + * archive_complete already had en/de/nl/pt/ru from migration 075 + * → this migration adds the missing `fr` row. + * - All other templates (admin, customer, backup, restore, version) + * had en/de only → this migration adds nl/pt/ru/fr. + * + * The translations below were generated by an LLM and are flagged in + * the PR description as needing native-speaker review before the next + * stable release. en / de remain hand-translated and are untouched. + * + * Idempotent: every insert checks (template_id, language) for an + * existing row first and skips if present. Safe to re-run. + * + * Variable placeholders ({{name}}) are preserved verbatim across all + * locales so emailProcessor's safeTemplateReplace continues to wire + * them up unchanged. + */ + +const TRANSLATIONS = { + // ──────────────────────────────────────────────────────────────── + // Gallery delivery (core) — only fr is missing, the rest landed in + // migration 075. + // ──────────────────────────────────────────────────────────────── + gallery_created: { + fr: { + subject: 'Votre galerie photo est prête !', + body_html: `
Bonjour {{host_name}},
+Votre galerie photo « {{event_name}} » a été créée avec succès !
+Détails de la galerie :
+Partagez ce lien et le mot de passe avec vos invités pour qu'ils puissent voir et télécharger les photos.
+{{#if welcome_message}}{{welcome_message}}
{{/if}}`, + body_text: `Galerie créée avec succès\n\nBonjour {{host_name}},\n\nVotre galerie photo « {{event_name}} » a été créée avec succès !\n\nLien de la galerie : {{gallery_link}}\nMot de passe : {{gallery_password}}\nExpire le : {{expiry_date}}`, + }, + }, + + expiration_warning: { + fr: { + subject: 'Votre galerie photo expire bientôt', + body_html: `Bonjour {{host_name}},
+Votre galerie photo « {{event_name}} » expire dans {{days_remaining}} jours.
+Après l'expiration, la galerie sera archivée et ne sera plus accessible aux invités.
+`, + body_text: `La galerie expire bientôt\n\nBonjour {{host_name}},\n\nVotre galerie photo « {{event_name}} » expire dans {{days_remaining}} jours.\n\nGalerie : {{gallery_link}}`, + }, + }, + + gallery_expired: { + fr: { + subject: 'Galerie photo expirée et archivée', + body_html: `Bonjour {{host_name}},
+Votre galerie photo « {{event_name}} » a expiré et a été archivée.
+Les invités ne peuvent plus accéder à la galerie. Contactez votre photographe si vous avez besoin de restaurer l'accès.
`, + body_text: `Galerie archivée\n\nBonjour {{host_name}},\n\nVotre galerie photo « {{event_name}} » a expiré et a été archivée.`, + }, + }, + + archive_complete: { + fr: { + subject: 'Galerie archivée : {{event_name}}', + body_html: `La galerie photo « {{event_name}} » a été archivée.
+Détails de l'archive :
+U bent uitgenodigd door {{inviter_name}} om deel te nemen aan PicPeak als {{role_name}}.
+Klik op de onderstaande link om uw account in te stellen:
+ +Deze uitnodiging verloopt op {{expires_at}}.
+Als u deze e-mail niet verwachtte, kunt u deze gerust negeren.
`, + body_text: `Welkom bij PicPeak\n\nU bent uitgenodigd door {{inviter_name}} om deel te nemen aan PicPeak als {{role_name}}.\n\nStel uw account in: {{invitation_link}}\n\nDeze uitnodiging verloopt op {{expires_at}}.`, + }, + pt: { + subject: 'Você foi convidado para o PicPeak como {{role_name}}', + body_html: `Você foi convidado(a) por {{inviter_name}} para participar do PicPeak como {{role_name}}.
+Clique no link abaixo para configurar sua conta:
+ +Este convite expira em {{expires_at}}.
+Se você não esperava este e-mail, pode ignorá-lo com segurança.
`, + body_text: `Bem-vindo(a) ao PicPeak\n\nVocê foi convidado(a) por {{inviter_name}} para participar do PicPeak como {{role_name}}.\n\nConfigure sua conta: {{invitation_link}}\n\nEste convite expira em {{expires_at}}.`, + }, + ru: { + subject: 'Вас пригласили присоединиться к PicPeak в роли {{role_name}}', + body_html: `{{inviter_name}} пригласил(а) вас присоединиться к PicPeak в роли {{role_name}}.
+Перейдите по ссылке ниже, чтобы настроить учётную запись:
+ +Срок действия приглашения истекает {{expires_at}}.
+Если вы не ожидали этого письма, можете его проигнорировать.
`, + body_text: `Добро пожаловать в PicPeak\n\n{{inviter_name}} пригласил(а) вас присоединиться к PicPeak в роли {{role_name}}.\n\nНастроить учётную запись: {{invitation_link}}\n\nСрок действия приглашения истекает {{expires_at}}.`, + }, + fr: { + subject: 'Vous avez été invité(e) à rejoindre PicPeak en tant que {{role_name}}', + body_html: `{{inviter_name}} vous a invité(e) à rejoindre PicPeak en tant que {{role_name}}.
+Cliquez sur le lien ci-dessous pour configurer votre compte :
+ +Cette invitation expire le {{expires_at}}.
+Si vous n'attendiez pas cet e-mail, vous pouvez l'ignorer en toute sécurité.
`, + body_text: `Bienvenue sur PicPeak\n\n{{inviter_name}} vous a invité(e) à rejoindre PicPeak en tant que {{role_name}}.\n\nConfigurer le compte : {{invitation_link}}\n\nCette invitation expire le {{expires_at}}.`, + }, + }, + + admin_password_reset: { + nl: { + subject: 'Uw PicPeak-administratorwachtwoord is opnieuw ingesteld', + body_html: `Hallo {{admin_name}},
+Uw PicPeak-administratorwachtwoord is opnieuw ingesteld door {{reset_by}}.
+Klik op de onderstaande link om een nieuw wachtwoord in te stellen:
+ +Deze link verloopt op {{expires_at}}. Heeft u deze actie niet aangevraagd? Neem dan onmiddellijk contact op met uw teambeheerder.
`, + body_text: `Wachtwoord opnieuw ingesteld\n\nHallo {{admin_name}},\n\nUw PicPeak-administratorwachtwoord is opnieuw ingesteld door {{reset_by}}.\n\nStel een nieuw wachtwoord in: {{reset_link}}\n\nDeze link verloopt op {{expires_at}}.`, + }, + pt: { + subject: 'Sua senha de administrador do PicPeak foi redefinida', + body_html: `Olá {{admin_name}},
+Sua senha de administrador do PicPeak foi redefinida por {{reset_by}}.
+Clique no link abaixo para definir uma nova senha:
+ +Este link expira em {{expires_at}}. Se você não solicitou esta ação, entre em contato com o administrador da sua equipe imediatamente.
`, + body_text: `Senha redefinida\n\nOlá {{admin_name}},\n\nSua senha de administrador do PicPeak foi redefinida por {{reset_by}}.\n\nDefinir nova senha: {{reset_link}}\n\nEste link expira em {{expires_at}}.`, + }, + ru: { + subject: 'Ваш пароль администратора PicPeak был сброшен', + body_html: `Здравствуйте, {{admin_name}}!
+Ваш пароль администратора PicPeak был сброшен пользователем {{reset_by}}.
+Перейдите по ссылке ниже, чтобы задать новый пароль:
+ +Срок действия ссылки истекает {{expires_at}}. Если вы не запрашивали это действие, немедленно свяжитесь с администратором вашей команды.
`, + body_text: `Пароль сброшен\n\nЗдравствуйте, {{admin_name}}!\n\nВаш пароль администратора PicPeak был сброшен пользователем {{reset_by}}.\n\nЗадать новый пароль: {{reset_link}}\n\nСрок действия ссылки истекает {{expires_at}}.`, + }, + fr: { + subject: 'Votre mot de passe administrateur PicPeak a été réinitialisé', + body_html: `Bonjour {{admin_name}},
+Votre mot de passe administrateur PicPeak a été réinitialisé par {{reset_by}}.
+Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe :
+Définir un nouveau mot de passe
+Ce lien expire le {{expires_at}}. Si vous n'êtes pas à l'origine de cette demande, contactez immédiatement votre administrateur.
`, + body_text: `Mot de passe réinitialisé\n\nBonjour {{admin_name}},\n\nVotre mot de passe administrateur PicPeak a été réinitialisé par {{reset_by}}.\n\nDéfinir un nouveau mot de passe : {{reset_link}}\n\nCe lien expire le {{expires_at}}.`, + }, + }, + + // ──────────────────────────────────────────────────────────────── + // Customer portal (#354). customer_invitation already has hand-tuned + // en/de from migration 090 (themed button via migration 094). + // ──────────────────────────────────────────────────────────────── + customer_invitation: { + nl: { + subject: 'U bent uitgenodigd om uw fotogalerijen te bekijken', + body_html: `U bent uitgenodigd om een klantaccount aan te maken, zodat u al uw evenementgalerijen op één plek kunt bekijken — geen aparte links en wachtwoorden meer.
+ +Deze uitnodiging verloopt op {{expires_at}}. Werkt de link niet? Kopieer hem en plak hem in uw browser:
+{{invite_link}}
+Heeft u deze e-mail niet verwacht? U kunt deze gerust negeren.
`, + body_text: `Welkom bij uw fotogalerijen\n\nU bent uitgenodigd om een klantaccount aan te maken zodat u al uw galerijen op één plek kunt bekijken.\n\nAccount instellen: {{invite_link}}\n\nDeze uitnodiging verloopt op {{expires_at}}.`, + }, + pt: { + subject: 'Você foi convidado(a) a acessar suas galerias de fotos', + body_html: `Você foi convidado(a) a criar uma conta de cliente para visualizar todas as suas galerias de eventos em um só lugar — sem mais links e senhas separados.
+Este convite expira em {{expires_at}}. Se o link não funcionar, copie e cole-o no navegador:
+{{invite_link}}
+Se você não esperava este e-mail, pode ignorá-lo com segurança.
`, + body_text: `Bem-vindo(a) às suas galerias\n\nVocê foi convidado(a) a criar uma conta de cliente para acessar todas as suas galerias em um só lugar.\n\nConfigurar conta: {{invite_link}}\n\nEste convite expira em {{expires_at}}.`, + }, + ru: { + subject: 'Вас пригласили получить доступ к вашим фотогалереям', + body_html: `Вас пригласили создать учётную запись клиента, чтобы видеть все ваши галереи событий в одном месте — больше никаких отдельных ссылок и паролей.
+ +Срок действия приглашения истекает {{expires_at}}. Если ссылка не работает, скопируйте её в адресную строку браузера:
+{{invite_link}}
+Если вы не ожидали этого письма, можете его проигнорировать.
`, + body_text: `Добро пожаловать в ваши галереи\n\nВас пригласили создать учётную запись клиента, чтобы видеть все ваши галереи в одном месте.\n\nНастроить учётную запись: {{invite_link}}\n\nСрок действия приглашения истекает {{expires_at}}.`, + }, + fr: { + subject: 'Vous avez été invité(e) à accéder à vos galeries photo', + body_html: `Vous avez été invité(e) à créer un compte client pour voir toutes vos galeries d'événements en un seul endroit — plus de liens et mots de passe séparés.
+ +Cette invitation expire le {{expires_at}}. Si le lien ne fonctionne pas, copiez-le et collez-le dans votre navigateur :
+{{invite_link}}
+Si vous n'attendiez pas cet e-mail, vous pouvez l'ignorer en toute sécurité.
`, + body_text: `Bienvenue dans vos galeries photo\n\nVous avez été invité(e) à créer un compte client pour accéder à toutes vos galeries en un seul endroit.\n\nConfigurer le compte : {{invite_link}}\n\nCette invitation expire le {{expires_at}}.`, + }, + }, + + // ──────────────────────────────────────────────────────────────── + // Database backups + // ──────────────────────────────────────────────────────────────── + database_backup_completed: { + nl: { + subject: '[PicPeak] Database-back-up succesvol', + body_html: `De geplande database-back-up is succesvol voltooid.
+O backup agendado do banco de dados foi concluído com sucesso.
+Запланированное резервное копирование базы данных успешно завершено.
+La sauvegarde planifiée de la base de données s'est terminée avec succès.
+De geplande database-back-up is mislukt en moet handmatig worden onderzocht.
+Controleer de serverlogboeken voor meer details.
`, + body_text: `Database-back-up mislukt\n\nTijdstip: {{failed_at}}\nFoutmelding: {{error_message}}\n\nControleer de serverlogboeken voor meer details.`, + }, + pt: { + subject: '[PicPeak] FALHA no backup do banco de dados', + body_html: `O backup agendado do banco de dados falhou e precisa de investigação manual.
+Verifique os logs do servidor para mais detalhes.
`, + body_text: `Falha no backup do banco de dados\n\nHorário: {{failed_at}}\nErro: {{error_message}}\n\nVerifique os logs do servidor.`, + }, + ru: { + subject: '[PicPeak] ОШИБКА резервного копирования БД', + body_html: `Запланированное резервное копирование базы данных завершилось с ошибкой и требует ручной проверки.
+Проверьте журналы сервера для получения дополнительной информации.
`, + body_text: `Ошибка резервного копирования базы данных\n\nВремя: {{failed_at}}\nОшибка: {{error_message}}\n\nПроверьте журналы сервера.`, + }, + fr: { + subject: '[PicPeak] ÉCHEC de la sauvegarde de la base de données', + body_html: `La sauvegarde planifiée de la base de données a échoué et nécessite une investigation manuelle.
+Consultez les journaux du serveur pour plus de détails.
`, + body_text: `Échec de la sauvegarde de la base de données\n\nHeure : {{failed_at}}\nErreur : {{error_message}}\n\nConsultez les journaux du serveur.`, + }, + }, + + restore_completed: { + nl: { + subject: '[PicPeak] Database-herstel succesvol', + body_html: `De handmatige database-herstel-operatie is succesvol voltooid.
+A restauração manual do banco de dados foi concluída com sucesso.
+Ручная операция восстановления базы данных успешно завершена.
+L'opération manuelle de restauration de la base de données s'est terminée avec succès.
+De handmatige database-herstel-operatie is mislukt en moet handmatig worden onderzocht.
+A restauração manual do banco de dados falhou e precisa de investigação.
+Ручная операция восстановления базы данных завершилась с ошибкой и требует проверки.
+L'opération manuelle de restauration de la base de données a échoué et nécessite une investigation.
+De geplande bestandsback-up is succesvol voltooid.
+O backup agendado de arquivos foi concluído com sucesso.
+Запланированное резервное копирование файлов успешно завершено.
+La sauvegarde planifiée des fichiers s'est terminée avec succès.
+De geplande bestandsback-up is mislukt en moet worden onderzocht.
+O backup agendado de arquivos falhou e precisa de investigação.
+Запланированное резервное копирование файлов завершилось с ошибкой и требует проверки.
+La sauvegarde planifiée des fichiers a échoué et nécessite une investigation.
+Er is een nieuwe versie van PicPeak beschikbaar.
+Uma nova versão do PicPeak está disponível.
+Появилась новая версия PicPeak.
+Посмотреть примечания к выпуску
`, + body_text: `Доступна новая версия PicPeak\n\nТекущая версия: {{current_version}}\nНовая версия: {{new_version}}\nКанал: {{channel}}\n\nПримечания к выпуску: {{release_url}}`, + }, + fr: { + subject: 'Mise à jour PicPeak disponible : version {{new_version}}', + body_html: `Une nouvelle version de PicPeak est disponible.
+Dit is een testmail om te bevestigen dat de PicPeak-update-meldingen correct zijn geconfigureerd.
+Geen actie vereist.
`, + body_text: `Testmelding\n\nDit is een testmail om te bevestigen dat de PicPeak-update-meldingen correct zijn geconfigureerd. Geen actie vereist.`, + }, + pt: { + subject: '[Teste] Notificações de atualização do PicPeak funcionando', + body_html: `Este é um e-mail de teste para confirmar que as notificações de atualização do PicPeak estão configuradas corretamente.
+Nenhuma ação é necessária.
`, + body_text: `Notificação de teste\n\nEste é um e-mail de teste para confirmar que as notificações de atualização do PicPeak estão configuradas corretamente. Nenhuma ação é necessária.`, + }, + ru: { + subject: '[Тест] Уведомления об обновлениях PicPeak работают', + body_html: `Это тестовое письмо для подтверждения корректной настройки уведомлений об обновлениях PicPeak.
+Никаких действий не требуется.
`, + body_text: `Тестовое уведомление\n\nЭто тестовое письмо для подтверждения корректной настройки уведомлений об обновлениях PicPeak. Никаких действий не требуется.`, + }, + fr: { + subject: '[Test] Les notifications de mise à jour PicPeak fonctionnent', + body_html: `Ceci est un e-mail de test confirmant que les notifications de mise à jour PicPeak sont correctement configurées.
+Aucune action n'est requise.
`, + body_text: `Notification de test\n\nCeci est un e-mail de test confirmant que les notifications de mise à jour PicPeak sont correctement configurées. Aucune action n'est requise.`, + }, + }, +}; + +exports.up = async function(knex) { + if (!(await knex.schema.hasTable('email_templates'))) return; + if (!(await knex.schema.hasTable('email_template_translations'))) return; + + // Resolve template_key → id once, skip keys that aren't seeded on + // this install (e.g. customer_invitation on a pre-090 instance). + const rows = await knex('email_templates') + .whereIn('template_key', Object.keys(TRANSLATIONS)) + .select('id', 'template_key'); + const keyToId = Object.fromEntries(rows.map((r) => [r.template_key, r.id])); + + let inserted = 0; + let skipped = 0; + + for (const [key, perLocale] of Object.entries(TRANSLATIONS)) { + const templateId = keyToId[key]; + if (!templateId) { + // Template not present on this install (older release than the + // seeding migration). Skip — there's nothing to attach to. + continue; + } + + for (const [language, content] of Object.entries(perLocale)) { + const existing = await knex('email_template_translations') + .where({ template_id: templateId, language }) + .first(); + if (existing) { + skipped += 1; + continue; + } + await knex('email_template_translations').insert({ + template_id: templateId, + language, + subject: content.subject, + body_html: content.body_html, + body_text: content.body_text, + created_at: new Date(), + updated_at: new Date(), + }); + inserted += 1; + } + } + + console.log(`099_seed_missing_email_template_translations: inserted=${inserted}, skipped=${skipped}`); +}; + +exports.down = async function(knex) { + // Down-migration intentionally a no-op. We don't know which of the + // locale rows existed before this migration vs were inserted by it — + // dropping every nl/pt/ru/fr row would wipe content the admin may + // have edited in the UI. Rollback by hand if you really need to. +};