diff --git a/.changeset/chrome-a11y-names-i18n-5430.md b/.changeset/chrome-a11y-names-i18n-5430.md
new file mode 100644
index 0000000000..f8a499a940
--- /dev/null
+++ b/.changeset/chrome-a11y-names-i18n-5430.md
@@ -0,0 +1,29 @@
+---
+'@object-ui/components': patch
+'@object-ui/plugin-detail': patch
+'@object-ui/i18n': patch
+---
+
+Localize the last untranslated console-chrome accessible names (objectstack#5430)
+
+Four icon-only controls still carried hardcoded English accessible names, so
+under a non-English session they were the only English left in the record
+chrome — and because the controls have no visible label, that literal *is* the
+control to a screen reader and to the hover tooltip.
+
+- `page:header`'s `role="toolbar"` — now `detail.pageHeaderActions` (its `⋯`
+ overflow trigger eight lines below was fixed in #5407; the toolbar was missed)
+- `ReactionPicker`'s `role="listbox"` popup — now `detail.emojiPicker`
+- `ReactionPicker`'s per-reaction chip, which built its name by concatenation
+ with English pluralization baked in (`reaction${count !== 1 ? 's' : ''}`) —
+ now `detail.reactionCount` / `detail.reactionCountOne`
+- `NavigationOverlay`'s drawer close and split-panel close — now `common.close`
+ (the key the rest of the console already uses) and `common.closePanel`
+
+The pluralized label follows this repo's **two-key** convention
+(`detail.relatedRecords`/`relatedRecordOne`, `lookup.recordCount`/`recordCountOne`)
+rather than an i18next `_one`/`_other` pair: zh/ja/ko have no separate singular
+form, so those packs would legitimately omit the `_one` half and
+`all-locales-key-parity` would read that as a lost key.
+
+All five new keys are added to all ten locale packs.
diff --git a/packages/components/src/__tests__/navigation-overlay-close-i18n.test.tsx b/packages/components/src/__tests__/navigation-overlay-close-i18n.test.tsx
new file mode 100644
index 0000000000..c34f24dcc5
--- /dev/null
+++ b/packages/components/src/__tests__/navigation-overlay-close-i18n.test.tsx
@@ -0,0 +1,129 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * The record overlay's close affordances speak the session locale —
+ * objectstack#5430.
+ *
+ * `NavigationOverlay` had two hardcoded English accessible names:
+ * - drawer mode: the header `X` button (`aria-label`/`title` = "Close")
+ * - split mode: the detail panel's `X` button (`aria-label` = "Close panel")
+ *
+ * Both are icon-only, so the literal WAS the control to a screen reader and to
+ * the hover tooltip. They now read `common.close` / `common.closePanel`.
+ * `common.close` is deliberately the key the rest of the console already uses
+ * for a bare "Close" rather than a new overlay-private one.
+ */
+
+import { describe, it, expect, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import { I18nProvider } from '@object-ui/i18n';
+import { NavigationOverlay } from '../custom/navigation-overlay';
+
+const record = { _id: 'rec_1', name: 'Acme Corp' };
+
+function renderDrawerIn(language: string) {
+ return render(
+
+ {}}
+ setIsOpen={() => {}}
+ title="Acme Corp"
+ >
+ {(r) =>
}
+
+ ,
+ );
+}
+
+afterEach(() => cleanup());
+
+/**
+ * Addressing the drawer's close button: by `title`, NOT by role+name.
+ *
+ * The shadcn `Sheet` primitive auto-renders a close button of its own, whose
+ * only accessible name is a hardcoded English `sr-only` span
+ * (`packages/components/src/ui/sheet.tsx:80` — an upstream No-Touch zone,
+ * AGENTS.md #7). `NavigationOverlay` CSS-hides it with
+ * `[&>button:last-of-type]:hidden`, so a real browser drops it from the
+ * accessibility tree — but jsdom does not apply Tailwind, so RTL still sees it
+ * and `getByRole('button', { name: 'Close' })` matches two elements under `en`.
+ *
+ * Ours is the only close carrying a `title`, so that is the precise handle. The
+ * primitive's own untranslated label is a separate, out-of-scope finding.
+ */
+describe('NavigationOverlay drawer close — accessible name (objectstack#5430)', () => {
+ it('still reads English under an en session', () => {
+ renderDrawerIn('en');
+
+ expect(screen.getByTitle('Close').getAttribute('aria-label')).toBe('Close');
+ });
+
+ it('reads the zh bundle value under a zh session', () => {
+ renderDrawerIn('zh');
+
+ expect(screen.getByTitle('关闭').getAttribute('aria-label')).toBe('关闭');
+ expect(screen.getByRole('button', { name: '关闭' })).toBeTruthy();
+ // The literal this replaced, scoped to OUR button via `title` so the
+ // primitive's hidden English one cannot mask a re-inlined string here.
+ expect(screen.queryByTitle('Close')).toBeNull();
+ });
+
+ it('reads the de bundle value under a de session', () => {
+ renderDrawerIn('de');
+
+ expect(screen.getByTitle('Schließen').getAttribute('aria-label')).toBe('Schließen');
+ expect(screen.getByRole('button', { name: 'Schließen' })).toBeTruthy();
+ expect(screen.queryByTitle('Close')).toBeNull();
+ });
+});
+
+describe('NavigationOverlay split close panel — accessible name (objectstack#5430)', () => {
+ it('still reads English under an en session', () => {
+ renderSplitIn('en');
+
+ expect(screen.getByRole('button', { name: 'Close panel' })).toBeTruthy();
+ });
+
+ it('reads the zh bundle value under a zh session', () => {
+ renderSplitIn('zh');
+
+ expect(screen.getByRole('button', { name: '关闭面板' })).toBeTruthy();
+ expect(screen.queryByRole('button', { name: 'Close panel' })).toBeNull();
+ });
+
+ it('reads the ja bundle value under a ja session', () => {
+ renderSplitIn('ja');
+
+ expect(screen.getByRole('button', { name: 'パネルを閉じる' })).toBeTruthy();
+ expect(screen.queryByRole('button', { name: 'Close panel' })).toBeNull();
+ });
+});
diff --git a/packages/components/src/__tests__/page-header-toolbar-name-i18n.test.tsx b/packages/components/src/__tests__/page-header-toolbar-name-i18n.test.tsx
new file mode 100644
index 0000000000..4ba012d225
--- /dev/null
+++ b/packages/components/src/__tests__/page-header-toolbar-name-i18n.test.tsx
@@ -0,0 +1,81 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * The page header's action toolbar speaks the session locale —
+ * objectstack#5430.
+ *
+ * `page:header` wraps its action buttons in a `role="toolbar"` whose accessible
+ * name was the English literal "Page header actions". A toolbar has no visible
+ * label, so that literal IS the group as far as a screen reader is concerned:
+ * under a zh/ja session it announced the only English left in the header row.
+ * (#5407 fixed the `⋯` trigger eight lines below it and missed this one.)
+ *
+ * It now reads `detail.pageHeaderActions`.
+ */
+
+import { describe, it, expect, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import { ComponentRegistry } from '@object-ui/core';
+import { ActionProvider } from '@object-ui/react';
+import { I18nProvider } from '@object-ui/i18n';
+// Registers `page:header` at module scope, NOT inside a `beforeAll` — there the
+// cold transform is billed to `hookTimeout` (objectui#3010/#3021).
+import '../renderers';
+
+function PageHeader({ schema }: { schema: any }) {
+ const Component = ComponentRegistry.get('page:header');
+ if (!Component) throw new Error('page:header not registered');
+ // eslint-disable-next-line react-hooks/static-components -- ComponentRegistry.get returns a registered component (stable), not one created during render
+ return ;
+}
+
+const schema = {
+ type: 'page:header',
+ title: 'Acme Corp',
+ actions: [
+ { name: 'convert', locations: ['record_header'], label: 'Convert', type: 'flow' },
+ { name: 'clone', locations: ['record_header'], label: 'Clone', type: 'flow' },
+ ],
+};
+
+function renderHeaderIn(language: string) {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+afterEach(() => cleanup());
+
+describe('page:header action toolbar — accessible name (objectstack#5430)', () => {
+ it('still reads English under an en session', () => {
+ renderHeaderIn('en');
+
+ expect(screen.getByRole('toolbar', { name: 'Page header actions' })).toBeTruthy();
+ });
+
+ it('reads the zh bundle value under a zh session', () => {
+ renderHeaderIn('zh');
+
+ expect(screen.getByRole('toolbar', { name: '页面标题栏操作' })).toBeTruthy();
+ // The literal this replaced. Asserted negatively so a re-inlined English
+ // string cannot pass by rendering a second, untranslated toolbar.
+ expect(screen.queryByRole('toolbar', { name: 'Page header actions' })).toBeNull();
+ });
+
+ it('reads the ja bundle value under a ja session', () => {
+ renderHeaderIn('ja');
+
+ expect(screen.getByRole('toolbar', { name: 'ページヘッダーの操作' })).toBeTruthy();
+ expect(screen.queryByRole('toolbar', { name: 'Page header actions' })).toBeNull();
+ });
+});
diff --git a/packages/components/src/custom/navigation-overlay.tsx b/packages/components/src/custom/navigation-overlay.tsx
index ce8006bc4f..a5633728b5 100644
--- a/packages/components/src/custom/navigation-overlay.tsx
+++ b/packages/components/src/custom/navigation-overlay.tsx
@@ -66,6 +66,7 @@ import {
ResizableHandle,
} from './resizable';
import { usePopperAwareInteractOutside } from './mobile-dialog-content';
+import { useSafeTranslate } from '@object-ui/i18n';
/** Navigation mode type — matches ViewNavigationConfig.mode */
export type NavigationOverlayMode =
@@ -286,6 +287,10 @@ export const NavigationOverlay: React.FC = ({
// Inline-edit dropdowns render in body-level poppers; without this guard the
// click that closes an open dropdown also dismisses the drawer/modal (#2156).
const handleInteractOutside = usePopperAwareInteractOutside();
+ // Both close affordances below are icon-only, so their accessible name IS
+ // the control to a screen reader and to the hover tooltip (objectstack#5430).
+ // Must stay above the conditional returns — rules-of-hooks.
+ const tt = useSafeTranslate();
// Non-overlay modes don't render anything
if (mode === 'page' || mode === 'new_window' || mode === 'none') {
@@ -388,8 +393,8 @@ export const NavigationOverlay: React.FC = ({