diff --git a/CHANGELOG.md b/CHANGELOG.md
index b031ba16f..c0318b599 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
## Unreleased
+### Ctrl+B shows and hides the sidebar on a layout that does not write Latin letters
+
+The sidebar toggle's tooltip names Ctrl+B, or ⌘B on a Mac, and the shortcut was recognised by the
+character the key writes. On a Russian or Greek layout the B key writes "и" or "β", so the shortcut
+did nothing there. It is now read the way the app's own shortcuts read it since Shift+N was fixed
+for the same layouts: from the physical key when the layout writes a character outside ASCII there.
### Scrolling a Bot's browser no longer scrolls or zooms the page around it
While somebody drives a Bot's browser, a turn of the mouse wheel over its screen is sent to it, and
diff --git a/app/src/components/ui/sidebar.tsx b/app/src/components/ui/sidebar.tsx
index a9d79fb63..9e927cf9a 100644
--- a/app/src/components/ui/sidebar.tsx
+++ b/app/src/components/ui/sidebar.tsx
@@ -6,6 +6,7 @@ import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import { useIsMobile } from "@/hooks/use-mobile";
+import { keyOf } from "@/lib/hotkeys/hotkeys";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -112,8 +113,10 @@ function SidebarProvider({
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
+ // `keyOf` rather than `key`, as the app's own shortcuts read it: on a layout that writes
+ // another script the B key writes "и" or "β", and only its `code` still says B.
if (
- event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ keyOf(event) === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
diff --git a/app/tests/sidebar-shortcut-layouts.test.tsx b/app/tests/sidebar-shortcut-layouts.test.tsx
new file mode 100644
index 000000000..52713d12f
--- /dev/null
+++ b/app/tests/sidebar-shortcut-layouts.test.tsx
@@ -0,0 +1,80 @@
+import { afterAll, afterEach, beforeAll, expect, test } from "bun:test";
+import { GlobalRegistrator } from "@happy-dom/global-registrator";
+import { act, cleanup, render } from "@testing-library/react";
+import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
+
+/**
+ * The sidebar shortcut on a keyboard layout that does not write Latin letters.
+ *
+ * The toggle's tooltip names it as Ctrl+B, or ⌘B on a Mac, and the sidebar's listener compared
+ * `KeyboardEvent.key` with "b". On Russian the B key writes "и", and on Greek it writes "β", so the
+ * shortcut the tooltip names never fired for anybody with one of those layouts selected. It is the
+ * same miss Shift+N and the paste shortcut on a Bot's screen had.
+ */
+
+beforeAll(() => {
+ GlobalRegistrator.register();
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+afterAll(() => {
+ GlobalRegistrator.unregister();
+});
+
+function SidebarState() {
+ return ;
+}
+
+type Modifiers = Partial>;
+
+/**
+ * A sidebar, and a way to press a key at it that answers with the state it is left in.
+ *
+ * The keydown is the one a browser reports: `key` from the layout, `code` from the physical key.
+ */
+function renderSidebar() {
+ const { container } = render(
+
+
+ ,
+ );
+ return (key: string, code: string, modifiers: Modifiers) => {
+ act(() => {
+ window.dispatchEvent(
+ new KeyboardEvent("keydown", {
+ key,
+ code,
+ ...modifiers,
+ bubbles: true,
+ cancelable: true,
+ }),
+ );
+ });
+ return container.querySelector("output")?.textContent;
+ };
+}
+
+test("Ctrl+B and Cmd+B toggle the sidebar on a layout that writes another script", () => {
+ const press = renderSidebar();
+
+ expect(press("и", "KeyB", { ctrlKey: true })).toBe("closed");
+ expect(press("β", "KeyB", { metaKey: true })).toBe("open");
+});
+
+test("a layout that writes Latin letters still goes by the letter, wherever its key is", () => {
+ const press = renderSidebar();
+
+ // Dvorak writes B on the key QWERTY calls N, and X on the key QWERTY calls B.
+ expect(press("b", "KeyN", { ctrlKey: true })).toBe("closed");
+ expect(press("x", "KeyB", { ctrlKey: true })).toBe("closed");
+});
+
+test("another letter, or the B key without its modifier, still does nothing", () => {
+ const press = renderSidebar();
+
+ expect(press("т", "KeyN", { ctrlKey: true })).toBe("open");
+ expect(press("и", "KeyB", {})).toBe("open");
+});