The fifth prose-content branch, and it was broken before anyone translated it

exp-shepherd swept all of plugin-designs and library on main for branches on a
user-visible string's CONTENT and found a FIFTH instance in my file that my own
fix had missed:

  copyFlash.value = ok ? 'Copied!' : 'Copy failed — press Ctrl/Cmd-C'
  setTimeout(() => { if (copyFlash.value.startsWith('Copied')) copyFlash.value = '' }, 1800)

★ THAT GUARD ONLY MATCHED THE SUCCESS BRANCH. "Copy failed" NEVER CLEARED — in
English, today, before any of this was translated. A pre-existing bug that the
prose fix alone would have widened to both branches in ten languages. It is
rendered at :2649, so it is genuinely user-visible.

WHY I MISSED IT, which is the part worth keeping: I grepped
`status.value.startsWith` — the exact shape I already knew — and found four.
exp-shepherd grepped for the PATTERN CLASS across the whole tree and found the
ternary form. I searched for the instance; they searched for the property. Same
distinction that has cost me three wrong conclusions today.

Both strings now come from messages.* in eleven locales, and flashStatus is
generalised to flashRef(target, text, ms) so the identity-clear works for any
transient ref rather than only `status`. flashStatus stays as a thin alias, so
the four existing call sites are untouched.

VERIFIED: zero LIVE prose-content branches remain anywhere in my plugin — the two
grep hits left are my own comments describing the removed code.
check:i18n-parity PASS (0 debt), check:i18n-never-translated PASS with both new
keys 11/11 distinct and adding nothing to the allowlist.
BUNDLE=my-upg-100-list vite build clean.

exp-shepherd confirms upg-neighbor, pplr and country-posters have no such guards,
so this class is now closed across the tree rather than only in my file.
This commit is contained in:
Doxa Maps 2026-09-26 13:28:19 +00:00
parent aaf0a336dc
commit 5747c2ef88
13 changed files with 81 additions and 54 deletions

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "فشل إنشاء كُتيّب التحديد: {reason}",
"posterOneTerritory": "الملصق يطبع منطقة واحدة في كل مرة — {count} محددة.",
"selectionPosterFailed": "فشل إنشاء ملصق التحديد: {reason}",
"mapUnavailable": "الخريطة غير متاحة"
"mapUnavailable": "الخريطة غير متاحة",
"copied": "تم النسخ!",
"copyFailed": "فشل النسخ — اضغط Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Heft der Auswahl fehlgeschlagen: {reason}",
"posterOneTerritory": "Das Plakat druckt ein Gebiet auf einmal — {count} ausgewählt.",
"selectionPosterFailed": "Plakat der Auswahl fehlgeschlagen: {reason}",
"mapUnavailable": "Karte nicht verfügbar"
"mapUnavailable": "Karte nicht verfügbar",
"copied": "Kopiert!",
"copyFailed": "Kopieren fehlgeschlagen — Strg/Cmd-C drücken"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Selection packet failed: {reason}",
"posterOneTerritory": "Poster prints one territory at a time — {count} selected.",
"selectionPosterFailed": "Selection poster failed: {reason}",
"mapUnavailable": "Map unavailable"
"mapUnavailable": "Map unavailable",
"copied": "Copied!",
"copyFailed": "Copy failed — press Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Error al generar el cuadernillo de la selección: {reason}",
"posterOneTerritory": "El cartel imprime un territorio a la vez: {count} seleccionados.",
"selectionPosterFailed": "Error al generar el cartel de la selección: {reason}",
"mapUnavailable": "Mapa no disponible"
"mapUnavailable": "Mapa no disponible",
"copied": "¡Copiado!",
"copyFailed": "Error al copiar: pulsa Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Échec du livret de la sélection : {reason}",
"posterOneTerritory": "L’affiche imprime un territoire à la fois — {count} sélectionnés.",
"selectionPosterFailed": "Échec de l’affiche de la sélection : {reason}",
"mapUnavailable": "Carte indisponible"
"mapUnavailable": "Carte indisponible",
"copied": "Copié !",
"copyFailed": "Échec de la copie — appuyez sur Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "चयन की पुस्तिका विफल: {reason}",
"posterOneTerritory": "पोस्टर एक समय में एक क्षेत्र छापता है — {count} चयनित।",
"selectionPosterFailed": "चयन का पोस्टर विफल: {reason}",
"mapUnavailable": "मानचित्र अनुपलब्ध"
"mapUnavailable": "मानचित्र अनुपलब्ध",
"copied": "कॉपी हो गया!",
"copyFailed": "कॉपी विफल — Ctrl/Cmd-C दबाएँ"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Libretto della selezione non riuscito: {reason}",
"posterOneTerritory": "Il manifesto stampa un territorio alla volta — {count} selezionati.",
"selectionPosterFailed": "Manifesto della selezione non riuscito: {reason}",
"mapUnavailable": "Mappa non disponibile"
"mapUnavailable": "Mappa non disponibile",
"copied": "Copiato!",
"copyFailed": "Copia non riuscita — premi Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Falha ao gerar o caderno da seleção: {reason}",
"posterOneTerritory": "O cartaz imprime um território de cada vez — {count} selecionados.",
"selectionPosterFailed": "Falha ao gerar o cartaz da seleção: {reason}",
"mapUnavailable": "Mapa indisponível"
"mapUnavailable": "Mapa indisponível",
"copied": "Copiado!",
"copyFailed": "Falha ao copiar — pressione Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Broșura selecției a eșuat: {reason}",
"posterOneTerritory": "Afișul tipărește un teritoriu pe rând — {count} selectate.",
"selectionPosterFailed": "Afișul selecției a eșuat: {reason}",
"mapUnavailable": "Harta indisponibilă"
"mapUnavailable": "Harta indisponibilă",
"copied": "Copiat!",
"copyFailed": "Copierea a eșuat — apasă Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "Не удалось создать буклет выбора: {reason}",
"posterOneTerritory": "Плакат печатает по одной территории — выбрано {count}.",
"selectionPosterFailed": "Не удалось создать плакат выбора: {reason}",
"mapUnavailable": "Карта недоступна"
"mapUnavailable": "Карта недоступна",
"copied": "Скопировано!",
"copyFailed": "Не удалось скопировать — нажмите Ctrl/Cmd-C"
}

