fix(i18n): remove duplicate keys and dead frontend branches

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-10-04 18:58:14 +02:00
parent 668681dd24
commit 2f4c8529e9
5 changed files with 17 additions and 46 deletions

View File

@ -13,6 +13,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Voice list rows no longer show the "Source unknown" badge** (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel.
### Fixed
- **Duplicate German i18n keys and dead frontend code.** Removed 14 shadowed duplicate keys in `i18n.js` (effective translations unchanged), the unreachable body of `audiobookShowPreview`, and two identical-branch ternaries.
- **Security hardening (Sonar findings).** The Docker installer downloads `get-docker.sh` into a private `mkdtemp` dir (not a predictable `/tmp` path) and removes it afterwards; the OMDb poster lookup uses HTTPS; the fish.audio cache key marks MD5 as `usedforsecurity=False`.
- **Background jobs could be garbage-collected mid-run.** Fire-and-forget `asyncio.create_task` calls (magazine, conversation, tts, stt, library, sources) now go through `core.tasks.spawn`, which keeps a strong reference until the task ends and logs failures by exception class only.
- **Voice archetype gallery broke `npm run check:frontend`**: its `say` helper collided with `magazine.js`; renamed to `archSay`.

View File

@ -56,36 +56,6 @@ async function audiobookOpenCurrentInRehearser() {
function audiobookShowPreview() {
audiobookOpenCurrentInRehearser();
return;
const segs = _audiobook.segments || [];
const roster = _audiobook.roster || [];
document.getElementById('audiobook-preview')?.remove();
const ov = document.createElement('div');
ov.id = 'audiobook-preview';
ov.className = 'audiobook-overlay';
// datalist = Narrator + LLM roster + any speakers present in the segments (incl. Unknown)
const speakerSet = [...new Set(['Narrator', ...roster, ...segs.filter(s => s.type === 'dialogue' && s.speaker).map(s => s.speaker)])];
const charCount = speakerSet.filter(n => !/^Unknown|Unbekannt/i.test(n)).length;
const opts = speakerSet.map(n => `<option value="${escHtml(n)}">`).join('');
ov.innerHTML = `<div class="audiobook-box audiobook-preview-box">
<div class="audiobook-title"><span class="mdi mdi-drama-masks"></span> Review cast &amp; lines
<span class="audiobook-count">${segs.length} segments · ${charCount} character${charCount !== 1 ? 's' : ''}</span></div>
<div class="audiobook-msg">Fix any wrong speaker or emotion, then open in the Script Rehearser to assign voices.${_audiobook.narratedPassages ? ` <span class="audiobook-narr-note">${_audiobook.narratedPassages} passage${_audiobook.narratedPassages !== 1 ? 's' : ''} had no dialogue (narrator).</span>` : ''}${_audiobook.degraded ? ` <span class="audiobook-narr-note">${_audiobook.degraded} passage${_audiobook.degraded !== 1 ? 's' : ''} used quick detection — set the “Unknown” speakers.</span>` : ''}${_audiobook.recoveredGaps ? ` <span class="audiobook-narr-note">${_audiobook.recoveredGaps} dropped sentence/paragraph gap${_audiobook.recoveredGaps !== 1 ? 's' : ''} recovered as Narrator — review placement.</span>` : ''}</div>
<datalist id="audiobook-roster">${opts}</datalist>
<div class="audiobook-seglist" id="audiobook-seglist"></div>
<div class="audiobook-actions">
<button class="btn-secondary btn-sm" id="audiobook-preview-cancel">Cancel</button>
<button class="btn-primary btn-sm" id="audiobook-preview-open"><span class="mdi mdi-account-music-outline"></span> Open in Rehearser</button>
</div>
</div>`;
document.body.appendChild(ov);
ov.querySelector('#audiobook-seglist').innerHTML = segs.map((s, i) => `<div class="audiobook-seg${s.type === 'dialogue' ? ' is-dialog' : ''}">
<input class="audiobook-seg-sp" data-i="${i}" list="audiobook-roster" value="${escHtml(s.speaker || 'Narrator')}" aria-label="Speaker">
<input class="audiobook-seg-emo" data-i="${i}" value="${escHtml(s.emotion || '')}" placeholder="emotion" aria-label="Emotion"${s.type === 'dialogue' ? '' : ' disabled'}>
<div class="audiobook-seg-text">${_abMarkQuotes(highlightText(_abDisplayText(s)))}</div>
</div>`).join('');
ov.querySelector('#audiobook-preview-cancel').addEventListener('click', () => ov.remove());
ov.querySelector('#audiobook-preview-open').addEventListener('click', () => { audiobookApplyPreviewAndOpen(); ov.remove(); });
}
function audiobookApplyPreviewAndOpen() {

View File

@ -40,16 +40,16 @@ const I18N_DICT = {
'All categories': 'Alle Kategorien',
'Category': 'Kategorie',
'Load more': 'Mehr laden',
'Age': 'Alter',
'Language': 'Sprache',
'male': 'männlich',
'female': 'weiblich',
'neutral': 'neutral',
'child': 'Kind',
'teenager': 'Jugendlich',
'young adult': 'jung',
'middle-aged': 'mittleren Alters',
'elderly': 'älter',
'high': 'hoch',
'mid': 'mittel',
'low': 'tief',
@ -62,13 +62,13 @@ const I18N_DICT = {
'southern': 'südlich',
'German': 'Deutsch',
'English': 'Englisch',
'warm': 'warm',
'husky': 'rau',
'crisp': 'klar',
'smooth': 'weich',
'rich': 'voll',
'magnetic': 'magnetisch',
'authoritative': 'autoritär',
// Seed Finder auto-pick
'Generates up to 8 candidates, scores them and highlights the best. Nothing is saved until you click Use seed.': 'Erzeugt bis zu 8 Kandidaten, bewertet sie und hebt den besten hervor. Gespeichert wird erst nach Klick auf „Seed verwenden“.',
'Auto-pick best': 'Besten automatisch wählen',

View File

@ -1259,7 +1259,7 @@ function _lcdField(label, value, multiline) {
if (!v) return '';
return '<div class="lcd-field">'
+ '<div class="lcd-field-label">' + label + '</div>'
+ '<div class="lcd-field-value">' + (multiline ? escHtml(v) : escHtml(v)) + '</div>'
+ '<div class="lcd-field-value">' + escHtml(v) + '</div>'
+ '</div>';
}

View File

@ -4852,7 +4852,7 @@ function renderArticle() {
: '';
return '<span class="mag-w" data-w="' + j + '">' + esc(w) + '</span>' + marker;
}).join(' ');
return '<' + tag + ' class="mag-s' + (open === 'span' ? '' : '') + '" data-i="' + i + '" ' +
return '<' + tag + ' class="mag-s" data-i="' + i + '" ' +
'onclick="magSeek(' + i + ')">' + wordsHtml + '</' + open + '> ' + (asidesAfter.get(i) || '');
}).join('');
highlight(state.idx);