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: `

Galerie créée avec succès

+

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: `

La galerie expire bientôt

+

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.

+

Voir la galerie

`, + 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: `

Galerie archivée

+

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: `

Galerie archivée avec succès

+

La galerie photo « {{event_name}} » a été archivée.

+

Détails de l'archive :

+`, + body_text: `Galerie archivée avec succès\n\nLa galerie photo « {{event_name}} » a été archivée.\n\nTaille : {{archive_size}}\nPhotos : {{photo_count}}\nEmplacement : {{archive_path}}`, + }, + }, + + // ──────────────────────────────────────────────────────────────── + // Admin / RBAC — invitation + password reset. en/de exist, adding + // nl/pt/ru/fr. + // ──────────────────────────────────────────────────────────────── + admin_invitation: { + nl: { + subject: 'U bent uitgenodigd om deel te nemen aan PicPeak als {{role_name}}', + body_html: `

Welkom bij PicPeak

+

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:

+

Account instellen

+

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: `

Bem-vindo(a) ao PicPeak

+

Você foi convidado(a) por {{inviter_name}} para participar do PicPeak como {{role_name}}.

+

Clique no link abaixo para configurar sua conta:

+

Configurar 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: `

Добро пожаловать в PicPeak

+

{{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: `

Bienvenue sur PicPeak

+

{{inviter_name}} vous a invité(e) à rejoindre PicPeak en tant que {{role_name}}.

+

Cliquez sur le lien ci-dessous pour configurer votre compte :

+

Configurer le 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: `

Wachtwoord opnieuw ingesteld

+

Hallo {{admin_name}},

+

Uw PicPeak-administratorwachtwoord is opnieuw ingesteld door {{reset_by}}.

+

Klik op de onderstaande link om een nieuw wachtwoord in te stellen:

+

Nieuw wachtwoord instellen

+

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: `

Senha redefinida

+

Olá {{admin_name}},

+

Sua senha de administrador do PicPeak foi redefinida por {{reset_by}}.

+

Clique no link abaixo para definir uma nova senha:

+

Definir 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: `

Mot de passe réinitialisé

+

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: `

Welkom bij uw fotogalerijen

+

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.

+
+ Account instellen +
+

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: `

Bem-vindo(a) às suas galerias

+

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.

+
+ Configurar conta +
+

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: `

Bienvenue dans vos galeries photo

+

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.

+
+ Configurer le compte +
+

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: `

Database-back-up voltooid

+

De geplande database-back-up is succesvol voltooid.

+`, + body_text: `Database-back-up voltooid\n\nTijdstip: {{completed_at}}\nGrootte: {{backup_size}}\nLocatie: {{backup_path}}`, + }, + pt: { + subject: '[PicPeak] Backup do banco de dados concluído', + body_html: `

Backup do banco de dados concluído

+

O backup agendado do banco de dados foi concluído com sucesso.

+`, + body_text: `Backup do banco de dados concluído\n\nHorário: {{completed_at}}\nTamanho: {{backup_size}}\nLocalização: {{backup_path}}`, + }, + ru: { + subject: '[PicPeak] Резервная копия БД успешно создана', + body_html: `

Резервная копия базы данных создана

+

Запланированное резервное копирование базы данных успешно завершено.

+`, + body_text: `Резервная копия базы данных создана\n\nВремя: {{completed_at}}\nРазмер: {{backup_size}}\nРасположение: {{backup_path}}`, + }, + fr: { + subject: '[PicPeak] Sauvegarde de la base de données réussie', + body_html: `

Sauvegarde de la base de données terminée

+

La sauvegarde planifiée de la base de données s'est terminée avec succès.

+`, + body_text: `Sauvegarde de la base de données terminée\n\nHeure : {{completed_at}}\nTaille : {{backup_size}}\nEmplacement : {{backup_path}}`, + }, + }, + + database_backup_failed: { + nl: { + subject: '[PicPeak] Database-back-up MISLUKT', + body_html: `

Database-back-up mislukt

+

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: `

Falha no backup do banco de dados

+

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: `

Échec de la sauvegarde de la base de données

+

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: `

Database-herstel voltooid

+

De handmatige database-herstel-operatie is succesvol voltooid.

+`, + body_text: `Database-herstel voltooid\n\nTijdstip: {{completed_at}}\nHerstelpunt: {{source_backup}}`, + }, + pt: { + subject: '[PicPeak] Restauração do banco de dados concluída', + body_html: `

Restauração concluída

+

A restauração manual do banco de dados foi concluída com sucesso.

+`, + body_text: `Restauração concluída\n\nHorário: {{completed_at}}\nOrigem: {{source_backup}}`, + }, + ru: { + subject: '[PicPeak] Восстановление БД успешно завершено', + body_html: `

Восстановление базы данных завершено

+

Ручная операция восстановления базы данных успешно завершена.

+`, + body_text: `Восстановление базы данных завершено\n\nВремя: {{completed_at}}\nТочка восстановления: {{source_backup}}`, + }, + fr: { + subject: '[PicPeak] Restauration de la base de données réussie', + body_html: `

Restauration de la base terminée

+

L'opération manuelle de restauration de la base de données s'est terminée avec succès.

+`, + body_text: `Restauration terminée\n\nHeure : {{completed_at}}\nSource : {{source_backup}}`, + }, + }, + + restore_failed: { + nl: { + subject: '[PicPeak] Database-herstel MISLUKT', + body_html: `

Database-herstel mislukt

+

De handmatige database-herstel-operatie is mislukt en moet handmatig worden onderzocht.

+`, + body_text: `Database-herstel mislukt\n\nTijdstip: {{failed_at}}\nFoutmelding: {{error_message}}`, + }, + pt: { + subject: '[PicPeak] FALHA na restauração do banco de dados', + body_html: `

Falha na restauração

+

A restauração manual do banco de dados falhou e precisa de investigação.

+`, + body_text: `Falha na restauração\n\nHorário: {{failed_at}}\nErro: {{error_message}}`, + }, + ru: { + subject: '[PicPeak] ОШИБКА восстановления БД', + body_html: `

Ошибка восстановления базы данных

+

Ручная операция восстановления базы данных завершилась с ошибкой и требует проверки.

+`, + body_text: `Ошибка восстановления базы данных\n\nВремя: {{failed_at}}\nОшибка: {{error_message}}`, + }, + fr: { + subject: '[PicPeak] ÉCHEC de la restauration de la base de données', + body_html: `

Échec de la restauration

+

L'opération manuelle de restauration de la base de données a échoué et nécessite une investigation.

+`, + body_text: `Échec de la restauration\n\nHeure : {{failed_at}}\nErreur : {{error_message}}`, + }, + }, + + // ──────────────────────────────────────────────────────────────── + // File backups (legacy backup_completed / backup_failed pair). + // Mostly identical content to database_backup_* but kept separate + // because the legacy keys are still wired to a different code path. + // ──────────────────────────────────────────────────────────────── + backup_completed: { + nl: { + subject: '[PicPeak] Bestandsback-up voltooid', + body_html: `

