Skip to content

Commit a70aa00

Browse files
committed
test(plugin-grid): pin the bulk confirm sentence to the display locale
The bulk confirm sentence formats its numbers and dates in en-US for every viewer. This pin is red until BulkActionDialog passes the display locale through bulkConfirmText to fillTemplateHoles. Claude-Session: https://claude.ai/code/session_01SMps7wq7rBTk53e8KTSEup Co-authored-by: Claude <noreply@anthropic.com>
1 parent 61ba04f commit a70aa00

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): a bulk action's confirm sentence formats its numbers and dates in the viewer's display locale
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A bulk `confirmText` such as `Set the amount to {{ amount | currency:EUR }} on {{ count }} records?` formatted its numbers and dates in `en-US` for every viewer, while the same action's single-record confirm formats them in the viewer's language. `BulkActionDialog` now reads `useDisplayLocale()`, the locale every date, number and currency renderer and the console's action runners read, and `bulkConfirmText` takes it as an optional third argument, handed to `fillTemplateHoles`'s locale channel apart from the template roots. A German viewer reads `1.234.567,50 €`; with no locale handed, the sentence formats in `en-US` as before.
11+
12+
What to do: nothing.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#26307 — a bulk `confirmText` formats its numbers and dates in
11+
* the viewer's display locale, the one `useDisplayLocale()` resolves, the way
12+
* the same action's single-record confirm does since objectstack#26232. It
13+
* used to format them in `en-US` for every viewer.
14+
*
15+
* Two halves:
16+
* - the helper `bulkConfirmText`: its `locale` argument reaches the
17+
* formatters, and an absent one keeps `en-US`;
18+
* - the real `BulkActionDialog` under a real `I18nProvider` and
19+
* `LocalizationProvider`: the confirm sentence formats in the tenant's
20+
* display locale.
21+
*
22+
* Expected strings come from `Intl` itself, so a newer ICU cannot drift them.
23+
*/
24+
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
25+
import React from 'react';
26+
import { render, screen, cleanup, fireEvent } from '@testing-library/react';
27+
import '@testing-library/jest-dom';
28+
import { I18nProvider, LocalizationProvider } from '@object-ui/i18n';
29+
import type { BulkActionDef, BulkActionParam } from '@object-ui/types';
30+
31+
import { BulkActionDialog } from '../components/BulkActionDialog';
32+
import { bulkConfirmText } from '../components/bulkConfirmText';
33+
34+
beforeAll(() => {
35+
if (!Element.prototype.scrollIntoView) {
36+
Element.prototype.scrollIntoView = vi.fn() as any;
37+
}
38+
});
39+
40+
afterEach(() => cleanup());
41+
42+
const AMOUNT_VALUE = 1234567.5;
43+
const DUE_VALUE = '2026-10-26';
44+
const money = (tag: string) => new Intl.NumberFormat(tag, { style: 'currency', currency: 'EUR' }).format(AMOUNT_VALUE);
45+
const fixed2 = (tag: string) =>
46+
new Intl.NumberFormat(tag, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(AMOUNT_VALUE);
47+
const longDate = (tag: string) => new Intl.DateTimeFormat(tag, { dateStyle: 'long' }).format(new Date(DUE_VALUE));
48+
49+
const AMOUNT: BulkActionParam = { name: 'amount', label: 'Amount', type: 'number' };
50+
const DUE: BulkActionParam = { name: 'due', label: 'Due', type: 'date' };
51+
52+
function fill(source: string, locale?: string) {
53+
return bulkConfirmText(source, { count: 2, params: [AMOUNT, DUE], values: { amount: AMOUNT_VALUE, due: DUE_VALUE } }, locale);
54+
}
55+
56+
describe('bulkConfirmText formats in the locale it is handed (objectstack#26307)', () => {
57+
it('formats a currency, a number and a date hole in a German display locale', () => {
58+
expect(money('de-DE')).not.toBe(money('en-US'));
59+
expect(fill('Set {{ amount | currency:EUR }} on {{ count }} records?', 'de-DE')).toBe(`Set ${money('de-DE')} on 2 records?`);
60+
expect(fill('Set {{ amount | number:2 }}?', 'de-DE')).toBe(`Set ${fixed2('de-DE')}?`);
61+
expect(fill('Due {{ due | date:long }}?', 'de-DE')).toBe(`Due ${longDate('de-DE')}?`);
62+
});
63+
64+
it('formats a date hole in a Chinese display locale', () => {
65+
expect(longDate('zh-CN')).not.toBe(longDate('en-US'));
66+
expect(fill('Due {{ due | date:long }}?', 'zh-CN')).toBe(`Due ${longDate('zh-CN')}?`);
67+
});
68+
69+
it('keeps en-US when no locale is handed', () => {
70+
expect(fill('Set {{ amount | currency:EUR }}, due {{ due | date:long }}?')).toBe(
71+
`Set ${money('en-US')}, due ${longDate('en-US')}?`,
72+
);
73+
});
74+
});
75+
76+
const ROWS = [{ id: 'r1', name: 'Acme' }, { id: 'r2', name: 'Globex' }];
77+
78+
const REPRICE: BulkActionDef = {
79+
name: 'reprice',
80+
label: 'Reprice',
81+
operation: 'update',
82+
params: [{ ...AMOUNT, default: AMOUNT_VALUE }],
83+
confirmText: { dialect: 'template', source: 'Set the amount to {{ amount | currency:EUR }} on {{ count }} records?' },
84+
};
85+
86+
function openDialog(tenantLocale: string | undefined) {
87+
render(
88+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
89+
<LocalizationProvider value={{ locale: tenantLocale }}>
90+
<BulkActionDialog
91+
def={REPRICE}
92+
rows={ROWS}
93+
resource="account"
94+
dataSource={{ update: vi.fn(async () => ({})), delete: vi.fn(async () => ({})) } as any}
95+
open
96+
onClose={() => {}}
97+
/>
98+
</LocalizationProvider>
99+
</I18nProvider>,
100+
);
101+
}
102+
103+
describe('BulkActionDialog formats its confirm sentence in the display locale (objectstack#26307)', () => {
104+
it('formats a currency hole in the tenant display locale', async () => {
105+
openDialog('de-DE');
106+
fireEvent.click(await screen.findByRole('button', { name: 'Next' }));
107+
expect(await screen.findByText(`Set the amount to ${money('de-DE')} on 2 records?`)).toBeInTheDocument();
108+
});
109+
110+
it('formats in the UI language when no tenant locale is set', async () => {
111+
openDialog(undefined);
112+
fireEvent.click(await screen.findByRole('button', { name: 'Next' }));
113+
expect(await screen.findByText(`Set the amount to ${money('en')} on 2 records?`)).toBeInTheDocument();
114+
});
115+
});

0 commit comments

Comments
 (0)