fix(email): give gallery_created a real German translation

translations.de for gallery_created was the English copy word for word, while
nl/pt/ru/fr/es/sl are all localized. This is the mail sent on every gallery
creation, so German-default installs have been silently mailing English.

Root cause chain, fresh installs only: 001_init seeds the English template;
059 introduces the multilingual columns and fills subject_de/body_html_de/
body_text_de from their _en counterparts (its own comment: "Copy to German as
default"); 075 then materialises exactly those columns as the `de` row. The
real German only ever existed in migrations/legacy/026, and run-migrations.js
runs core/ only for fresh installs -- so every install created since 059 has
the English-as-German row.

A code-only fix would have changed nothing: knex will not re-run 059/075, so
existing installs would keep the bad row forever. Fixed as a content migration
following the repo's precedent for template repairs (094, 172).

Conservative about what it touches: the German row is rewritten only while it
is still byte-identical to English (or empty) -- precisely the broken state --
so a legacy install whose German came from 026, or any admin-edited template,
is left alone. Also repairs the legacy _de columns, which are still
emailProcessor's fallback path. Idempotent, hasTable-guarded, no-op down()
(reverting would restore English-as-German).

Placeholder parity with the English original is exact and test-asserted:
host_name, event_name, event_date, gallery_link, gallery_password, expiry_date.

Two related gaps found but deliberately not fixed, both outside the reported
bug: expiration_warning, gallery_expired and archive_complete are German-is-
English on fresh installs through the identical 059 mechanism (legacy 026
fixed all four). And nl/pt/ru/fr/es/sl additionally wrap a
{{#if welcome_message}} block that the English original lacks, even though
welcome_message is passed at send time -- so EN and now DE drop the
photographer's personal note. That is an English-side gap needing its own
decision.

Refs testplan REPORT.md #16 (Part 3, J.04).
This commit is contained in:
Paul Nothaft
2026-09-01 16:40:02 +02:00
parent 76a1453fa7
commit 73b08a7b5c
2 changed files with 317 additions and 0 deletions
@@ -0,0 +1,112 @@
/**
* Migration 194: give `gallery_created` a real German translation.
*
* On a fresh install the German copy of this template is the ENGLISH copy,
* verbatim. Migration 059 introduces the multilingual columns by seeding
* `subject_de`/`body_html_de`/`body_text_de` from their `_en` counterparts
* ("Copy to German as default"), and migration 075 then materialises exactly
* those columns as the `de` row in `email_template_translations`. The proper
* German lived only in the LEGACY migrations (009/026), which never run on a
* fresh install — so every install created since then mails English to
* German-locale recipients, while nl/pt/ru/fr/es/sl are all localised.
*
* This is the most customer-visible transactional mail we send (one per
* published gallery), so it is repaired as a content UPDATE: a code-only fix
* would leave every existing install on the English-as-German row forever,
* because Knex will not re-run 059/075.
*
* Idempotent, and deliberately conservative about WHICH rows it touches: the
* German row is rewritten only while it is still byte-identical to the English
* one (or empty), which is precisely the broken state. A legacy install whose
* German came from migration 026, or any install where an admin has edited the
* template themselves, is left alone.
*
* The placeholder set matches the English original exactly — host_name,
* event_name, event_date, gallery_link, gallery_password, expiry_date — which
* is also the template's declared `variables` array.
*/
const SUBJECT_DE = 'Ihre Fotogalerie ist bereit!';
const HTML_DE = `<h2>Galerie erfolgreich erstellt</h2>
<p>Guten Tag {{host_name}},</p>
<p>Ihre Fotogalerie „{{event_name}}“ wurde erfolgreich erstellt!</p>
<p><strong>Details zur Galerie:</strong></p>
<ul>
<li>Veranstaltungsdatum: {{event_date}}</li>
<li>Link zur Galerie: <a href="{{gallery_link}}">{{gallery_link}}</a></li>
<li>Passwort: {{gallery_password}}</li>
<li>Verfügbar bis: {{expiry_date}}</li>
</ul>
<p>Teilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.</p>`;
const TEXT_DE = 'Galerie erfolgreich erstellt\n\nGuten Tag {{host_name}},\n\nIhre Fotogalerie „{{event_name}}“ wurde erfolgreich erstellt!\n\nVeranstaltungsdatum: {{event_date}}\nLink zur Galerie: {{gallery_link}}\nPasswort: {{gallery_password}}\nVerfügbar bis: {{expiry_date}}\n\nTeilen Sie diesen Link und das Passwort mit Ihren Gästen, damit diese die Fotos ansehen und herunterladen können.';
// "Not translated yet" = empty, or still the English text.
const isUntranslated = (german, english) => {
const de = (german || '').trim();
if (!de) return true;
return de === (english || '').trim();
};
exports.up = async function(knex) {
if (!(await knex.schema.hasTable('email_templates'))) return;
const master = await knex('email_templates')
.where('template_key', 'gallery_created')
.first();
if (!master) return; // Template not seeded on this install — nothing to fix.
const now = new Date().toISOString();
if (await knex.schema.hasTable('email_template_translations')) {
const enRow = await knex('email_template_translations')
.where({ template_id: master.id, language: 'en' })
.first();
const deRow = await knex('email_template_translations')
.where({ template_id: master.id, language: 'de' })
.first();
const englishHtml = (enRow && enRow.body_html) || master.body_html_en || master.body_html || '';
if (!deRow) {
await knex('email_template_translations').insert({
template_id: master.id,
language: 'de',
subject: SUBJECT_DE,
body_html: HTML_DE,
body_text: TEXT_DE,
created_at: now,
updated_at: now,
});
} else if (isUntranslated(deRow.body_html, englishHtml)) {
await knex('email_template_translations')
.where({ id: deRow.id })
.update({
subject: SUBJECT_DE,
body_html: HTML_DE,
body_text: TEXT_DE,
updated_at: now,
});
}
}
// Legacy per-language columns on the master row — still the fallback path in
// emailProcessor.processTemplate when the translations table is unavailable.
const cols = await knex('email_templates').columnInfo();
if (cols.body_html_de && isUntranslated(master.body_html_de, master.body_html_en)) {
await knex('email_templates')
.where({ id: master.id })
.update({
subject_de: SUBJECT_DE,
body_html_de: HTML_DE,
body_text_de: TEXT_DE,
updated_at: now,
});
}
};
exports.down = async function() {
// No-op: reverting would restore English-as-German. Admins who want
// different copy can edit it under Settings → Email → Templates.
};