0} ariaLabel={t('grid.refreshing')} />
{pullDistance > 0 && (
{
+ if (!Element.prototype.scrollIntoView) {
+ Element.prototype.scrollIntoView = vi.fn() as any;
+ }
+});
+
+beforeEach(() => {
+ // The provider persists the last language; keep one case from leaking into the next.
+ window.localStorage.clear();
+});
+
+// Module constants: `I18nProvider` rebuilds its i18next instance whenever the
+// `config` object changes identity, so an inline literal would re-boot it on
+// every render.
+const DE = { defaultLanguage: 'de', detectBrowserLanguage: false };
+const EN = { defaultLanguage: 'en', detectBrowserLanguage: false };
+
+const OBJECT = 'duly_task';
+
+/** The first `find` answers at once; every later one stays in flight. */
+function makeDataSource() {
+ let calls = 0;
+ const find = vi.fn(() => {
+ calls += 1;
+ if (calls === 1) {
+ return Promise.resolve({ data: [{ id: 'r1', name: 'First row' }], total: 1, hasMore: false, pageSize: 50 });
+ }
+ return new Promise(() => {});
+ });
+ return {
+ find,
+ getObjectSchema: async (name: string) => ({
+ name,
+ fields: { id: { type: 'text' }, name: { type: 'text' } },
+ }),
+ } as any;
+}
+
+const SCHEMA: any = {
+ type: 'object-grid',
+ objectName: OBJECT,
+ columns: [{ field: 'name', label: 'Name' }],
+ pagination: { pageSize: 50 },
+};
+
+const indicator = () => screen.queryByTestId('refresh-indicator');
+
+async function readBarNameDuringReRead(config: typeof DE) {
+ const ds = makeDataSource();
+ render(
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByText('First row')).toBeInTheDocument());
+ expect(indicator(), 'no bar while nothing is in flight').toBeNull();
+ const before = ds.find.mock.calls.length;
+
+ await act(async () => {
+ notifyDataChanged({ objectName: OBJECT });
+ });
+ await waitFor(() => expect(ds.find.mock.calls.length).toBeGreaterThan(before));
+ await waitFor(() => expect(indicator()).not.toBeNull());
+ // The rows on screen stay while the re-read runs; the bar sits over them.
+ expect(screen.getByText('First row')).toBeInTheDocument();
+}
+
+describe('ObjectGrid names its refresh bar in the active locale (objectui#10580)', () => {
+ it('de: the bar is named by the de pack, not by an English literal', async () => {
+ await readBarNameDuringReRead(DE);
+ await waitFor(() => expect(screen.getByRole('progressbar', { name: 'Wird aktualisiert…' })).toBeInTheDocument());
+ expect(indicator()!.getAttribute('aria-label')).not.toMatch(/Refreshing/);
+ });
+
+ it('en control: the bar is named by the en pack value', async () => {
+ await readBarNameDuringReRead(EN);
+ await waitFor(() => expect(screen.getByRole('progressbar', { name: 'Refreshing…' })).toBeInTheDocument());
+ });
+});
diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx
index f4fb73b54a..f4dd609150 100644
--- a/packages/plugin-list/src/ListView.tsx
+++ b/packages/plugin-list/src/ListView.tsx
@@ -4980,7 +4980,7 @@ export const ListView = React.forwardRef(({
{/* Re-fetch indicator: thin top progress bar shown when refreshing
existing data (filter/sort/search change). Skipped during the
initial load — the full skeleton below handles that case. */}
- 0} />
+ 0} ariaLabel={t('list.refreshing')} />
{/* Empty state is rendered here ONLY for tabular/list-like views.
Structural views (kanban/calendar/gallery/gantt/timeline/map) own
their own empty rendering so their column/lane/grid structure
diff --git a/packages/plugin-list/src/__tests__/ListView.refreshIndicatorLocale-10580.test.tsx b/packages/plugin-list/src/__tests__/ListView.refreshIndicatorLocale-10580.test.tsx
new file mode 100644
index 0000000000..c06660caa5
--- /dev/null
+++ b/packages/plugin-list/src/__tests__/ListView.refreshIndicatorLocale-10580.test.tsx
@@ -0,0 +1,94 @@
+/**
+ * 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.
+ */
+
+/**
+ * objectui#10580 — the bar `ListView` draws over its rows while it re-reads
+ * them is named in the active locale.
+ *
+ * `RefreshIndicator` used to default its accessible name to the English
+ * literal "Refreshing", and this view passed none, so a screen reader
+ * announced English in every locale. The view now names the bar with
+ * `list.refreshing`, the key its pull-to-refresh text already reads, and the
+ * component has no default left to fall to.
+ *
+ * The re-read is the toolbar's own refresh button (`ListView.refreshButton.test.tsx`).
+ * The second `find` is held open by hand, so the bar is read while the query
+ * really is in flight.
+ */
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, waitFor, screen, fireEvent, act } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import React from 'react';
+import { I18nProvider } from '@object-ui/i18n';
+import { SchemaRendererProvider } from '@object-ui/react';
+import { ListView } from '../ListView';
+
+beforeEach(() => {
+ // The provider persists the last language; keep one case from leaking into the next.
+ window.localStorage.clear();
+});
+
+// Module constants: `I18nProvider` rebuilds its i18next instance whenever the
+// `config` object changes identity, so an inline literal would re-boot it on
+// every render.
+const ZH = { defaultLanguage: 'zh', detectBrowserLanguage: false };
+const EN = { defaultLanguage: 'en', detectBrowserLanguage: false };
+
+/** The first `find` answers at once; every later one stays in flight. */
+function makeDataSource() {
+ let calls = 0;
+ const find = vi.fn(() => {
+ calls += 1;
+ if (calls === 1) return Promise.resolve({ data: [{ _id: 'p1', name: 'Alpha' }], total: 1 });
+ return new Promise(() => {});
+ });
+ return {
+ find,
+ findOne: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ } as any;
+}
+
+const indicator = () => screen.queryByTestId('refresh-indicator');
+const settle = () => act(() => new Promise((resolve) => setTimeout(resolve, 100)));
+
+async function readBarNameDuringReRead(config: typeof ZH) {
+ const ds = makeDataSource();
+ render(
+
+
+
+
+ ,
+ );
+ await waitFor(() => expect(ds.find).toHaveBeenCalledTimes(1));
+ await settle();
+ expect(indicator(), 'no bar while nothing is in flight').toBeNull();
+
+ fireEvent.click(screen.getByTestId('refresh-button'));
+ await waitFor(() => expect(ds.find).toHaveBeenCalledTimes(2));
+ await waitFor(() => expect(indicator()).not.toBeNull());
+}
+
+describe('ListView names its refresh bar in the active locale (objectui#10580)', () => {
+ it('zh: the bar is named by the zh pack, not by an English literal', async () => {
+ await readBarNameDuringReRead(ZH);
+ await waitFor(() => expect(screen.getByRole('progressbar', { name: '刷新中…' })).toBeInTheDocument());
+ expect(indicator()!.getAttribute('aria-label')).not.toMatch(/Refreshing/);
+ });
+
+ it('en control: the bar is named by the en pack value', async () => {
+ await readBarNameDuringReRead(EN);
+ await waitFor(() => expect(screen.getByRole('progressbar', { name: 'Refreshing…' })).toBeInTheDocument());
+ });
+});