Bestandsback-up voltooid

+

De geplande bestandsback-up is succesvol voltooid.

+`, + body_text: `Bestandsback-up voltooid\n\nTijdstip: {{completed_at}}\nGrootte: {{backup_size}}\nLocatie: {{backup_path}}`, + }, + pt: { + subject: '[PicPeak] Backup de arquivos concluído', + body_html: `

Backup de arquivos concluído

+

O backup agendado de arquivos foi concluído com sucesso.

+`, + body_text: `Backup de arquivos concluído\n\nHorário: {{completed_at}}\nTamanho: {{backup_size}}\nLocalização: {{backup_path}}`, + }, + ru: { + subject: '[PicPeak] Резервное копирование файлов завершено', + body_html: `

Резервное копирование файлов завершено

+

Запланированное резервное копирование файлов успешно завершено.

+`, + body_text: `Резервное копирование файлов завершено\n\nВремя: {{completed_at}}\nРазмер: {{backup_size}}\nРасположение: {{backup_path}}`, + }, + fr: { + subject: '[PicPeak] Sauvegarde des fichiers terminée', + body_html: `

Sauvegarde des fichiers terminée

+

La sauvegarde planifiée des fichiers s'est terminée avec succès.

+`, + body_text: `Sauvegarde des fichiers terminée\n\nHeure : {{completed_at}}\nTaille : {{backup_size}}\nEmplacement : {{backup_path}}`, + }, + }, + + backup_failed: { + nl: { + subject: '[PicPeak] Bestandsback-up MISLUKT', + body_html: `

