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