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, Keyboard,
Loader2, Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
@@ -103,10 +103,16 @@ export function DictationReadinessChecklist({
}, },
}); });
const downloadByModel = new Map<string, ActiveDownloadTask>(); // 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 ?? []) { for (const dl of activeTasks?.downloads ?? []) {
if (dl.status === 'downloading') downloadByModel.set(dl.model_name, dl); if (dl.status === 'downloading') m.set(dl.model_name, dl);
} }
return m;
}, [activeTasks]);
// When a download disappears from activeTasks, it just finished — refetch // When a download disappears from activeTasks, it just finished — refetch
// readiness immediately so the row flips to ✓ instead of waiting up to 5s // readiness immediately so the row flips to ✓ instead of waiting up to 5s
@@ -122,7 +128,7 @@ export function DictationReadinessChecklist({
} }
} }
prevActive.current = current; prevActive.current = current;
}, [activeTasks, queryClient, downloadByModel]); }, [downloadByModel, queryClient]);
const downloadMutation = useMutation({ const downloadMutation = useMutation({
mutationFn: async ({ modelName }: { gate: ReadinessGate; modelName: string }) => mutationFn: async ({ modelName }: { gate: ReadinessGate; modelName: string }) =>
@@ -65,8 +65,8 @@ export function ChordPicker({
setCaptured(initialKeys); setCaptured(initialKeys);
setUnsupportedAttempt(null); setUnsupportedAttempt(null);
// Defer focus to the next paint so the dialog is mounted. // Defer focus to the next paint so the dialog is mounted.
const t = window.setTimeout(() => captureRef.current?.focus(), 50); const timeoutId = window.setTimeout(() => captureRef.current?.focus(), 50);
return () => window.clearTimeout(t); return () => window.clearTimeout(timeoutId);
} }
return; return;
}, [open, initialKeys]); }, [open, initialKeys]);
@@ -427,7 +427,7 @@ export function CapturesPage() {
{t('settings.captures.refinement.model.size17', { tail: t('settings.captures.refinement.model.tail.fast') })} {t('settings.captures.refinement.model.size17', { tail: t('settings.captures.refinement.model.tail.fast') })}
</SelectItem> </SelectItem>
<SelectItem value="4B"> <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> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
+3 -3
View File
@@ -271,8 +271,8 @@
"defaultEffects": "Default Effects", "defaultEffects": "Default Effects",
"defaultEffectsHint": "Effects applied automatically to all new generations with this voice.", "defaultEffectsHint": "Effects applied automatically to all new generations with this voice.",
"personalityLabel": "Personality", "personalityLabel": "Personality",
"personalityPlaceholder": "Optional. Who this voice is and how they talk. E.g. \"a grumpy pirate who only speaks in nautical metaphors\". Drives the Compose button and the in-character rewrite toggle on the generate page.", "personalityPlaceholder": "e.g. \"a grumpy pirate who only speaks in nautical metaphors\"",
"personalityHint": "Leave blank to hide the Compose button and persona toggle on the generate page." "personalityHint": "Who this voice is and how they talk. Drives the Compose button and the in-character rewrite toggle on the generate page. Leave blank to hide both."
}, },
"avatar": { "avatar": {
"alt": "Avatar preview" "alt": "Avatar preview"
@@ -953,7 +953,7 @@
"description": "Larger models are slower but handle subtle self-corrections and technical vocabulary better.", "description": "Larger models are slower but handle subtle self-corrections and technical vocabulary better.",
"size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}",
"size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}",
"size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}",
"tail": { "tail": {
"veryFast": "Very fast", "veryFast": "Very fast",
"fast": "Fast", "fast": "Fast",
+3 -3
View File
@@ -271,8 +271,8 @@
"defaultEffects": "デフォルトエフェクト", "defaultEffects": "デフォルトエフェクト",
"defaultEffectsHint": "このボイスで新しく生成するすべてのものに自動適用されるエフェクトです。", "defaultEffectsHint": "このボイスで新しく生成するすべてのものに自動適用されるエフェクトです。",
"personalityLabel": "パーソナリティ", "personalityLabel": "パーソナリティ",
"personalityPlaceholder": "任意。このボイスがどんな人物で、どのように話すかを記述します。例:「航海の比喩でしか話さない不機嫌な海賊」。生成ページの「Compose」ボタンと、キャラクターになりきって書き換えるトグルに反映されます。", "personalityPlaceholder": "例:「航海の比喩でしか話さない不機嫌な海賊」",
"personalityHint": "空欄にすると、生成ページの「Compose」ボタンとペルソナトグルは表示されません。" "personalityHint": "このボイスがどんな人物で、どのように話すか。生成ページの「Compose」ボタンとキャラクター書き換えトグルに反映されます。空欄にすると両方とも表示されません。"
}, },
"avatar": { "avatar": {
"alt": "アバタープレビュー" "alt": "アバタープレビュー"
@@ -953,7 +953,7 @@
"description": "大きなモデルは遅くなりますが、微妙な自己修正や専門用語をより適切に処理します。", "description": "大きなモデルは遅くなりますが、微妙な自己修正や専門用語をより適切に処理します。",
"size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}",
"size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}",
"size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}",
"tail": { "tail": {
"veryFast": "超高速", "veryFast": "超高速",
"fast": "高速", "fast": "高速",
+5 -5
View File
@@ -271,8 +271,8 @@
"defaultEffects": "默认效果", "defaultEffects": "默认效果",
"defaultEffectsHint": "自动应用于使用此声音的所有新生成的效果。", "defaultEffectsHint": "自动应用于使用此声音的所有新生成的效果。",
"personalityLabel": "人物设定", "personalityLabel": "人物设定",
"personalityPlaceholder": "可选。这个声音是谁、说话方式如何。例如「一位脾气暴躁的海盗,只会用航海比喻说话」。会驱动生成页面上的「撰写」按钮和入戏改写开关。", "personalityPlaceholder": "例如:「一位脾气暴躁的海盗,只会用航海比喻说话」",
"personalityHint": "留空将隐藏生成页面上的「撰写」按钮和人物设定开关。" "personalityHint": "这个声音是谁、说话方式如何。会驱动生成页面上的「撰写」按钮和入戏改写开关。留空则两者都隐藏。"
}, },
"avatar": { "avatar": {
"alt": "头像预览" "alt": "头像预览"
@@ -708,9 +708,9 @@
"failedDescription": "无法根据此人物设定生成文本。" "failedDescription": "无法根据此人物设定生成文本。"
}, },
"persona": { "persona": {
"tooltipActive": "以人物设定朗读", "tooltipActive": "正以人物设定朗读",
"tooltipInactive": "以人物设定朗读", "tooltipInactive": "以人物设定朗读",
"ariaLabelActive": "以人物设定朗读", "ariaLabelActive": "正以人物设定朗读",
"ariaLabelInactive": "以人物设定朗读" "ariaLabelInactive": "以人物设定朗读"
} }
}, },
@@ -953,7 +953,7 @@
"description": "更大的模型速度较慢,但能更好地处理细微的自我纠正和技术词汇。", "description": "更大的模型速度较慢,但能更好地处理细微的自我纠正和技术词汇。",
"size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}",
"size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}",
"size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}",
"tail": { "tail": {
"veryFast": "非常快", "veryFast": "非常快",
"fast": "快速", "fast": "快速",
+3 -3
View File
@@ -271,8 +271,8 @@
"defaultEffects": "預設效果", "defaultEffects": "預設效果",
"defaultEffectsHint": "自動套用於使用此聲音所有新生成的效果。", "defaultEffectsHint": "自動套用於使用此聲音所有新生成的效果。",
"personalityLabel": "個性", "personalityLabel": "個性",
"personalityPlaceholder": "選填。描述這個聲音是誰以及他們如何說話。例如:「一位脾氣暴躁的海盜,只會用航海比喻說話」。會驅動生成頁面上的「撰寫」按鈕和角色化重寫切換。", "personalityPlaceholder": "例如:「一位脾氣暴躁的海盜,只會用航海比喻說話」",
"personalityHint": "留空則在生成頁面隱藏「撰寫」按鈕和角色切換。" "personalityHint": "這個聲音是誰以及他們如何說話。會驅動生成頁面上的「撰寫」按鈕和角色化重寫切換。留空則兩者都隱藏。"
}, },
"avatar": { "avatar": {
"alt": "頭像預覽" "alt": "頭像預覽"
@@ -953,7 +953,7 @@
"description": "較大的模型較慢,但對於細微的自我更正與專業詞彙處理得更好。", "description": "較大的模型較慢,但對於細微的自我更正與專業詞彙處理得更好。",
"size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}",
"size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}",
"size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}",
"tail": { "tail": {
"veryFast": "非常快", "veryFast": "非常快",
"fast": "快速", "fast": "快速",