Skip to content

Commit a0fe4ec

Browse files
committed
Support resource specific translations in DeleteButton
1 parent 997e3b5 commit a0fe4ec

File tree

7 files changed

+211
-24
lines changed

7 files changed

+211
-24
lines changed

docs/Buttons.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,6 @@ export const en = {
730730

731731
You can also customize this label by specifying a custom `label` prop:
732732

733-
734733
```jsx
735734
export const PostCreateButton = () => (
736735
<CreateButton label="New post" />
@@ -799,7 +798,7 @@ You can also call it with a record and a resource:
799798
| Prop | Required | Type | Default | Description |
800799
|-------------------- |----------|--------------------------------- |-------------------|-------------------------------------------------------------------------|
801800
| `className` | Optional | `string` | - | Class name to customize the look and feel of the button element itself |
802-
| `label` | Optional | `string` | 'Delete' | label or translation message to use |
801+
| `label` | Optional | `string` | - | label or translation message to use |
803802
| `icon` | Optional | `ReactElement` | `<DeleteIcon>` | iconElement, e.g. `<CommentIcon />` |
804803
| `mutationMode` | Optional | `string` | `'undoable'` | Mutation mode (`'undoable'`, `'pessimistic'` or `'optimistic'`) |
805804
| `mutation Options` | Optional | | null | options for react-query `useMutation` hook |
@@ -813,7 +812,27 @@ You can also call it with a record and a resource:
813812

814813
By default, the label is `Delete` in English. In other languages, it's the translation of the `'ra.action.delete'` key.
815814

816-
To customize the `<DeleteButton>` label, you can either change the translation in your i18nProvider, or pass a `label` prop:
815+
You can customize this label by providing a resource specific translation with the key `resources.RESOURCE.action.delete` (e.g. `resources.posts.action.delete`):
816+
817+
```js
818+
// in src/i18n/en.js
819+
import englishMessages from 'ra-language-english';
820+
821+
export const en = {
822+
...englishMessages,
823+
resources: {
824+
posts: {
825+
name: 'Post |||| Posts',
826+
action: {
827+
delete: 'Destroy %{recordRepresentation}'
828+
}
829+
},
830+
},
831+
...
832+
};
833+
```
834+
835+
You can also customize this label by specifying a custom `label` prop:
817836

818837
```jsx
819838
<DeleteButton label="Delete this comment" />

packages/ra-ui-materialui/src/button/DeleteButton.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,26 @@ import {
55
NotificationDefault,
66
NotificationTranslated,
77
FullApp,
8+
Label,
89
} from './DeleteButton.stories';
910

1011
describe('<DeleteButton />', () => {
12+
it('should provide a default label', async () => {
13+
render(<Label translations="default" />);
14+
await screen.findByText('Delete');
15+
fireEvent.click(screen.getByText('English', { selector: 'button' }));
16+
fireEvent.click(await screen.findByText('Français'));
17+
await screen.findByText('Supprimer');
18+
});
19+
20+
it('should allow resource specific default title', async () => {
21+
render(<Label translations="resource specific" />);
22+
await screen.findByText('Destroy War and Peace');
23+
fireEvent.click(screen.getByText('English', { selector: 'button' }));
24+
fireEvent.click(await screen.findByText('Français'));
25+
await screen.findByText('Détruire War and Peace');
26+
});
27+
1128
it('should only render when users have the right to delete', async () => {
1229
render(<FullApp />);
1330
await screen.findByText('War and Peace');

packages/ra-ui-materialui/src/button/DeleteButton.stories.tsx

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { QueryClient } from '@tanstack/react-query';
77
import fakeRestDataProvider from 'ra-data-fakerest';
88
import {
99
AuthProvider,
10+
I18nProvider,
11+
memoryStore,
12+
mergeTranslations,
13+
MutationMode,
14+
RecordContextProvider,
1015
Resource,
1116
ResourceContextProvider,
1217
TestMemoryRouter,
@@ -18,6 +23,9 @@ import { Datagrid } from '../list/datagrid/Datagrid';
1823
import { TextField } from '../field/TextField';
1924
import { AdminUI } from '../AdminUI';
2025
import { Notification } from '../layout';
26+
import { LocalesMenuButton } from './LocalesMenuButton';
27+
28+
export default { title: 'ra-ui-materialui/button/DeleteButton' };
2129

2230
const theme = createTheme({
2331
palette: {
@@ -32,12 +40,43 @@ const theme = createTheme({
3240
},
3341
});
3442

35-
const i18nProvider = polyglotI18nProvider(
36-
locale => (locale === 'fr' ? frenchMessages : englishMessages),
37-
'en'
38-
);
43+
const defaultI18nProvider = () =>
44+
polyglotI18nProvider(
45+
locale => (locale === 'fr' ? frenchMessages : englishMessages),
46+
'en',
47+
[
48+
{ locale: 'en', name: 'English' },
49+
{ locale: 'fr', name: 'Français' },
50+
]
51+
);
3952

40-
export default { title: 'ra-ui-materialui/button/DeleteButton' };
53+
const customI18nProvider = polyglotI18nProvider(
54+
locale =>
55+
locale === 'fr'
56+
? mergeTranslations(frenchMessages, {
57+
resources: {
58+
books: {
59+
action: {
60+
delete: 'Détruire %{recordRepresentation}',
61+
},
62+
},
63+
},
64+
})
65+
: mergeTranslations(englishMessages, {
66+
resources: {
67+
books: {
68+
action: {
69+
delete: 'Destroy %{recordRepresentation}',
70+
},
71+
},
72+
},
73+
}),
74+
'en',
75+
[
76+
{ locale: 'en', name: 'English' },
77+
{ locale: 'fr', name: 'Français' },
78+
]
79+
);
4180

4281
export const Basic = () => (
4382
<AdminContext>
@@ -48,7 +87,7 @@ export const Basic = () => (
4887
);
4988

5089
export const Pessimistic = () => (
51-
<AdminContext i18nProvider={i18nProvider}>
90+
<AdminContext i18nProvider={defaultI18nProvider()}>
5291
<ResourceContextProvider value="posts">
5392
<DeleteButton
5493
mutationMode="pessimistic"
@@ -60,7 +99,7 @@ export const Pessimistic = () => (
6099
);
61100

62101
export const PessimisticWithCustomDialogContent = () => (
63-
<AdminContext i18nProvider={i18nProvider}>
102+
<AdminContext i18nProvider={defaultI18nProvider()}>
64103
<ResourceContextProvider value="posts">
65104
<DeleteButton
66105
mutationMode="pessimistic"
@@ -102,6 +141,53 @@ export const ContainedWithUserDefinedPalette = () => (
102141
</AdminContext>
103142
);
104143

144+
export const Label = ({
145+
mutationMode = 'undoable',
146+
translations = 'default',
147+
i18nProvider = translations === 'default'
148+
? defaultI18nProvider()
149+
: customI18nProvider,
150+
label,
151+
}: {
152+
mutationMode?: MutationMode;
153+
i18nProvider?: I18nProvider;
154+
translations?: 'default' | 'resource specific';
155+
label?: string;
156+
}) => (
157+
<TestMemoryRouter>
158+
<AdminContext i18nProvider={i18nProvider} store={memoryStore()}>
159+
<ResourceContextProvider value="books">
160+
<RecordContextProvider
161+
value={{ id: 1, title: 'War and Peace' }}
162+
>
163+
<div>
164+
<DeleteButton
165+
label={label}
166+
mutationMode={mutationMode}
167+
/>
168+
</div>
169+
</RecordContextProvider>
170+
<LocalesMenuButton />
171+
</ResourceContextProvider>
172+
</AdminContext>
173+
</TestMemoryRouter>
174+
);
175+
176+
Label.args = {
177+
mutationMode: 'undoable',
178+
translations: 'default',
179+
};
180+
Label.argTypes = {
181+
mutationMode: {
182+
options: ['undoable', 'optimistic', 'pessimistic'],
183+
control: { type: 'select' },
184+
},
185+
translations: {
186+
options: ['default', 'resource specific'],
187+
control: { type: 'radio' },
188+
},
189+
};
190+
105191
export const FullApp = () => {
106192
const queryClient = new QueryClient();
107193

@@ -314,7 +400,10 @@ export const InList = ({ mutationMode }) => {
314400
process.env.NODE_ENV === 'development' ? 500 : 0
315401
);
316402
return (
317-
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>
403+
<AdminContext
404+
dataProvider={dataProvider}
405+
i18nProvider={defaultI18nProvider()}
406+
>
318407
<AdminUI>
319408
<Resource
320409
name="books"
@@ -340,7 +429,10 @@ export const NotificationDefault = () => {
340429
delete: () => Promise.resolve({ data: { id: 1 } }),
341430
} as any;
342431
return (
343-
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>
432+
<AdminContext
433+
dataProvider={dataProvider}
434+
i18nProvider={defaultI18nProvider()}
435+
>
344436
<DeleteButton record={{ id: 1 }} resource="books" />
345437
<Notification />
346438
</AdminContext>

packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.spec.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ describe('<DeleteWithConfirmButton />', () => {
4949

5050
expect(spy).not.toHaveBeenCalled();
5151
expect(
52-
screen.getByLabelText('ra.action.delete').getAttribute('type')
52+
screen
53+
.getByLabelText('resources.posts.action.delete')
54+
.getAttribute('type')
5355
).toEqual('button');
5456

5557
spy.mockRestore();
@@ -92,7 +94,9 @@ describe('<DeleteWithConfirmButton />', () => {
9294
await waitFor(() => {
9395
expect(screen.queryByDisplayValue('lorem')).not.toBeNull();
9496
});
95-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
97+
fireEvent.click(
98+
await screen.findByLabelText('resources.comments.action.delete')
99+
);
96100
fireEvent.click(screen.getByText('ra.action.confirm'));
97101
await waitFor(() => {
98102
expect(dataProvider.delete).toHaveBeenCalledWith('comments', {
@@ -134,7 +138,9 @@ describe('<DeleteWithConfirmButton />', () => {
134138
await waitFor(() => {
135139
expect(screen.queryByDisplayValue('lorem')).not.toBeNull();
136140
});
137-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
141+
fireEvent.click(
142+
await screen.findByLabelText('resources.posts.action.delete')
143+
);
138144
fireEvent.click(screen.getByText('ra.action.confirm'));
139145

140146
await waitFor(() => {
@@ -175,7 +181,9 @@ describe('<DeleteWithConfirmButton />', () => {
175181
await waitFor(() => {
176182
expect(screen.queryByDisplayValue('lorem')).not.toBeNull();
177183
});
178-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
184+
fireEvent.click(
185+
await screen.findByLabelText('resources.posts.action.delete')
186+
);
179187
fireEvent.click(screen.getByText('ra.action.confirm'));
180188
await waitFor(() => {
181189
expect(dataProvider.delete).toHaveBeenCalled();
@@ -222,7 +230,9 @@ describe('<DeleteWithConfirmButton />', () => {
222230
await waitFor(() => {
223231
expect(screen.queryByDisplayValue('lorem')).toBeDefined();
224232
});
225-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
233+
fireEvent.click(
234+
await screen.findByLabelText('resources.posts.action.delete')
235+
);
226236
fireEvent.click(screen.getByText('ra.action.confirm'));
227237
await waitFor(() => {
228238
expect(dataProvider.delete).toHaveBeenCalled();
@@ -274,7 +284,9 @@ describe('<DeleteWithConfirmButton />', () => {
274284
expect(screen.queryByDisplayValue('lorem')).toBeDefined();
275285
});
276286

277-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
287+
fireEvent.click(
288+
await screen.findByLabelText('resources.posts.action.delete')
289+
);
278290
expect(screen.queryByDisplayValue('#20061703')).toBeDefined();
279291
});
280292

@@ -322,7 +334,9 @@ describe('<DeleteWithConfirmButton />', () => {
322334
await waitFor(() => {
323335
expect(screen.queryByDisplayValue('lorem')).not.toBeNull();
324336
});
325-
fireEvent.click(await screen.findByLabelText('ra.action.delete'));
337+
fireEvent.click(
338+
await screen.findByLabelText('resources.comments.action.delete')
339+
);
326340
fireEvent.click(screen.getByText('ra.action.confirm'));
327341
await waitFor(() => {
328342
expect(notificationsSpy).toEqual([

packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
useTranslate,
1414
RedirectionSideEffect,
1515
useGetRecordRepresentation,
16+
useGetResourceLabel,
1617
} from 'ra-core';
1718

1819
import { Confirm } from '../layout';
@@ -28,7 +29,7 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
2829
confirmContent: confirmContentProp,
2930
confirmColor = 'primary',
3031
icon = defaultIcon,
31-
label = 'ra.action.delete',
32+
label: labelProp,
3233
mutationMode = 'pessimistic',
3334
onClick,
3435
redirect = 'list',
@@ -43,6 +44,11 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
4344
const translate = useTranslate();
4445
const record = useRecordContext(props);
4546
const resource = useResourceContext(props);
47+
if (!resource) {
48+
throw new Error(
49+
'<DeleteWithConfirmButton> components should be used inside a <Resource> component or provided with a resource prop. (The <Resource> component set the resource prop for all its children).'
50+
);
51+
}
4652

4753
const {
4854
open,
@@ -59,6 +65,7 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
5965
resource,
6066
successMessage,
6167
});
68+
const getResourceLabel = useGetResourceLabel();
6269
const getRecordRepresentation = useGetRecordRepresentation(resource);
6370
let recordRepresentation = getRecordRepresentation(record);
6471
let confirmTitle = `resources.${resource}.message.delete_title`;
@@ -77,6 +84,15 @@ export const DeleteWithConfirmButton = <RecordType extends RaRecord = any>(
7784
if (isValidElement(recordRepresentation)) {
7885
recordRepresentation = `#${record?.id}`;
7986
}
87+
const label =
88+
labelProp ??
89+
translate(`resources.${resource}.action.delete`, {
90+
recordRepresentation,
91+
_: translate(`ra.action.delete`, {
92+
name: getResourceLabel(resource, 1),
93+
recordRepresentation,
94+
}),
95+
});
8096

8197
return (
8298
<Fragment>

0 commit comments

Comments
 (0)