fixes lang update

This commit is contained in:
ValueOn AG 2026-04-09 00:21:39 +02:00
parent c3646075ff
commit fc2ef731a2
2 changed files with 141 additions and 59 deletions

View file

@ -985,3 +985,19 @@
color: var(--text-tertiary, #999);
margin-top: 0.5rem;
}
/* i18n language update overlay — second progress bar */
.i18nKeysProgressTrack {
width: 100%;
height: 8px;
background: var(--border-color, #e2e8f0);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.i18nKeysProgressFill {
height: 100%;
border-radius: 4px;
background: var(--accent-color, var(--primary-color, #3182ce));
}

View file

@ -23,6 +23,8 @@ type ProgressInfo = {
total: number;
error?: string;
done?: boolean;
/** Short title (e.g. language label) to avoid long sentences in the overlay. */
progressHeading?: string;
/** Keys in language set before sync (from sync-diff). */
keysCurrent?: number;
/** New keys vs xx before PUT (from sync-diff). */
@ -96,28 +98,56 @@ const _isoChoices: { value: string; label: string }[] = [
// ---------------------------------------------------------------------------
const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) => {
const { t } = useLanguage();
const pct = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
const master = progress.keysMasterTotal;
let keysLine: string | null = null;
if (master != null && master > 0) {
const pending = progress.keysPending ?? 0;
if (pending > 0 && progress.keysTranslated !== undefined) {
keysLine = t('{tr} / {pending} neue Schlüssel übersetzt · Basis {m}', {
tr: String(progress.keysTranslated),
pending: String(pending),
m: String(master),
});
} else if (pending > 0) {
keysLine = t('{pending} / {m} (neu / Basis-Schlüssel)', {
pending: String(pending),
m: String(master),
});
} else {
const cur = progress.keysCurrent ?? 0;
keysLine = t('{cur} / {m} Schlüssel abgedeckt', { cur: String(cur), m: String(master) });
const translated = progress.keysTranslated;
const hasMaster = master != null && master > 0;
const entryJ = pending > 0 ? (translated ?? 0) : cur;
const entryTotal = pending > 0 ? pending : (master ?? 0);
const showEntryMeter = hasMaster && entryTotal > 0;
const awaitingAi =
pending > 0 &&
translated === undefined &&
!progress.done &&
!progress.error;
const [softEntryPct, setSoftEntryPct] = useState(0);
useEffect(() => {
if (!awaitingAi) {
setSoftEntryPct(0);
return;
}
const started = Date.now();
const id = window.setInterval(() => {
const elapsed = Date.now() - started;
const cap = 94;
setSoftEntryPct(Math.min(cap, cap * (1 - Math.exp(-elapsed / 14000))));
}, 220);
return () => clearInterval(id);
}, [awaitingAi]);
let entryPct = 0;
if (pending > 0) {
if (translated !== undefined && pending > 0) {
entryPct = Math.min(100, Math.round((translated / pending) * 100));
}
} else if (master && master > 0) {
entryPct = Math.min(100, Math.round((cur / master) * 100));
}
const entryBarPct = awaitingAi ? softEntryPct : entryPct;
const entryJShown = awaitingAi
? Math.min(pending, Math.max(0, Math.floor((softEntryPct / 100) * pending)))
: entryJ;
const showLangMeter = progress.total > 1 || !showEntryMeter;
const pctLang = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
const title = progress.progressHeading ?? progress.message;
const barBg = progress.error ? 'var(--error-color, #c53030)' : 'var(--primary-color, #3182ce)';
return (
<div
style={{
@ -138,14 +168,27 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
border: '1px solid var(--border-color, #ddd)',
borderRadius: 'var(--border-radius, 8px)',
padding: '2rem 2.5rem',
minWidth: 340,
maxWidth: 480,
minWidth: 320,
maxWidth: 420,
boxShadow: '0 4px 24px rgba(0,0,0,0.12)',
textAlign: 'center',
}}
>
<p style={{ fontWeight: 600, fontSize: '1.05rem', marginBottom: '0.75rem' }}>
{progress.message}
<p style={{ fontWeight: 600, fontSize: '1.05rem', margin: '0 0 1rem', lineHeight: 1.3 }}>
{title}
</p>
{showLangMeter && (
<>
<p
style={{
margin: '0 0 0.35rem',
fontSize: '1rem',
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
color: 'var(--text-primary, #1e293b)',
}}
>
{progress.current} / {progress.total}
</p>
<div
style={{
@ -154,35 +197,48 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
background: 'var(--border-color, #e2e8f0)',
borderRadius: 4,
overflow: 'hidden',
marginBottom: '0.5rem',
marginBottom: showEntryMeter ? '0.85rem' : 0,
}}
>
<div
style={{
width: `${pct}%`,
width: `${pctLang}%`,
height: '100%',
background: progress.error
? 'var(--error-color, #c53030)'
: 'var(--primary-color, #3182ce)',
background: barBg,
borderRadius: 4,
transition: 'width 0.3s ease',
}}
/>
</div>
{progress.total > 1 && (
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>
{t('Schritt {cur} von {tot}', { cur: String(progress.current), tot: String(progress.total) })}
{progress.done && !progress.error && `${t('fertig')}`}
</>
)}
{showEntryMeter && (
<>
<p
style={{
margin: showLangMeter ? '0 0 0.35rem' : '0 0 0.35rem',
fontSize: '1rem',
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
color: 'var(--text-primary, #1e293b)',
}}
>
{entryJShown} / {entryTotal}
</p>
)}
{progress.total === 1 && progress.done && !progress.error && (
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>{t('fertig')}</p>
)}
{keysLine && (
<p style={{ fontSize: '0.8rem', opacity: 0.75, marginTop: '0.35rem' }}>{keysLine}</p>
<div className={styles.i18nKeysProgressTrack}>
<div
className={styles.i18nKeysProgressFill}
style={{
width: `${entryBarPct}%`,
background: barBg,
transition: awaitingAi ? 'width 0.25s ease-out' : 'width 0.35s ease',
}}
/>
</div>
</>
)}
{progress.error && (
<p style={{ color: 'var(--error-color, #c53030)', fontSize: '0.85rem', marginTop: '0.5rem' }}>
<p style={{ color: 'var(--error-color, #c53030)', fontSize: '0.85rem', marginTop: '0.75rem' }}>
{progress.error}
</p>
)}
@ -321,6 +377,7 @@ export const AdminLanguagesPage: React.FC = () => {
}
setProgress({
message: t('Aktualisiere {lang}…', { lang: label }),
progressHeading: label,
current: 0,
total: 1,
keysCurrent,
@ -333,6 +390,7 @@ export const AdminLanguagesPage: React.FC = () => {
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({
message: t('{lang} aktualisiert.', { lang: label }),
progressHeading: label,
current: 1,
total: 1,
done: true,
@ -346,7 +404,14 @@ export const AdminLanguagesPage: React.FC = () => {
await reloadLanguage();
} catch (e: any) {
const msg = e.response?.data?.detail || e.message;
setProgress({ message: t('Fehler bei {lang}', { lang: label }), current: 0, total: 1, error: msg, done: true });
setProgress({
message: t('Fehler bei {lang}', { lang: label }),
progressHeading: label,
current: 0,
total: 1,
error: msg,
done: true,
});
setError(msg);
} finally {
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2000);
@ -411,6 +476,7 @@ export const AdminLanguagesPage: React.FC = () => {
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
setProgress({
message: t('Aktualisiere {lang}…', { lang: label }),
progressHeading: label,
current: step + 1,
total: totalSteps,
keysCurrent: typeof d.entriesCount === 'number' ? d.entriesCount : keysCurrent,
@ -453,7 +519,7 @@ export const AdminLanguagesPage: React.FC = () => {
const _delete = async (code: string) => {
if (busyRef.current) return;
if (code === 'xx' || code === 'de') return;
if (code === 'xx') return;
const ok = await confirm(t('Sprachset {code} wirklich löschen?', { code }), {
confirmLabel: t('Löschen'),
cancelLabel: t('Abbrechen'),
@ -653,7 +719,7 @@ export const AdminLanguagesPage: React.FC = () => {
title: t('Löschen'),
icon: <FaTrash />,
onClick: (row: LangRow) => _delete(row.id),
visible: (row: LangRow) => row.id !== 'xx' && row.id !== 'de',
visible: (row: LangRow) => row.id !== 'xx',
},
]}
emptyMessage={t('Keine Einträge')}