View file

@ -12,5 +12,7 @@
"selectionPacketFailed": "所选内容的祷告册生成失败:{reason}",
"posterOneTerritory": "海报一次只能打印一个区域——已选 {count} 个。",
"selectionPosterFailed": "所选内容的海报生成失败:{reason}",
"mapUnavailable": "地图不可用"
"mapUnavailable": "地图不可用",
"copied": "已复制!",
"copyFailed": "复制失败——请按 Ctrl/Cmd-C"
}

View file

@ -808,8 +808,12 @@ async function copySelection() {
} catch (err) {
ok = legacyCopy(text) // secure-context / permission failure → fallback
}
copyFlash.value = ok ? 'Copied!' : 'Copy failed — press Ctrl/Cmd-C'
setTimeout(() => { if (copyFlash.value.startsWith('Copied')) copyFlash.value = '' }, 1800)
// ⚠ THE OLD GUARD ONLY MATCHED THE SUCCESS BRANCH — `startsWith('Copied')` — so "Copy failed" NEVER
// CLEARED, in English, before any of this was translated. A pre-existing bug that the prose fix would
// have widened to both branches in ten languages. Found by exp-shepherd sweeping the whole tree for the
// PATTERN (a branch on a user-visible string's content) after I had only grepped for the exact shape I
// already knew, `status.value.startsWith` — which is why I missed the ternary form in my own file.
flashRef(copyFlash, ok ? t('messages.copied') : t('messages.copyFailed'), 1800)
}
// textarea + execCommand('copy') fallback (non-secure context, no clipboard API).
@ -1314,10 +1318,11 @@ async function packetMapSvg(zoneId, R) {
// the status never clears in any non-English locale: a stuck error message, in ten languages, from a fix
// that looked purely cosmetic. Same family as counting prayer points by matching the English word
// "prayer". So compare against the exact string we set instead — locale-independent by construction.
function flashStatus(text, ms) {
status.value = text;
setTimeout(() => { if (status.value === text) status.value = ''; }, ms);
function flashRef(target, text, ms) {
target.value = text;
setTimeout(() => { if (target.value === text) target.value = ''; }, ms);
}
const flashStatus = (text, ms) => flashRef(status, text, ms);
async function printPrayerPacket(zoneId) {
const zone = allLists().find((z) => z.id === zoneId);

File diff suppressed because one or more lines are too long