From 70ec8d995b16a61620496ffbf5ff38b16cba586d Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 25 Apr 2026 01:27:06 -0700 Subject: [PATCH] fix: i18n cleanup + readiness checklist effect cadence + ChordPicker shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../DictationReadinessChecklist.tsx | 18 ++++++++++++------ app/src/components/ChordPicker/ChordPicker.tsx | 4 ++-- app/src/components/ServerTab/CapturesPage.tsx | 2 +- app/src/i18n/locales/en/translation.json | 6 +++--- app/src/i18n/locales/ja/translation.json | 6 +++--- app/src/i18n/locales/zh-CN/translation.json | 10 +++++----- app/src/i18n/locales/zh-TW/translation.json | 6 +++--- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/components/CapturesTab/DictationReadinessChecklist.tsx b/app/src/components/CapturesTab/DictationReadinessChecklist.tsx index 08e1db7d..136f1f58 100644 --- a/app/src/components/CapturesTab/DictationReadinessChecklist.tsx +++ b/app/src/components/CapturesTab/DictationReadinessChecklist.tsx @@ -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(); - 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(); + 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 }) => diff --git a/app/src/components/ChordPicker/ChordPicker.tsx b/app/src/components/ChordPicker/ChordPicker.tsx index 23150e23..f4ca88d0 100644 --- a/app/src/components/ChordPicker/ChordPicker.tsx +++ b/app/src/components/ChordPicker/ChordPicker.tsx @@ -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]); diff --git a/app/src/components/ServerTab/CapturesPage.tsx b/app/src/components/ServerTab/CapturesPage.tsx index b11aaaa4..9c903ee9 100644 --- a/app/src/components/ServerTab/CapturesPage.tsx +++ b/app/src/components/ServerTab/CapturesPage.tsx @@ -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.size4', { tail: t('settings.captures.refinement.model.tail.fullQuality') })} + {t('settings.captures.refinement.model.size40', { tail: t('settings.captures.refinement.model.tail.fullQuality') })} diff --git a/app/src/i18n/locales/en/translation.json b/app/src/i18n/locales/en/translation.json index 53bcef4f..5eafe63f 100644 --- a/app/src/i18n/locales/en/translation.json +++ b/app/src/i18n/locales/en/translation.json @@ -271,8 +271,8 @@ "defaultEffects": "Default Effects", "defaultEffectsHint": "Effects applied automatically to all new generations with this voice.", "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.", - "personalityHint": "Leave blank to hide the Compose button and persona toggle on the generate page." + "personalityPlaceholder": "e.g. \"a grumpy pirate who only speaks in nautical metaphors\"", + "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": { "alt": "Avatar preview" @@ -953,7 +953,7 @@ "description": "Larger models are slower but handle subtle self-corrections and technical vocabulary better.", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", - "size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", "tail": { "veryFast": "Very fast", "fast": "Fast", diff --git a/app/src/i18n/locales/ja/translation.json b/app/src/i18n/locales/ja/translation.json index f9531531..fffe000e 100644 --- a/app/src/i18n/locales/ja/translation.json +++ b/app/src/i18n/locales/ja/translation.json @@ -271,8 +271,8 @@ "defaultEffects": "デフォルトエフェクト", "defaultEffectsHint": "このボイスで新しく生成するすべてのものに自動適用されるエフェクトです。", "personalityLabel": "パーソナリティ", - "personalityPlaceholder": "任意。このボイスがどんな人物で、どのように話すかを記述します。例:「航海の比喩でしか話さない不機嫌な海賊」。生成ページの「Compose」ボタンと、キャラクターになりきって書き換えるトグルに反映されます。", - "personalityHint": "空欄にすると、生成ページの「Compose」ボタンとペルソナトグルは表示されません。" + "personalityPlaceholder": "例:「航海の比喩でしか話さない不機嫌な海賊」", + "personalityHint": "このボイスがどんな人物で、どのように話すか。生成ページの「Compose」ボタンとキャラクター書き換えトグルに反映されます。空欄にすると両方とも表示されません。" }, "avatar": { "alt": "アバタープレビュー" @@ -953,7 +953,7 @@ "description": "大きなモデルは遅くなりますが、微妙な自己修正や専門用語をより適切に処理します。", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", - "size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", "tail": { "veryFast": "超高速", "fast": "高速", diff --git a/app/src/i18n/locales/zh-CN/translation.json b/app/src/i18n/locales/zh-CN/translation.json index 4df9eadc..a069c8a7 100644 --- a/app/src/i18n/locales/zh-CN/translation.json +++ b/app/src/i18n/locales/zh-CN/translation.json @@ -271,8 +271,8 @@ "defaultEffects": "默认效果", "defaultEffectsHint": "自动应用于使用此声音的所有新生成的效果。", "personalityLabel": "人物设定", - "personalityPlaceholder": "可选。这个声音是谁、说话方式如何。例如「一位脾气暴躁的海盗,只会用航海比喻说话」。会驱动生成页面上的「撰写」按钮和入戏改写开关。", - "personalityHint": "留空将隐藏生成页面上的「撰写」按钮和人物设定开关。" + "personalityPlaceholder": "例如:「一位脾气暴躁的海盗,只会用航海比喻说话」", + "personalityHint": "这个声音是谁、说话方式如何。会驱动生成页面上的「撰写」按钮和入戏改写开关。留空则两者都隐藏。" }, "avatar": { "alt": "头像预览" @@ -708,9 +708,9 @@ "failedDescription": "无法根据此人物设定生成文本。" }, "persona": { - "tooltipActive": "以人物设定朗读", + "tooltipActive": "正以人物设定朗读", "tooltipInactive": "以人物设定朗读", - "ariaLabelActive": "以人物设定朗读", + "ariaLabelActive": "正以人物设定朗读", "ariaLabelInactive": "以人物设定朗读" } }, @@ -953,7 +953,7 @@ "description": "更大的模型速度较慢,但能更好地处理细微的自我纠正和技术词汇。", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", - "size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", "tail": { "veryFast": "非常快", "fast": "快速", diff --git a/app/src/i18n/locales/zh-TW/translation.json b/app/src/i18n/locales/zh-TW/translation.json index c7388ae1..11155099 100644 --- a/app/src/i18n/locales/zh-TW/translation.json +++ b/app/src/i18n/locales/zh-TW/translation.json @@ -271,8 +271,8 @@ "defaultEffects": "預設效果", "defaultEffectsHint": "自動套用於使用此聲音所有新生成的效果。", "personalityLabel": "個性", - "personalityPlaceholder": "選填。描述這個聲音是誰以及他們如何說話。例如:「一位脾氣暴躁的海盜,只會用航海比喻說話」。會驅動生成頁面上的「撰寫」按鈕和角色化重寫切換。", - "personalityHint": "留空則在生成頁面隱藏「撰寫」按鈕和角色切換。" + "personalityPlaceholder": "例如:「一位脾氣暴躁的海盜,只會用航海比喻說話」", + "personalityHint": "這個聲音是誰以及他們如何說話。會驅動生成頁面上的「撰寫」按鈕和角色化重寫切換。留空則兩者都隱藏。" }, "avatar": { "alt": "頭像預覽" @@ -953,7 +953,7 @@ "description": "較大的模型較慢,但對於細微的自我更正與專業詞彙處理得更好。", "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", - "size4": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", "tail": { "veryFast": "非常快", "fast": "快速",