feat(email-templates): seed missing nl/pt/ru/fr translations
This commit is contained in:
@@ -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: `<h2>Galerie créée avec succès</h2>
|
||||||
|
<p>Bonjour {{host_name}},</p>
|
||||||
|
<p>Votre galerie photo « {{event_name}} » a été créée avec succès !</p>
|
||||||
|
<p><strong>Détails de la galerie :</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Date de l'événement : {{event_date}}</li>
|
||||||
|
<li>Lien de la galerie : <a href="{{gallery_link}}">{{gallery_link}}</a></li>
|
||||||
|
<li>Mot de passe : {{gallery_password}}</li>
|
||||||
|
<li>Expire le : {{expiry_date}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Partagez ce lien et le mot de passe avec vos invités pour qu'ils puissent voir et télécharger les photos.</p>
|
||||||
|
{{#if welcome_message}}<p><em>{{welcome_message}}</em></p>{{/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: `<h2>La galerie expire bientôt</h2>
|
||||||
|
<p>Bonjour {{host_name}},</p>
|
||||||
|
<p>Votre galerie photo « {{event_name}} » expire dans {{days_remaining}} jours.</p>
|
||||||
|
<p>Après l'expiration, la galerie sera archivée et ne sera plus accessible aux invités.</p>
|
||||||
|
<p><a href="{{gallery_link}}">Voir la galerie</a></p>`,
|
||||||
|
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: `<h2>Galerie archivée</h2>
|
||||||
|
<p>Bonjour {{host_name}},</p>
|
||||||
|
<p>Votre galerie photo « {{event_name}} » a expiré et a été archivée.</p>
|
||||||
|
<p>Les invités ne peuvent plus accéder à la galerie. Contactez votre photographe si vous avez besoin de restaurer l'accès.</p>`,
|
||||||
|
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: `<h2>Galerie archivée avec succès</h2>
|
||||||
|
<p>La galerie photo « {{event_name}} » a été archivée.</p>
|
||||||
|
<p><strong>Détails de l'archive :</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Taille de l'archive : {{archive_size}}</li>
|
||||||
|
<li>Nombre de photos : {{photo_count}}</li>
|
||||||
|
<li>Emplacement : {{archive_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Welkom bij PicPeak</h2>
|
||||||
|
<p>U bent uitgenodigd door {{inviter_name}} om deel te nemen aan PicPeak als {{role_name}}.</p>
|
||||||
|
<p>Klik op de onderstaande link om uw account in te stellen:</p>
|
||||||
|
<p><a href="{{invitation_link}}" class="button">Account instellen</a></p>
|
||||||
|
<p>Deze uitnodiging verloopt op {{expires_at}}.</p>
|
||||||
|
<p>Als u deze e-mail niet verwachtte, kunt u deze gerust negeren.</p>`,
|
||||||
|
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: `<h2>Bem-vindo(a) ao PicPeak</h2>
|
||||||
|
<p>Você foi convidado(a) por {{inviter_name}} para participar do PicPeak como {{role_name}}.</p>
|
||||||
|
<p>Clique no link abaixo para configurar sua conta:</p>
|
||||||
|
<p><a href="{{invitation_link}}" class="button">Configurar conta</a></p>
|
||||||
|
<p>Este convite expira em {{expires_at}}.</p>
|
||||||
|
<p>Se você não esperava este e-mail, pode ignorá-lo com segurança.</p>`,
|
||||||
|
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: `<h2>Добро пожаловать в PicPeak</h2>
|
||||||
|
<p>{{inviter_name}} пригласил(а) вас присоединиться к PicPeak в роли {{role_name}}.</p>
|
||||||
|
<p>Перейдите по ссылке ниже, чтобы настроить учётную запись:</p>
|
||||||
|
<p><a href="{{invitation_link}}" class="button">Настроить учётную запись</a></p>
|
||||||
|
<p>Срок действия приглашения истекает {{expires_at}}.</p>
|
||||||
|
<p>Если вы не ожидали этого письма, можете его проигнорировать.</p>`,
|
||||||
|
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: `<h2>Bienvenue sur PicPeak</h2>
|
||||||
|
<p>{{inviter_name}} vous a invité(e) à rejoindre PicPeak en tant que {{role_name}}.</p>
|
||||||
|
<p>Cliquez sur le lien ci-dessous pour configurer votre compte :</p>
|
||||||
|
<p><a href="{{invitation_link}}" class="button">Configurer le compte</a></p>
|
||||||
|
<p>Cette invitation expire le {{expires_at}}.</p>
|
||||||
|
<p>Si vous n'attendiez pas cet e-mail, vous pouvez l'ignorer en toute sécurité.</p>`,
|
||||||
|
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: `<h2>Wachtwoord opnieuw ingesteld</h2>
|
||||||
|
<p>Hallo {{admin_name}},</p>
|
||||||
|
<p>Uw PicPeak-administratorwachtwoord is opnieuw ingesteld door {{reset_by}}.</p>
|
||||||
|
<p>Klik op de onderstaande link om een nieuw wachtwoord in te stellen:</p>
|
||||||
|
<p><a href="{{reset_link}}" class="button">Nieuw wachtwoord instellen</a></p>
|
||||||
|
<p>Deze link verloopt op {{expires_at}}. Heeft u deze actie niet aangevraagd? Neem dan onmiddellijk contact op met uw teambeheerder.</p>`,
|
||||||
|
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: `<h2>Senha redefinida</h2>
|
||||||
|
<p>Olá {{admin_name}},</p>
|
||||||
|
<p>Sua senha de administrador do PicPeak foi redefinida por {{reset_by}}.</p>
|
||||||
|
<p>Clique no link abaixo para definir uma nova senha:</p>
|
||||||
|
<p><a href="{{reset_link}}" class="button">Definir nova senha</a></p>
|
||||||
|
<p>Este link expira em {{expires_at}}. Se você não solicitou esta ação, entre em contato com o administrador da sua equipe imediatamente.</p>`,
|
||||||
|
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: `<h2>Пароль сброшен</h2>
|
||||||
|
<p>Здравствуйте, {{admin_name}}!</p>
|
||||||
|
<p>Ваш пароль администратора PicPeak был сброшен пользователем {{reset_by}}.</p>
|
||||||
|
<p>Перейдите по ссылке ниже, чтобы задать новый пароль:</p>
|
||||||
|
<p><a href="{{reset_link}}" class="button">Задать новый пароль</a></p>
|
||||||
|
<p>Срок действия ссылки истекает {{expires_at}}. Если вы не запрашивали это действие, немедленно свяжитесь с администратором вашей команды.</p>`,
|
||||||
|
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: `<h2>Mot de passe réinitialisé</h2>
|
||||||
|
<p>Bonjour {{admin_name}},</p>
|
||||||
|
<p>Votre mot de passe administrateur PicPeak a été réinitialisé par {{reset_by}}.</p>
|
||||||
|
<p>Cliquez sur le lien ci-dessous pour définir un nouveau mot de passe :</p>
|
||||||
|
<p><a href="{{reset_link}}" class="button">Définir un nouveau mot de passe</a></p>
|
||||||
|
<p>Ce lien expire le {{expires_at}}. Si vous n'êtes pas à l'origine de cette demande, contactez immédiatement votre administrateur.</p>`,
|
||||||
|
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: `<h2>Welkom bij uw fotogalerijen</h2>
|
||||||
|
<p>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.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{{invite_link}}" class="button">Account instellen</a>
|
||||||
|
</div>
|
||||||
|
<p>Deze uitnodiging verloopt op {{expires_at}}. Werkt de link niet? Kopieer hem en plak hem in uw browser:</p>
|
||||||
|
<p style="word-break: break-all; font-size: 13px; color: #666;">{{invite_link}}</p>
|
||||||
|
<p>Heeft u deze e-mail niet verwacht? U kunt deze gerust negeren.</p>`,
|
||||||
|
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: `<h2>Bem-vindo(a) às suas galerias</h2>
|
||||||
|
<p>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.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{{invite_link}}" class="button">Configurar conta</a>
|
||||||
|
</div>
|
||||||
|
<p>Este convite expira em {{expires_at}}. Se o link não funcionar, copie e cole-o no navegador:</p>
|
||||||
|
<p style="word-break: break-all; font-size: 13px; color: #666;">{{invite_link}}</p>
|
||||||
|
<p>Se você não esperava este e-mail, pode ignorá-lo com segurança.</p>`,
|
||||||
|
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: `<h2>Добро пожаловать в ваши галереи</h2>
|
||||||
|
<p>Вас пригласили создать учётную запись клиента, чтобы видеть все ваши галереи событий в одном месте — больше никаких отдельных ссылок и паролей.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{{invite_link}}" class="button">Настроить учётную запись</a>
|
||||||
|
</div>
|
||||||
|
<p>Срок действия приглашения истекает {{expires_at}}. Если ссылка не работает, скопируйте её в адресную строку браузера:</p>
|
||||||
|
<p style="word-break: break-all; font-size: 13px; color: #666;">{{invite_link}}</p>
|
||||||
|
<p>Если вы не ожидали этого письма, можете его проигнорировать.</p>`,
|
||||||
|
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: `<h2>Bienvenue dans vos galeries photo</h2>
|
||||||
|
<p>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.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{{invite_link}}" class="button">Configurer le compte</a>
|
||||||
|
</div>
|
||||||
|
<p>Cette invitation expire le {{expires_at}}. Si le lien ne fonctionne pas, copiez-le et collez-le dans votre navigateur :</p>
|
||||||
|
<p style="word-break: break-all; font-size: 13px; color: #666;">{{invite_link}}</p>
|
||||||
|
<p>Si vous n'attendiez pas cet e-mail, vous pouvez l'ignorer en toute sécurité.</p>`,
|
||||||
|
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: `<h2>Database-back-up voltooid</h2>
|
||||||
|
<p>De geplande database-back-up is succesvol voltooid.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{completed_at}}</li>
|
||||||
|
<li>Grootte: {{backup_size}}</li>
|
||||||
|
<li>Locatie: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Backup do banco de dados concluído</h2>
|
||||||
|
<p>O backup agendado do banco de dados foi concluído com sucesso.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{completed_at}}</li>
|
||||||
|
<li>Tamanho: {{backup_size}}</li>
|
||||||
|
<li>Localização: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Резервная копия базы данных создана</h2>
|
||||||
|
<p>Запланированное резервное копирование базы данных успешно завершено.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{completed_at}}</li>
|
||||||
|
<li>Размер: {{backup_size}}</li>
|
||||||
|
<li>Расположение: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Sauvegarde de la base de données terminée</h2>
|
||||||
|
<p>La sauvegarde planifiée de la base de données s'est terminée avec succès.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{completed_at}}</li>
|
||||||
|
<li>Taille : {{backup_size}}</li>
|
||||||
|
<li>Emplacement : {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Database-back-up mislukt</h2>
|
||||||
|
<p>De geplande database-back-up is mislukt en moet handmatig worden onderzocht.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{failed_at}}</li>
|
||||||
|
<li>Foutmelding: {{error_message}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Controleer de serverlogboeken voor meer details.</p>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Falha no backup do banco de dados</h2>
|
||||||
|
<p>O backup agendado do banco de dados falhou e precisa de investigação manual.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{failed_at}}</li>
|
||||||
|
<li>Erro: {{error_message}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Verifique os logs do servidor para mais detalhes.</p>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Ошибка резервного копирования базы данных</h2>
|
||||||
|
<p>Запланированное резервное копирование базы данных завершилось с ошибкой и требует ручной проверки.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{failed_at}}</li>
|
||||||
|
<li>Ошибка: {{error_message}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Проверьте журналы сервера для получения дополнительной информации.</p>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Échec de la sauvegarde de la base de données</h2>
|
||||||
|
<p>La sauvegarde planifiée de la base de données a échoué et nécessite une investigation manuelle.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{failed_at}}</li>
|
||||||
|
<li>Erreur : {{error_message}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Consultez les journaux du serveur pour plus de détails.</p>`,
|
||||||
|
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: `<h2>Database-herstel voltooid</h2>
|
||||||
|
<p>De handmatige database-herstel-operatie is succesvol voltooid.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{completed_at}}</li>
|
||||||
|
<li>Herstelpunt: {{source_backup}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Restauração concluída</h2>
|
||||||
|
<p>A restauração manual do banco de dados foi concluída com sucesso.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{completed_at}}</li>
|
||||||
|
<li>Origem: {{source_backup}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Restauração concluída\n\nHorário: {{completed_at}}\nOrigem: {{source_backup}}`,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
subject: '[PicPeak] Восстановление БД успешно завершено',
|
||||||
|
body_html: `<h2>Восстановление базы данных завершено</h2>
|
||||||
|
<p>Ручная операция восстановления базы данных успешно завершена.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{completed_at}}</li>
|
||||||
|
<li>Точка восстановления: {{source_backup}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Восстановление базы данных завершено\n\nВремя: {{completed_at}}\nТочка восстановления: {{source_backup}}`,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: '[PicPeak] Restauration de la base de données réussie',
|
||||||
|
body_html: `<h2>Restauration de la base terminée</h2>
|
||||||
|
<p>L'opération manuelle de restauration de la base de données s'est terminée avec succès.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{completed_at}}</li>
|
||||||
|
<li>Source : {{source_backup}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Restauration terminée\n\nHeure : {{completed_at}}\nSource : {{source_backup}}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
restore_failed: {
|
||||||
|
nl: {
|
||||||
|
subject: '[PicPeak] Database-herstel MISLUKT',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Database-herstel mislukt</h2>
|
||||||
|
<p>De handmatige database-herstel-operatie is mislukt en moet handmatig worden onderzocht.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{failed_at}}</li>
|
||||||
|
<li>Foutmelding: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Falha na restauração</h2>
|
||||||
|
<p>A restauração manual do banco de dados falhou e precisa de investigação.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{failed_at}}</li>
|
||||||
|
<li>Erro: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Falha na restauração\n\nHorário: {{failed_at}}\nErro: {{error_message}}`,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
subject: '[PicPeak] ОШИБКА восстановления БД',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Ошибка восстановления базы данных</h2>
|
||||||
|
<p>Ручная операция восстановления базы данных завершилась с ошибкой и требует проверки.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{failed_at}}</li>
|
||||||
|
<li>Ошибка: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Ошибка восстановления базы данных\n\nВремя: {{failed_at}}\nОшибка: {{error_message}}`,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: '[PicPeak] ÉCHEC de la restauration de la base de données',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Échec de la restauration</h2>
|
||||||
|
<p>L'opération manuelle de restauration de la base de données a échoué et nécessite une investigation.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{failed_at}}</li>
|
||||||
|
<li>Erreur : {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Bestandsback-up voltooid</h2>
|
||||||
|
<p>De geplande bestandsback-up is succesvol voltooid.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{completed_at}}</li>
|
||||||
|
<li>Grootte: {{backup_size}}</li>
|
||||||
|
<li>Locatie: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Backup de arquivos concluído</h2>
|
||||||
|
<p>O backup agendado de arquivos foi concluído com sucesso.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{completed_at}}</li>
|
||||||
|
<li>Tamanho: {{backup_size}}</li>
|
||||||
|
<li>Localização: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Backup de arquivos concluído\n\nHorário: {{completed_at}}\nTamanho: {{backup_size}}\nLocalização: {{backup_path}}`,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
subject: '[PicPeak] Резервное копирование файлов завершено',
|
||||||
|
body_html: `<h2>Резервное копирование файлов завершено</h2>
|
||||||
|
<p>Запланированное резервное копирование файлов успешно завершено.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{completed_at}}</li>
|
||||||
|
<li>Размер: {{backup_size}}</li>
|
||||||
|
<li>Расположение: {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Резервное копирование файлов завершено\n\nВремя: {{completed_at}}\nРазмер: {{backup_size}}\nРасположение: {{backup_path}}`,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: '[PicPeak] Sauvegarde des fichiers terminée',
|
||||||
|
body_html: `<h2>Sauvegarde des fichiers terminée</h2>
|
||||||
|
<p>La sauvegarde planifiée des fichiers s'est terminée avec succès.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{completed_at}}</li>
|
||||||
|
<li>Taille : {{backup_size}}</li>
|
||||||
|
<li>Emplacement : {{backup_path}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2 style="color:#b91c1c;">Bestandsback-up mislukt</h2>
|
||||||
|
<p>De geplande bestandsback-up is mislukt en moet worden onderzocht.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tijdstip: {{failed_at}}</li>
|
||||||
|
<li>Foutmelding: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Bestandsback-up mislukt\n\nTijdstip: {{failed_at}}\nFoutmelding: {{error_message}}`,
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
subject: '[PicPeak] FALHA no backup de arquivos',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Falha no backup de arquivos</h2>
|
||||||
|
<p>O backup agendado de arquivos falhou e precisa de investigação.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Horário: {{failed_at}}</li>
|
||||||
|
<li>Erro: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Falha no backup de arquivos\n\nHorário: {{failed_at}}\nErro: {{error_message}}`,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
subject: '[PicPeak] ОШИБКА резервного копирования файлов',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Ошибка резервного копирования файлов</h2>
|
||||||
|
<p>Запланированное резервное копирование файлов завершилось с ошибкой и требует проверки.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Время: {{failed_at}}</li>
|
||||||
|
<li>Ошибка: {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
body_text: `Ошибка резервного копирования файлов\n\nВремя: {{failed_at}}\nОшибка: {{error_message}}`,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: '[PicPeak] ÉCHEC de la sauvegarde des fichiers',
|
||||||
|
body_html: `<h2 style="color:#b91c1c;">Échec de la sauvegarde des fichiers</h2>
|
||||||
|
<p>La sauvegarde planifiée des fichiers a échoué et nécessite une investigation.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Heure : {{failed_at}}</li>
|
||||||
|
<li>Erreur : {{error_message}}</li>
|
||||||
|
</ul>`,
|
||||||
|
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: `<h2>Nieuwe PicPeak-versie beschikbaar</h2>
|
||||||
|
<p>Er is een nieuwe versie van PicPeak beschikbaar.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Huidige versie: {{current_version}}</li>
|
||||||
|
<li>Nieuwe versie: {{new_version}}</li>
|
||||||
|
<li>Releasekanaal: {{channel}}</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{release_url}}" class="button">Release-notities bekijken</a></p>`,
|
||||||
|
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: `<h2>Nova versão do PicPeak disponível</h2>
|
||||||
|
<p>Uma nova versão do PicPeak está disponível.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Versão atual: {{current_version}}</li>
|
||||||
|
<li>Nova versão: {{new_version}}</li>
|
||||||
|
<li>Canal: {{channel}}</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{release_url}}" class="button">Ver notas da versão</a></p>`,
|
||||||
|
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: `<h2>Доступна новая версия PicPeak</h2>
|
||||||
|
<p>Появилась новая версия PicPeak.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Текущая версия: {{current_version}}</li>
|
||||||
|
<li>Новая версия: {{new_version}}</li>
|
||||||
|
<li>Канал выпуска: {{channel}}</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{release_url}}" class="button">Посмотреть примечания к выпуску</a></p>`,
|
||||||
|
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: `<h2>Nouvelle version de PicPeak disponible</h2>
|
||||||
|
<p>Une nouvelle version de PicPeak est disponible.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Version actuelle : {{current_version}}</li>
|
||||||
|
<li>Nouvelle version : {{new_version}}</li>
|
||||||
|
<li>Canal : {{channel}}</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{release_url}}" class="button">Voir les notes de version</a></p>`,
|
||||||
|
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: `<h2>Testmelding</h2>
|
||||||
|
<p>Dit is een testmail om te bevestigen dat de PicPeak-update-meldingen correct zijn geconfigureerd.</p>
|
||||||
|
<p>Geen actie vereist.</p>`,
|
||||||
|
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: `<h2>Notificação de teste</h2>
|
||||||
|
<p>Este é um e-mail de teste para confirmar que as notificações de atualização do PicPeak estão configuradas corretamente.</p>
|
||||||
|
<p>Nenhuma ação é necessária.</p>`,
|
||||||
|
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: `<h2>Тестовое уведомление</h2>
|
||||||
|
<p>Это тестовое письмо для подтверждения корректной настройки уведомлений об обновлениях PicPeak.</p>
|
||||||
|
<p>Никаких действий не требуется.</p>`,
|
||||||
|
body_text: `Тестовое уведомление\n\nЭто тестовое письмо для подтверждения корректной настройки уведомлений об обновлениях PicPeak. Никаких действий не требуется.`,
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
subject: '[Test] Les notifications de mise à jour PicPeak fonctionnent',
|
||||||
|
body_html: `<h2>Notification de test</h2>
|
||||||
|
<p>Ceci est un e-mail de test confirmant que les notifications de mise à jour PicPeak sont correctement configurées.</p>
|
||||||
|
<p>Aucune action n'est requise.</p>`,
|
||||||
|
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.
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user