diff --git a/packages/ra-core/src/core/SourceContext.tsx b/packages/ra-core/src/core/SourceContext.tsx index ec32286518c..404549bd78d 100644 --- a/packages/ra-core/src/core/SourceContext.tsx +++ b/packages/ra-core/src/core/SourceContext.tsx @@ -30,15 +30,21 @@ export type SourceContextValue = { * ); * }; */ -export const SourceContext = createContext({ +export const SourceContext = createContext( + undefined +); + +const defaultContextValue = { getSource: (source: string) => source, getLabel: (source: string) => source, -}); - +}; export const SourceContextProvider = SourceContext.Provider; export const useSourceContext = () => { const context = useContext(SourceContext); + if (!context) { + return defaultContextValue; + } return context; }; diff --git a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx index a863f289493..63f1699f339 100644 --- a/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx +++ b/packages/ra-core/src/i18n/useTranslateLabel.spec.tsx @@ -1,177 +1,95 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { useTranslateLabel } from './useTranslateLabel'; -import { TestTranslationProvider } from './TestTranslationProvider'; -import { SourceContextProvider } from '..'; + +import { + Basic, + I18nLabelAsKey, + I18nNoTranslation, + I18nTranslation, + InSourceContext, + InSourceContextI18nKey, + InSourceContextNoTranslation, + InSourceContextWithResource, + LabelElement, + LabelEmpty, + LabelFalse, + LabelText, + Resource, + Source, +} from './useTranslateLabel.stories'; describe('useTranslateLabel', () => { - const TranslateLabel = ({ - source, - label, - resource, - }: { - source?: string; - label?: string | false | React.ReactElement; - resource?: string; - }) => { - const translateLabel = useTranslateLabel(); - return ( - <> - {translateLabel({ - label, - source, - resource, - })} - - ); - }; + it('should compose a translation key from the resource and source', () => { + render(); + screen.getByText('resources.posts.fields.title'); + }); + + it('should use the resource in the translation key', () => { + render(); + screen.getByText('resources.comments.fields.title'); + }); + + it('should use the source in the translation key', () => { + render(); + screen.getByText('resources.posts.fields.date'); + }); it('should return null when label is false', () => { - render( - - - - ); + render(); expect(screen.queryByText(/title/)).toBeNull(); }); it('should return null when label is empty', () => { - render( - - - - ); + render(); expect(screen.queryByText(/title/)).toBeNull(); }); it('should return the label element when provided', () => { - render( - - My title} - source="title" - resource="posts" - /> - - ); + render(); screen.getByText('My title'); }); it('should return the label text when provided', () => { - render( - - - - ); + render(); screen.getByText('My title'); }); - it('should return the translated label text when provided', () => { - render( - - - - ); - screen.getByText('My title'); - }); + describe('i18n', () => { + it('should use the source and resource to create a default translation key', () => { + render(); + screen.getByText('My Title'); + }); - it('should return the inferred label from source and resource when no label is provided', () => { - render( - - - - ); - screen.getByText('Title'); - }); + it('should use the label as key when provided', () => { + render(); + screen.getByText('My title'); + }); - it('should return the translated inferred label from source and resource when no label is provided', () => { - render( - - - - ); - screen.getByText('My Title'); + it('should infer a human readable default label when no translation is provided', () => { + render(); + screen.getByText('Title'); + }); }); - it('should return the inferred label from SourceContext when no label is provided but a SourceContext is present', () => { - render( - - source, - getLabel: source => `test.${source}`, - }} - > - - - - ); - screen.getByText('Title'); - }); + describe('SourceContext', () => { + it('should call getLabel for the default label', () => { + render(); + screen.getByText('Label for title'); + }); - it('should return the translated label from SourceContext when no label is provided but a SourceContext is present', () => { - render( - - source, - getLabel: source => `test.${source}`, - }} - > - - - - ); - screen.getByText('Label for title'); - }); + it('should use the getLabel return as translation key', () => { + render(); + screen.getByText('test.title'); + }); + + it('should infer a human readable default label when no translation is provided', () => { + render(); + screen.getByText('Title'); + }); - it('should return the inferred label when a resource prop is provided even when a SourceContext is present', () => { - render( - - source, - getLabel: source => `test.${source}`, - }} - > - - - - ); - screen.getByText('Title'); + it('should infer a human readable default label when a resource is provided', () => { + render(); + screen.getByText('Title'); + }); }); }); diff --git a/packages/ra-core/src/i18n/useTranslateLabel.stories.tsx b/packages/ra-core/src/i18n/useTranslateLabel.stories.tsx new file mode 100644 index 00000000000..fcde6694262 --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslateLabel.stories.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { useTranslateLabel } from './useTranslateLabel'; +import { TestTranslationProvider } from './TestTranslationProvider'; +import { SourceContextProvider } from '..'; + +export default { + title: 'ra-core/i18n/useTranslateLabel', +}; + +const TranslateLabel = ({ + source, + label, + resource, +}: { + source?: string; + label?: string | false | React.ReactElement; + resource?: string; +}) => { + const translateLabel = useTranslateLabel(); + return ( + <> + {translateLabel({ + label, + source, + resource, + })} + + ); +}; +export const Basic = () => ( + m}> + + +); + +export const Source = () => ( + m}> + + +); + +export const Resource = () => ( + m}> + + +); + +export const LabelFalse = () => ( + + + +); + +export const LabelEmpty = () => ( + + + +); + +export const LabelElement = () => ( + + My title} + source="title" + resource="posts" + /> + +); + +export const LabelText = () => ( + + + +); + +export const I18nTranslation = () => ( + + + +); + +export const I18nLabelAsKey = () => ( + + + +); + +export const I18nNoTranslation = () => ( + + + +); + +export const InSourceContext = () => ( + + source, + getLabel: source => `test.${source}`, + }} + > + + + +); + +export const InSourceContextI18nKey = () => ( + m}> + source, + getLabel: source => `test.${source}`, + }} + > + + + +); + +export const InSourceContextNoTranslation = () => ( + + source, + getLabel: source => `test.${source}`, + }} + > + + + +); + +export const InSourceContextWithResource = () => ( + + source, + getLabel: source => `test.${source}`, + }} + > + + + +); diff --git a/packages/ra-ui-materialui/src/Labeled.stories.tsx b/packages/ra-ui-materialui/src/Labeled.stories.tsx index 4f9a37c9a77..acecef575d7 100644 --- a/packages/ra-ui-materialui/src/Labeled.stories.tsx +++ b/packages/ra-ui-materialui/src/Labeled.stories.tsx @@ -1,5 +1,9 @@ import * as React from 'react'; -import { RecordContextProvider, ResourceContext } from 'ra-core'; +import { + I18nContextProvider, + RecordContextProvider, + ResourceContext, +} from 'ra-core'; import { TextField } from './field'; import { Labeled } from './Labeled'; import { Box, Stack } from '@mui/material'; @@ -128,3 +132,21 @@ export const FullWidthNoLabel = () => ( ); + +export const I18nKey = () => ( + 'en', + translate: m => m, + changeLocale: async () => {}, + }} + > + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/detail/SimpleShowLayout.spec.tsx b/packages/ra-ui-materialui/src/detail/SimpleShowLayout.spec.tsx index c805a1c59ab..656c6b8b455 100644 --- a/packages/ra-ui-materialui/src/detail/SimpleShowLayout.spec.tsx +++ b/packages/ra-ui-materialui/src/detail/SimpleShowLayout.spec.tsx @@ -2,7 +2,12 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; import { RecordContextProvider } from 'ra-core'; import { SimpleShowLayout } from './SimpleShowLayout'; -import { Basic, CustomChild, CustomLabel } from './SimpleShowLayout.stories'; +import { + Basic, + CustomChild, + CustomLabel, + I18nKey, +} from './SimpleShowLayout.stories'; import { TextField } from '../field'; describe('', () => { @@ -15,20 +20,29 @@ describe('', () => { ); - expect(screen.queryByText('foo')).not.toBeNull(); - expect(screen.queryByText('bar')).not.toBeNull(); + screen.getByText('foo'); + screen.getByText('bar'); }); it('should add a label for each field', () => { render(); - expect(screen.queryByText('Title')).not.toBeNull(); - expect(screen.queryByText('War and Peace')).not.toBeNull(); + screen.getByText('Title'); + screen.getByText('War and Peace'); + }); + + it('should translate the labels', () => { + render(); + screen.getByText('resources.books.fields.id'); + screen.getByText('resources.books.fields.title'); + screen.getByText('resources.books.fields.author'); + screen.getByText('resources.books.fields.summary'); + screen.getByText('resources.books.fields.year'); }); it('should accept custom children', () => { render(); - expect(screen.queryByText('War and Peace')).not.toBeNull(); - expect(screen.queryByText('Leo Tolstoy')).not.toBeNull(); + screen.getByText('War and Peace'); + screen.getByText('Leo Tolstoy'); }); it('should allows to customize or disable the label', () => { diff --git a/packages/ra-ui-materialui/src/detail/SimpleShowLayout.stories.tsx b/packages/ra-ui-materialui/src/detail/SimpleShowLayout.stories.tsx index 328517adab7..a8d8d3cf475 100644 --- a/packages/ra-ui-materialui/src/detail/SimpleShowLayout.stories.tsx +++ b/packages/ra-ui-materialui/src/detail/SimpleShowLayout.stories.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Grid, Divider as MuiDivider } from '@mui/material'; import { + I18nContextProvider, RecordContextProvider, ResourceContext, useRecordContext, @@ -173,3 +174,25 @@ export const Nested = () => ( ); + +export const I18nKey = () => ( + 'en', + translate: m => m, + changeLocale: async () => {}, + }} + > + + + + + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx index 45e27d06bba..997eabc8f2a 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx @@ -12,6 +12,8 @@ import { AdminContext } from '../AdminContext'; import { Edit } from '../detail'; import { NumberInput, TextInput } from '../input'; import { SimpleForm } from './SimpleForm'; +import { Labeled } from '../Labeled'; +import { TextField, NumberField } from '../field'; export default { title: 'ra-ui-materialui/forms/SimpleForm' }; @@ -87,6 +89,31 @@ export const NoToolbar = () => ( ); +export const WithFields = () => ( + x, + changeLocale: async () => {}, + getLocale: () => 'en', + }} + > + + + + + + + + + + + + + + + +); + const translate = (x, options) => { switch (x) { case 'resources.books.name':