fixes lang update
This commit is contained in:
parent
c3646075ff
commit
fc2ef731a2
2 changed files with 141 additions and 59 deletions
|
|
@ -985,3 +985,19 @@
|
||||||
color: var(--text-tertiary, #999);
|
color: var(--text-tertiary, #999);
|
||||||
margin-top: 0.5rem;
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ type ProgressInfo = {
|
||||||
total: number;
|
total: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
done?: boolean;
|
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). */
|
/** Keys in language set before sync (from sync-diff). */
|
||||||
keysCurrent?: number;
|
keysCurrent?: number;
|
||||||
/** New keys vs xx before PUT (from sync-diff). */
|
/** 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 _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;
|
const master = progress.keysMasterTotal;
|
||||||
let keysLine: string | null = null;
|
const pending = progress.keysPending ?? 0;
|
||||||
if (master != null && master > 0) {
|
const cur = progress.keysCurrent ?? 0;
|
||||||
const pending = progress.keysPending ?? 0;
|
const translated = progress.keysTranslated;
|
||||||
if (pending > 0 && progress.keysTranslated !== undefined) {
|
const hasMaster = master != null && master > 0;
|
||||||
keysLine = t('{tr} / {pending} neue Schlüssel übersetzt · Basis {m}', {
|
|
||||||
tr: String(progress.keysTranslated),
|
const entryJ = pending > 0 ? (translated ?? 0) : cur;
|
||||||
pending: String(pending),
|
const entryTotal = pending > 0 ? pending : (master ?? 0);
|
||||||
m: String(master),
|
const showEntryMeter = hasMaster && entryTotal > 0;
|
||||||
});
|
const awaitingAi =
|
||||||
} else if (pending > 0) {
|
pending > 0 &&
|
||||||
keysLine = t('{pending} / {m} (neu / Basis-Schlüssel)', {
|
translated === undefined &&
|
||||||
pending: String(pending),
|
!progress.done &&
|
||||||
m: String(master),
|
!progress.error;
|
||||||
});
|
|
||||||
} else {
|
const [softEntryPct, setSoftEntryPct] = useState(0);
|
||||||
const cur = progress.keysCurrent ?? 0;
|
useEffect(() => {
|
||||||
keysLine = t('{cur} / {m} Schlüssel abgedeckt', { cur: String(cur), m: String(master) });
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -138,51 +168,77 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
|
||||||
border: '1px solid var(--border-color, #ddd)',
|
border: '1px solid var(--border-color, #ddd)',
|
||||||
borderRadius: 'var(--border-radius, 8px)',
|
borderRadius: 'var(--border-radius, 8px)',
|
||||||
padding: '2rem 2.5rem',
|
padding: '2rem 2.5rem',
|
||||||
minWidth: 340,
|
minWidth: 320,
|
||||||
maxWidth: 480,
|
maxWidth: 420,
|
||||||
boxShadow: '0 4px 24px rgba(0,0,0,0.12)',
|
boxShadow: '0 4px 24px rgba(0,0,0,0.12)',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p style={{ fontWeight: 600, fontSize: '1.05rem', marginBottom: '0.75rem' }}>
|
<p style={{ fontWeight: 600, fontSize: '1.05rem', margin: '0 0 1rem', lineHeight: 1.3 }}>
|
||||||
{progress.message}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<div
|
{showLangMeter && (
|
||||||
style={{
|
<>
|
||||||
width: '100%',
|
<p
|
||||||
height: 8,
|
style={{
|
||||||
background: 'var(--border-color, #e2e8f0)',
|
margin: '0 0 0.35rem',
|
||||||
borderRadius: 4,
|
fontSize: '1rem',
|
||||||
overflow: 'hidden',
|
fontWeight: 600,
|
||||||
marginBottom: '0.5rem',
|
fontVariantNumeric: 'tabular-nums',
|
||||||
}}
|
color: 'var(--text-primary, #1e293b)',
|
||||||
>
|
}}
|
||||||
<div
|
>
|
||||||
style={{
|
{progress.current} / {progress.total}
|
||||||
width: `${pct}%`,
|
</p>
|
||||||
height: '100%',
|
<div
|
||||||
background: progress.error
|
style={{
|
||||||
? 'var(--error-color, #c53030)'
|
width: '100%',
|
||||||
: 'var(--primary-color, #3182ce)',
|
height: 8,
|
||||||
borderRadius: 4,
|
background: 'var(--border-color, #e2e8f0)',
|
||||||
transition: 'width 0.3s ease',
|
borderRadius: 4,
|
||||||
}}
|
overflow: 'hidden',
|
||||||
/>
|
marginBottom: showEntryMeter ? '0.85rem' : 0,
|
||||||
</div>
|
}}
|
||||||
{progress.total > 1 && (
|
>
|
||||||
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>
|
<div
|
||||||
{t('Schritt {cur} von {tot}', { cur: String(progress.current), tot: String(progress.total) })}
|
style={{
|
||||||
{progress.done && !progress.error && ` — ${t('fertig')}`}
|
width: `${pctLang}%`,
|
||||||
</p>
|
height: '100%',
|
||||||
|
background: barBg,
|
||||||
|
borderRadius: 4,
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{progress.total === 1 && progress.done && !progress.error && (
|
{showEntryMeter && (
|
||||||
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>{t('fertig')}</p>
|
<>
|
||||||
)}
|
<p
|
||||||
{keysLine && (
|
style={{
|
||||||
<p style={{ fontSize: '0.8rem', opacity: 0.75, marginTop: '0.35rem' }}>{keysLine}</p>
|
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>
|
||||||
|
<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 && (
|
{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}
|
{progress.error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -321,6 +377,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
setProgress({
|
setProgress({
|
||||||
message: t('Aktualisiere {lang}…', { lang: label }),
|
message: t('Aktualisiere {lang}…', { lang: label }),
|
||||||
|
progressHeading: label,
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 1,
|
total: 1,
|
||||||
keysCurrent,
|
keysCurrent,
|
||||||
|
|
@ -333,6 +390,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
||||||
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
|
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
|
||||||
setProgress({
|
setProgress({
|
||||||
message: t('{lang} aktualisiert.', { lang: label }),
|
message: t('{lang} aktualisiert.', { lang: label }),
|
||||||
|
progressHeading: label,
|
||||||
current: 1,
|
current: 1,
|
||||||
total: 1,
|
total: 1,
|
||||||
done: true,
|
done: true,
|
||||||
|
|
@ -346,7 +404,14 @@ export const AdminLanguagesPage: React.FC = () => {
|
||||||
await reloadLanguage();
|
await reloadLanguage();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const msg = e.response?.data?.detail || e.message;
|
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);
|
setError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => { setProgress(null); busyRef.current = false; }, 2000);
|
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);
|
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
|
||||||
setProgress({
|
setProgress({
|
||||||
message: t('Aktualisiere {lang}…', { lang: label }),
|
message: t('Aktualisiere {lang}…', { lang: label }),
|
||||||
|
progressHeading: label,
|
||||||
current: step + 1,
|
current: step + 1,
|
||||||
total: totalSteps,
|
total: totalSteps,
|
||||||
keysCurrent: typeof d.entriesCount === 'number' ? d.entriesCount : keysCurrent,
|
keysCurrent: typeof d.entriesCount === 'number' ? d.entriesCount : keysCurrent,
|
||||||
|
|
@ -453,7 +519,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
||||||
|
|
||||||
const _delete = async (code: string) => {
|
const _delete = async (code: string) => {
|
||||||
if (busyRef.current) return;
|
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 }), {
|
const ok = await confirm(t('Sprachset {code} wirklich löschen?', { code }), {
|
||||||
confirmLabel: t('Löschen'),
|
confirmLabel: t('Löschen'),
|
||||||
cancelLabel: t('Abbrechen'),
|
cancelLabel: t('Abbrechen'),
|
||||||
|
|
@ -653,7 +719,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
||||||
title: t('Löschen'),
|
title: t('Löschen'),
|
||||||
icon: <FaTrash />,
|
icon: <FaTrash />,
|
||||||
onClick: (row: LangRow) => _delete(row.id),
|
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')}
|
emptyMessage={t('Keine Einträge')}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue