fix: i18n cleanup + readiness checklist effect cadence + ChordPicker shadow

- DictationReadinessChecklist was constructing downloadByModel as a fresh Map every render and listing it in the cleanup effect's deps. With the 1 s polling cadence and arbitrary parent rerenders the effect ran more often than it needed to. Memoised the Map on activeTasks; the effect now keys off the memo's identity.
- zh-CN persona tooltipActive/ariaLabelActive matched their inactive twins byte-for-byte ("以人物设定朗读"). The other locales differentiate the active state with a -ing / -中 suffix; zh-CN now reads "正以人物设定朗读" when active.
- personalityPlaceholder was a ~290-character paragraph that doubled as both the example text and the explanation, repeating most of what personalityHint already said. Trimmed to the example only and folded the explanation + leave-blank consequence into the hint, across all four locales.
- Refinement model size keys were size06 / size17 / size4. Renamed the 4B variant to size40 so the decimal padding is consistent.
- ChordPicker's open-effect bound a window.setTimeout id to a local `t`, shadowing the i18n `t` from useTranslation. Renamed to timeoutId.
This commit is contained in:
Jamie Pine
2026-04-25 01:27:06 -07:00
parent c113faf131
commit 70ec8d995b
7 changed files with 29 additions and 23 deletions
@@ -9,7 +9,7 @@ import {
Keyboard,
Loader2,
} from 'lucide-react';
import { useEffect, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast';
@@ -103,10 +103,16 @@ export function DictationReadinessChecklist({
},
});
const downloadByModel = new Map<string, ActiveDownloadTask>();
for (const dl of activeTasks?.downloads ?? []) {
if (dl.status === 'downloading') downloadByModel.set(dl.model_name, dl);
}
// Memo so the Map identity is stable across renders that don't change
// activeTasks — otherwise the cleanup effect below saw a fresh Map every
// render and re-fired on every 1 s poll tick.
const downloadByModel = useMemo(() => {
const m = new Map<string, ActiveDownloadTask>();
for (const dl of activeTasks?.downloads ?? []) {
if (dl.status === 'downloading') m.set(dl.model_name, dl);
}
return m;
}, [activeTasks]);
// When a download disappears from activeTasks, it just finished — refetch
// readiness immediately so the row flips to ✓ instead of waiting up to 5s
@@ -122,7 +128,7 @@ export function DictationReadinessChecklist({
}
}
prevActive.current = current;
}, [activeTasks, queryClient, downloadByModel]);
}, [downloadByModel, queryClient]);
const downloadMutation = useMutation({
mutationFn: async ({ modelName }: { gate: ReadinessGate; modelName: string }) =>
@@ -65,8 +65,8 @@ export function ChordPicker({
setCaptured(initialKeys);
setUnsupportedAttempt(null);
// Defer focus to the next paint so the dialog is mounted.
const t = window.setTimeout(() => captureRef.current?.focus(), 50);
return () => window.clearTimeout(t);
const timeoutId = window.setTimeout(() => captureRef.current?.focus(), 50);
return () => window.clearTimeout(timeoutId);
}
return;
}, [open, initialKeys]);
@@ -427,7 +427,7 @@ export function CapturesPage() {
{t('settings.captures.refinement.model.size17', { tail: t('settings.captures.refinement.model.tail.fast') })}
</SelectItem>
<SelectItem value="4B">
{t('settings.captures.refinement.model.size4', { tail: t('settings.captures.refinement.model.tail.fullQuality') })}
{t('settings.captures.refinement.model.size40', { tail: t('settings.captures.refinement.model.tail.fullQuality') })}
</SelectItem>
</SelectContent>
</Select>