Bestandsback-up mislukt

+

De geplande bestandsback-up is mislukt en moet worden onderzocht.

+`, + body_text: `Bestandsback-up mislukt\n\nTijdstip: {{failed_at}}\nFoutmelding: {{error_message}}`, + }, + pt: { + subject: '[PicPeak] FALHA no backup de arquivos', + body_html: `

Falha no backup de arquivos

+

O backup agendado de arquivos falhou e precisa de investigação.

+`, + body_text: `Falha no backup de arquivos\n\nHorário: {{failed_at}}\nErro: {{error_message}}`, + }, + ru: { + subject: '[PicPeak] ОШИБКА резервного копирования файлов', + body_html: `

Ошибка резервного копирования файлов

+

Запланированное резервное копирование файлов завершилось с ошибкой и требует проверки.

+`, + body_text: `Ошибка резервного копирования файлов\n\nВремя: {{failed_at}}\nОшибка: {{error_message}}`, + }, + fr: { + subject: '[PicPeak] ÉCHEC de la sauvegarde des fichiers', + body_html: `

Échec de la sauvegarde des fichiers

+

La sauvegarde planifiée des fichiers a échoué et nécessite une investigation.

+`, + body_text: `Échec de la sauvegarde des fichiers\n\nHeure : {{failed_at}}\nErreur : {{error_message}}`, + }, + }, + + // ──────────────────────────────────────────────────────────────── + // Version update notifications + // ──────────────────────────────────────────────────────────────── + version_update_available: { + nl: { + subject: 'PicPeak-update beschikbaar: versie {{new_version}}', + body_html: `

Nieuwe PicPeak-versie beschikbaar

+

Er is een nieuwe versie van PicPeak beschikbaar.

+ +

Release-notities bekijken

`, + body_text: `Nieuwe PicPeak-versie beschikbaar\n\nHuidige versie: {{current_version}}\nNieuwe versie: {{new_version}}\nReleasekanaal: {{channel}}\n\nRelease-notities: {{release_url}}`, + }, + pt: { + subject: 'Atualização do PicPeak disponível: versão {{new_version}}', + body_html: `

Nova versão do PicPeak disponível

+

Uma nova versão do PicPeak está disponível.

+ +

Ver notas da versão

`, + body_text: `Nova versão do PicPeak disponível\n\nVersão atual: {{current_version}}\nNova versão: {{new_version}}\nCanal: {{channel}}\n\nNotas da versão: {{release_url}}`, + }, + ru: { + subject: 'Доступно обновление PicPeak: версия {{new_version}}', + body_html: `

Доступна новая версия PicPeak

+

Появилась новая версия 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: `

Nouvelle version de PicPeak disponible

+

Une nouvelle version de PicPeak est disponible.

+ +

Voir les notes de version

`, + body_text: `Nouvelle version de PicPeak disponible\n\nVersion actuelle : {{current_version}}\nNouvelle version : {{new_version}}\nCanal : {{channel}}\n\nNotes de version : {{release_url}}`, + }, + }, + + version_update_test: { + nl: { + subject: '[Test] PicPeak-update-meldingen werken', + body_html: `

Testmelding

+

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: `

Notificação de teste

+

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: `

Notification de test

+

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. +};