fix(help): render the help dialog from i18n instead of hardcoded English (#647)

The help dialog carried its 13 shortcut labels, its getting-started
paragraph and its version line as hardcoded English, while fully
translated strings for exactly those labels sat unused in all 21 locale
files. Every non-English user read English there. The translations did
not need writing, only reading: t.help.keyboardShortcuts already had
focusSearchBar, goToTools, processFile and the rest, in every locale.

Labels now index into t.help.keyboardShortcuts by key rather than
carrying text. Getting-started reads t.help.gettingStarted.description,
which drops the inline Kbd chip the hardcoded copy had, matching what all
21 locales already say. The version line goes through
t.help.versionLabel.

Also adds the type-to-search row that #644 left out, keyed
help.keyboardShortcuts.typeToSearch, translated into all 21 locales, and
regenerates the two darwin help-dialog baselines for the extra row.

Nothing caught the original bug because the i18n context defaults to en,
so asserting on English text passes whether or not the component reads
i18n at all. The new test mocks the context with sentinel values instead:
putting the hardcoded labels back fails 15 of its 19 cases.

Verified: 19 new unit tests, full unit suite 7576 passed, help-dialog
visual 3/3 against regenerated baselines, help accessibility e2e 7/7,
typecheck and lint clean, all 18 CI checks green.
This commit is contained in:
SnapOtter
2026-07-26 10:28:02 +08:00
committed by GitHub
parent 2d8b57c57f
commit 2848dd0e53
25 changed files with 164 additions and 19 deletions
+24 -19
View File
@@ -4,27 +4,32 @@ import { useEffect, useRef } from "react";
import { useTranslation } from "@/contexts/i18n-context"; import { useTranslation } from "@/contexts/i18n-context";
import { useFocusTrap } from "@/hooks/use-focus-trap"; import { useFocusTrap } from "@/hooks/use-focus-trap";
import { formatShortcut } from "@/hooks/use-keyboard-shortcuts"; import { formatShortcut } from "@/hooks/use-keyboard-shortcuts";
import { format } from "@/lib/format.js";
interface HelpDialogProps { interface HelpDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }
// Labels index into t.help.keyboardShortcuts rather than carrying English text,
// which is what the rest of the app does. The translations for these already
// existed in all 21 locales and simply were not being read.
const SHORTCUTS = [ const SHORTCUTS = [
{ keys: "mod+k", description: "Focus search bar" }, { keys: "mod+k", labelKey: "focusSearchBar" },
{ keys: "mod+/", description: "Go to tools" }, { keys: "a-z", labelKey: "typeToSearch" },
{ keys: "mod+enter", description: "Process file" }, { keys: "mod+/", labelKey: "goToTools" },
{ keys: "mod+s", description: "Download result" }, { keys: "mod+enter", labelKey: "processFile" },
{ keys: "mod+shift+d", description: "Toggle theme" }, { keys: "mod+s", labelKey: "downloadResult" },
{ keys: "mod+alt+1", description: "Go to Resize" }, { keys: "mod+shift+d", labelKey: "toggleTheme" },
{ keys: "mod+alt+2", description: "Go to Crop" }, { keys: "mod+alt+1", labelKey: "goToResize" },
{ keys: "mod+alt+3", description: "Go to Compress" }, { keys: "mod+alt+2", labelKey: "goToCrop" },
{ keys: "mod+alt+4", description: "Go to Convert" }, { keys: "mod+alt+3", labelKey: "goToCompress" },
{ keys: "mod+alt+5", description: "Go to Remove Background" }, { keys: "mod+alt+4", labelKey: "goToConvert" },
{ keys: "mod+alt+6", description: "Go to Watermark Text" }, { keys: "mod+alt+5", labelKey: "goToRemoveBackground" },
{ keys: "mod+alt+7", description: "Go to Strip Metadata" }, { keys: "mod+alt+6", labelKey: "goToWatermarkText" },
{ keys: "mod+alt+8", description: "Go to Image Info" }, { keys: "mod+alt+7", labelKey: "goToStripMetadata" },
]; { keys: "mod+alt+8", labelKey: "goToImageInfo" },
] as const;
export function HelpDialog({ open, onClose }: HelpDialogProps) { export function HelpDialog({ open, onClose }: HelpDialogProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -80,9 +85,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
<h3 className="text-sm font-semibold">{t.help.gettingStarted.heading}</h3> <h3 className="text-sm font-semibold">{t.help.gettingStarted.heading}</h3>
</div> </div>
<p className="text-sm text-muted-foreground leading-relaxed"> <p className="text-sm text-muted-foreground leading-relaxed">
Select a tool from the sidebar or search for one with <Kbd keys="mod+k" />. Upload a {t.help.gettingStarted.description}
file by dragging it onto the page or clicking the upload area. Adjust settings and
download your result.
</p> </p>
</section> </section>
@@ -100,7 +103,9 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
i !== SHORTCUTS.length - 1 ? "border-b border-border" : "" i !== SHORTCUTS.length - 1 ? "border-b border-border" : ""
}`} }`}
> >
<span className="text-muted-foreground">{s.description}</span> <span className="text-muted-foreground">
{t.help.keyboardShortcuts[s.labelKey]}
</span>
<Kbd keys={s.keys} /> <Kbd keys={s.keys} />
</div> </div>
))} ))}
@@ -155,7 +160,7 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
{/* Version */} {/* Version */}
<div className="text-xs text-muted-foreground pt-2 border-t border-border"> <div className="text-xs text-muted-foreground pt-2 border-t border-border">
SnapOtter v{APP_VERSION} {format(t.help.versionLabel, { version: APP_VERSION })}
</div> </div>
</div> </div>
</div> </div>
+1
View File
@@ -3932,6 +3932,7 @@ export const ar: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "اختصارات لوحة المفاتيح", heading: "اختصارات لوحة المفاتيح",
focusSearchBar: "التركيز على شريط البحث", focusSearchBar: "التركيز على شريط البحث",
typeToSearch: "ابدأ الكتابة للبحث",
goToTools: "الانتقال للأدوات", goToTools: "الانتقال للأدوات",
toggleTheme: "تبديل السمة", toggleTheme: "تبديل السمة",
goToResize: "الانتقال لتغيير الحجم", goToResize: "الانتقال لتغيير الحجم",
+1
View File
@@ -3988,6 +3988,7 @@ export const de: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Tastenkombinationen", heading: "Tastenkombinationen",
focusSearchBar: "Suchleiste fokussieren", focusSearchBar: "Suchleiste fokussieren",
typeToSearch: "Tippen, um zu suchen",
goToTools: "Zu Werkzeugen", goToTools: "Zu Werkzeugen",
toggleTheme: "Farbschema wechseln", toggleTheme: "Farbschema wechseln",
goToResize: "Zu Größe ändern", goToResize: "Zu Größe ändern",
+1
View File
@@ -3906,6 +3906,7 @@ export const en = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Keyboard Shortcuts", heading: "Keyboard Shortcuts",
focusSearchBar: "Focus search bar", focusSearchBar: "Focus search bar",
typeToSearch: "Start typing to search",
goToTools: "Go to tools", goToTools: "Go to tools",
toggleTheme: "Toggle theme", toggleTheme: "Toggle theme",
goToResize: "Go to Resize", goToResize: "Go to Resize",
+1
View File
@@ -3966,6 +3966,7 @@ export const es: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Atajos de teclado", heading: "Atajos de teclado",
focusSearchBar: "Enfocar barra de búsqueda", focusSearchBar: "Enfocar barra de búsqueda",
typeToSearch: "Empieza a escribir para buscar",
goToTools: "Ir a herramientas", goToTools: "Ir a herramientas",
toggleTheme: "Cambiar tema", toggleTheme: "Cambiar tema",
goToResize: "Ir a Redimensionar", goToResize: "Ir a Redimensionar",
+1
View File
@@ -3992,6 +3992,7 @@ export const fr: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Raccourcis clavier", heading: "Raccourcis clavier",
focusSearchBar: "Focus sur la barre de recherche", focusSearchBar: "Focus sur la barre de recherche",
typeToSearch: "Commencer à taper pour rechercher",
goToTools: "Aller aux outils", goToTools: "Aller aux outils",
toggleTheme: "Changer de thème", toggleTheme: "Changer de thème",
goToResize: "Aller à Redimensionner", goToResize: "Aller à Redimensionner",
+1
View File
@@ -3761,6 +3761,7 @@ export const hi: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "कीबोर्ड शॉर्टकट", heading: "कीबोर्ड शॉर्टकट",
focusSearchBar: "सर्च बार पर फोकस", focusSearchBar: "सर्च बार पर फोकस",
typeToSearch: "खोजने के लिए टाइप करें",
goToTools: "टूल्स पर जाएं", goToTools: "टूल्स पर जाएं",
toggleTheme: "थीम बदलें", toggleTheme: "थीम बदलें",
goToResize: "रीसाइज़ पर जाएं", goToResize: "रीसाइज़ पर जाएं",
+1
View File
@@ -3965,6 +3965,7 @@ export const id: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Pintasan Keyboard", heading: "Pintasan Keyboard",
focusSearchBar: "Fokus ke bilah pencarian", focusSearchBar: "Fokus ke bilah pencarian",
typeToSearch: "Mulai mengetik untuk mencari",
goToTools: "Ke halaman alat", goToTools: "Ke halaman alat",
toggleTheme: "Ganti tema", toggleTheme: "Ganti tema",
goToResize: "Ke Ubah Ukuran", goToResize: "Ke Ubah Ukuran",
+1
View File
@@ -3982,6 +3982,7 @@ export const it: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Scorciatoie da tastiera", heading: "Scorciatoie da tastiera",
focusSearchBar: "Focus sulla barra di ricerca", focusSearchBar: "Focus sulla barra di ricerca",
typeToSearch: "Inizia a digitare per cercare",
goToTools: "Vai agli strumenti", goToTools: "Vai agli strumenti",
toggleTheme: "Cambia tema", toggleTheme: "Cambia tema",
goToResize: "Vai a Ridimensiona", goToResize: "Vai a Ridimensiona",
+1
View File
@@ -3912,6 +3912,7 @@ export const ja: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "キーボードショートカット", heading: "キーボードショートカット",
focusSearchBar: "検索バーにフォーカス", focusSearchBar: "検索バーにフォーカス",
typeToSearch: "入力して検索",
goToTools: "ツールへ移動", goToTools: "ツールへ移動",
toggleTheme: "テーマ切り替え", toggleTheme: "テーマ切り替え",
goToResize: "リサイズへ移動", goToResize: "リサイズへ移動",
+1
View File
@@ -3891,6 +3891,7 @@ export const ko: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "키보드 단축키", heading: "키보드 단축키",
focusSearchBar: "검색바로 포커스", focusSearchBar: "검색바로 포커스",
typeToSearch: "입력하여 검색",
goToTools: "도구로 이동", goToTools: "도구로 이동",
toggleTheme: "테마 전환", toggleTheme: "테마 전환",
goToResize: "리사이즈로 이동", goToResize: "리사이즈로 이동",
+1
View File
@@ -3974,6 +3974,7 @@ export const nl: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Sneltoetsen", heading: "Sneltoetsen",
focusSearchBar: "Zoekbalk focussen", focusSearchBar: "Zoekbalk focussen",
typeToSearch: "Typen om te zoeken",
goToTools: "Naar gereedschap", goToTools: "Naar gereedschap",
toggleTheme: "Thema wisselen", toggleTheme: "Thema wisselen",
goToResize: "Naar Formaat wijzigen", goToResize: "Naar Formaat wijzigen",
+1
View File
@@ -3978,6 +3978,7 @@ export const pl: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Skróty klawiaturowe", heading: "Skróty klawiaturowe",
focusSearchBar: "Fokus na pasku wyszukiwania", focusSearchBar: "Fokus na pasku wyszukiwania",
typeToSearch: "Zacznij pisać, aby wyszukać",
goToTools: "Przejdź do narzędzi", goToTools: "Przejdź do narzędzi",
toggleTheme: "Przełącz motyw", toggleTheme: "Przełącz motyw",
goToResize: "Przejdź do zmiany rozmiaru", goToResize: "Przejdź do zmiany rozmiaru",
+1
View File
@@ -3973,6 +3973,7 @@ export const ptBR: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Atalhos de teclado", heading: "Atalhos de teclado",
focusSearchBar: "Focar na barra de busca", focusSearchBar: "Focar na barra de busca",
typeToSearch: "Comece a digitar para buscar",
goToTools: "Ir para ferramentas", goToTools: "Ir para ferramentas",
toggleTheme: "Alternar tema", toggleTheme: "Alternar tema",
goToResize: "Ir para Redimensionar", goToResize: "Ir para Redimensionar",
+1
View File
@@ -3970,6 +3970,7 @@ export const ru: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Горячие клавиши", heading: "Горячие клавиши",
focusSearchBar: "Фокус на строке поиска", focusSearchBar: "Фокус на строке поиска",
typeToSearch: "Начните вводить для поиска",
goToTools: "Перейти к инструментам", goToTools: "Перейти к инструментам",
toggleTheme: "Переключить тему", toggleTheme: "Переключить тему",
goToResize: "Перейти к изменению размера", goToResize: "Перейти к изменению размера",
+1
View File
@@ -3961,6 +3961,7 @@ export const sv: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Kortkommandon", heading: "Kortkommandon",
focusSearchBar: "Fokusera sökfält", focusSearchBar: "Fokusera sökfält",
typeToSearch: "Börja skriva för att söka",
goToTools: "Gå till verktyg", goToTools: "Gå till verktyg",
toggleTheme: "Växla tema", toggleTheme: "Växla tema",
goToResize: "Gå till Ändra storlek", goToResize: "Gå till Ändra storlek",
+1
View File
@@ -3910,6 +3910,7 @@ export const th: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "ปุ่มลัด", heading: "ปุ่มลัด",
focusSearchBar: "โฟกัสแถบค้นหา", focusSearchBar: "โฟกัสแถบค้นหา",
typeToSearch: "พิมพ์เพื่อค้นหา",
goToTools: "ไปที่เครื่องมือ", goToTools: "ไปที่เครื่องมือ",
toggleTheme: "สลับธีม", toggleTheme: "สลับธีม",
goToResize: "ไปที่ปรับขนาด", goToResize: "ไปที่ปรับขนาด",
+1
View File
@@ -3973,6 +3973,7 @@ export const tr: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Klavye Kısayolları", heading: "Klavye Kısayolları",
focusSearchBar: "Arama çubuğuna odaklan", focusSearchBar: "Arama çubuğuna odaklan",
typeToSearch: "Aramak için yazmaya başlayın",
goToTools: "Araçlara git", goToTools: "Araçlara git",
toggleTheme: "Temayı değiştir", toggleTheme: "Temayı değiştir",
goToResize: "Boyutlandırmaya git", goToResize: "Boyutlandırmaya git",
+1
View File
@@ -3968,6 +3968,7 @@ export const uk: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Гарячі клавіші", heading: "Гарячі клавіші",
focusSearchBar: "Фокус на рядку пошуку", focusSearchBar: "Фокус на рядку пошуку",
typeToSearch: "Почніть вводити для пошуку",
goToTools: "Перейти до інструментів", goToTools: "Перейти до інструментів",
toggleTheme: "Перемкнути тему", toggleTheme: "Перемкнути тему",
goToResize: "Перейти до зміни розміру", goToResize: "Перейти до зміни розміру",
+1
View File
@@ -3957,6 +3957,7 @@ export const vi: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "Phím tắt", heading: "Phím tắt",
focusSearchBar: "Lấy nét thanh tìm kiếm", focusSearchBar: "Lấy nét thanh tìm kiếm",
typeToSearch: "Bắt đầu nhập để tìm kiếm",
goToTools: "Đến công cụ", goToTools: "Đến công cụ",
toggleTheme: "Chuyển đổi giao diện", toggleTheme: "Chuyển đổi giao diện",
goToResize: "Đến Thay đổi kích thước", goToResize: "Đến Thay đổi kích thước",
+1
View File
@@ -3692,6 +3692,7 @@ export const zhCN: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "键盘快捷键", heading: "键盘快捷键",
focusSearchBar: "聚焦搜索栏", focusSearchBar: "聚焦搜索栏",
typeToSearch: "输入即可搜索",
goToTools: "前往工具", goToTools: "前往工具",
toggleTheme: "切换主题", toggleTheme: "切换主题",
goToResize: "前往调整大小", goToResize: "前往调整大小",
+1
View File
@@ -3694,6 +3694,7 @@ export const zhTW: TranslationKeys = {
keyboardShortcuts: { keyboardShortcuts: {
heading: "鍵盤快速鍵", heading: "鍵盤快速鍵",
focusSearchBar: "聚焦搜尋列", focusSearchBar: "聚焦搜尋列",
typeToSearch: "輸入即可搜尋",
goToTools: "前往工具", goToTools: "前往工具",
toggleTheme: "切換佈景主題", toggleTheme: "切換佈景主題",
goToResize: "前往調整大小", goToResize: "前往調整大小",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 135 KiB

+119
View File
@@ -0,0 +1,119 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { HelpDialog } from "@/components/help/help-dialog";
/**
* The help dialog once carried its shortcut labels and its getting-started copy
* as hardcoded English, while fully translated strings for exactly those labels
* sat unused in all 21 locale files. No test caught it, because the i18n context
* defaults to `en`: asserting on English text passes either way.
*
* So this mocks the context with sentinel values instead. Any label that goes
* back to hardcoded English stops rendering its sentinel and fails here.
*/
const SHORTCUT_KEYS = [
"focusSearchBar",
"typeToSearch",
"goToTools",
"processFile",
"downloadResult",
"toggleTheme",
"goToResize",
"goToCrop",
"goToCompress",
"goToConvert",
"goToRemoveBackground",
"goToWatermarkText",
"goToStripMetadata",
"goToImageInfo",
] as const;
vi.mock("@/contexts/i18n-context", () => ({
useTranslation: () => ({
locale: "xx",
setLocale: () => {},
t: {
a11y: { closeHelp: "SENTINEL_closeHelp" },
help: {
heading: "SENTINEL_heading",
gettingStarted: {
heading: "SENTINEL_gettingStartedHeading",
description: "SENTINEL_gettingStartedDescription",
},
keyboardShortcuts: {
heading: "SENTINEL_shortcutsHeading",
focusSearchBar: "SENTINEL_focusSearchBar",
typeToSearch: "SENTINEL_typeToSearch",
goToTools: "SENTINEL_goToTools",
processFile: "SENTINEL_processFile",
downloadResult: "SENTINEL_downloadResult",
toggleTheme: "SENTINEL_toggleTheme",
goToResize: "SENTINEL_goToResize",
goToCrop: "SENTINEL_goToCrop",
goToCompress: "SENTINEL_goToCompress",
goToConvert: "SENTINEL_goToConvert",
goToRemoveBackground: "SENTINEL_goToRemoveBackground",
goToWatermarkText: "SENTINEL_goToWatermarkText",
goToStripMetadata: "SENTINEL_goToStripMetadata",
goToImageInfo: "SENTINEL_goToImageInfo",
},
resources: {
heading: "SENTINEL_resourcesHeading",
githubLink: "SENTINEL_githubLink",
reportIssueLink: "SENTINEL_reportIssueLink",
docsLink: "SENTINEL_docsLink",
apiRefLink: "SENTINEL_apiRefLink",
},
versionLabel: "SENTINEL_version {version}",
},
},
}),
}));
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe("HelpDialog i18n wiring", () => {
it.each(SHORTCUT_KEYS)("renders the translated label for %s", (key) => {
render(<HelpDialog open={true} onClose={() => {}} />);
expect(screen.getByText(`SENTINEL_${key}`)).toBeDefined();
});
it("renders the translated getting-started copy", () => {
render(<HelpDialog open={true} onClose={() => {}} />);
expect(screen.getByText("SENTINEL_gettingStartedDescription")).toBeDefined();
});
it("renders the version through the translated label, not a hardcoded prefix", () => {
render(<HelpDialog open={true} onClose={() => {}} />);
expect(screen.getByText(/^SENTINEL_version /)).toBeDefined();
});
it("renders the translated section headings and close label", () => {
render(<HelpDialog open={true} onClose={() => {}} />);
expect(screen.getByText("SENTINEL_heading")).toBeDefined();
expect(screen.getByText("SENTINEL_shortcutsHeading")).toBeDefined();
expect(screen.getByText("SENTINEL_resourcesHeading")).toBeDefined();
expect(screen.getByLabelText("SENTINEL_closeHelp")).toBeDefined();
});
it("renders one row per shortcut and no others", () => {
render(<HelpDialog open={true} onClose={() => {}} />);
const rendered = SHORTCUT_KEYS.filter((k) => screen.queryByText(`SENTINEL_${k}`) !== null);
expect(rendered).toHaveLength(SHORTCUT_KEYS.length);
});
it("renders nothing when closed", () => {
const { container } = render(<HelpDialog open={false} onClose={() => {}} />);
expect(container.firstChild).toBeNull();
});
});