From d64a1686270d264ce2bacba5448d555df16586c7 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 12:22:45 +0100 Subject: [PATCH 01/27] Introduce TranslatableInputs and TranslatableFields --- .../ra-core/src/i18n/TranslatableContext.ts | 20 +++ .../src/i18n/TranslatableContextProvider.tsx | 20 +++ packages/ra-core/src/i18n/index.ts | 4 + packages/ra-core/src/i18n/useTranslatable.ts | 56 ++++++++ .../src/i18n/useTranslatableContext.ts | 20 +++ .../ra-ui-materialui/src/field/TextField.tsx | 1 + .../src/field/TranslatableFields.tsx | 125 ++++++++++++++++++ .../src/field/TranslatableFieldsTab.tsx | 28 ++++ .../field/TranslatableFieldsTabContent.tsx | 85 ++++++++++++ .../src/field/TranslatableFieldsTabs.tsx | 47 +++++++ packages/ra-ui-materialui/src/field/index.ts | 3 + .../src/input/TranslatableInputs.tsx | 104 +++++++++++++++ .../src/input/TranslatableInputsTab.tsx | 39 ++++++ .../input/TranslatableInputsTabContent.tsx | 65 +++++++++ .../src/input/TranslatableInputsTabs.tsx | 50 +++++++ packages/ra-ui-materialui/src/input/index.ts | 4 + 16 files changed, 671 insertions(+) create mode 100644 packages/ra-core/src/i18n/TranslatableContext.ts create mode 100644 packages/ra-core/src/i18n/TranslatableContextProvider.tsx create mode 100644 packages/ra-core/src/i18n/useTranslatable.ts create mode 100644 packages/ra-core/src/i18n/useTranslatableContext.ts create mode 100644 packages/ra-ui-materialui/src/field/TranslatableFields.tsx create mode 100644 packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx create mode 100644 packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx create mode 100644 packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx create mode 100644 packages/ra-ui-materialui/src/input/TranslatableInputs.tsx create mode 100644 packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx create mode 100644 packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx create mode 100644 packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx diff --git a/packages/ra-core/src/i18n/TranslatableContext.ts b/packages/ra-core/src/i18n/TranslatableContext.ts new file mode 100644 index 00000000000..9fd80b7621c --- /dev/null +++ b/packages/ra-core/src/i18n/TranslatableContext.ts @@ -0,0 +1,20 @@ +import { createContext } from 'react'; + +export const TranslatableContext = createContext( + undefined +); + +export interface TranslatableContextValue { + getInputLabel: GetTranslatableInputLabel; + getSource: GetTranslatableSource; + languages: string[]; + selectedLanguage: string; + selectLanguage: SelectTranslatableLanguage; +} + +export type GetTranslatableSource = ( + field: string, + language?: string +) => string; +export type GetTranslatableInputLabel = (field: string) => string; +export type SelectTranslatableLanguage = (language: string) => void; diff --git a/packages/ra-core/src/i18n/TranslatableContextProvider.tsx b/packages/ra-core/src/i18n/TranslatableContextProvider.tsx new file mode 100644 index 00000000000..c564655e46d --- /dev/null +++ b/packages/ra-core/src/i18n/TranslatableContextProvider.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { ReactElement, ReactNode } from 'react'; +import { + TranslatableContext, + TranslatableContextValue, +} from './TranslatableContext'; + +export const TranslatableContextProvider = ({ + children, + value, +}: { + children: ReactNode; + value: TranslatableContextValue; +}): ReactElement => { + return ( + + {children} + + ); +}; diff --git a/packages/ra-core/src/i18n/index.ts b/packages/ra-core/src/i18n/index.ts index 9782fedf160..27d7d7289ee 100644 --- a/packages/ra-core/src/i18n/index.ts +++ b/packages/ra-core/src/i18n/index.ts @@ -24,3 +24,7 @@ export const DEFAULT_LOCALE = 'en'; export * from './TranslationUtils'; export * from './TranslationContext'; export * from './TranslationMessages'; +export * from './TranslatableContext'; +export * from './TranslatableContextProvider'; +export * from './useTranslatable'; +export * from './useTranslatableContext'; diff --git a/packages/ra-core/src/i18n/useTranslatable.ts b/packages/ra-core/src/i18n/useTranslatable.ts new file mode 100644 index 00000000000..c265d5ebaf4 --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslatable.ts @@ -0,0 +1,56 @@ +import { useState, useMemo } from 'react'; +import { useResourceContext } from '../core'; +import { getFieldLabelTranslationArgs } from '../util'; +import { TranslatableContextValue } from './TranslatableContext'; +import useTranslate from './useTranslate'; + +/** + * Hook supplying the logic to translate a field value in multiple languages. + * + * @param options The hook options + * @param {string} options.defaultLanguage The locale of the default selected language. Defaults to 'en'. + * @param {Language[]} options.languages An array of the supported languages. Each is an object with a locale and a name property. For example { locale: 'en', name: 'English' }. + * + * @returns + * An object with following properties and methods: + * - selectedLanguage: The locale of the currently selected language + * - languages: An array of the supported languages + * - getLabelInput: A function which returns the translated label for the given field + * - getSource: A function which returns the source for the given field + * - selectLanguage: A function which set the selected language + */ +export const useTranslatable = ( + options: UseTranslatableOptions +): TranslatableContextValue => { + const { defaultLanguage = 'en', languages } = options; + const [selectedLanguage, setSelectedLanguage] = useState(defaultLanguage); + const resource = useResourceContext({}); + const translate = useTranslate(); + + const context = useMemo( + () => ({ + getSource: (source: string, locale: string = selectedLanguage) => + `${source}.${locale}`, + getInputLabel: (source: string) => { + return translate( + ...getFieldLabelTranslationArgs({ + source, + resource, + label: undefined, + }) + ); + }, + languages, + selectedLanguage, + selectLanguage: setSelectedLanguage, + }), + [languages, resource, selectedLanguage, translate] + ); + + return context; +}; + +export type UseTranslatableOptions = { + defaultLanguage?: string; + languages: string[]; +}; diff --git a/packages/ra-core/src/i18n/useTranslatableContext.ts b/packages/ra-core/src/i18n/useTranslatableContext.ts new file mode 100644 index 00000000000..4364f84fc92 --- /dev/null +++ b/packages/ra-core/src/i18n/useTranslatableContext.ts @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { + TranslatableContext, + TranslatableContextValue, +} from './TranslatableContext'; + +/** + * Gives access to the current TranslatableContext. + */ +export const useTranslatableContext = (): TranslatableContextValue => { + const context = useContext(TranslatableContext); + + if (!context) { + throw new Error( + 'useTranslatableContext must be used inside a TranslatableContextProvider' + ); + } + + return context; +}; diff --git a/packages/ra-ui-materialui/src/field/TextField.tsx b/packages/ra-ui-materialui/src/field/TextField.tsx index ea13e4fc4f8..6e5097f9c0b 100644 --- a/packages/ra-ui-materialui/src/field/TextField.tsx +++ b/packages/ra-ui-materialui/src/field/TextField.tsx @@ -9,6 +9,7 @@ import { PublicFieldProps, InjectedFieldProps, fieldPropTypes } from './types'; const TextField: FC = memo( ({ className, source, record = {}, emptyText, ...rest }) => { const value = get(record, source); + console.log({ record, source }); return ( Basic usage + * + * {({ getSource }) => ( + * <> + * + * + * + * )} + * + * + * @example With a custom language selector + * } + * languages={[ + * { locale: 'en', name: 'English' } + * { locale: 'fr', name: 'Français' } + * ]} + * > + * {({ getSource }) => ( + * + * )} + * +> + * + * const MyLanguageSelector = () => { + * const { + * languages, + * selectedLanguage, + * selectLanguage, + * } = useTranslatable(availableLanguages, validate); + * + * return ( + * + * ); + * } + * + * * @param props The component props + * * @param {string} props.defaultLanguage The language selected by default (accept the language locale). Default to 'en'. + * * @param {Language[]} props.languages An array of the possible languages in the form: `[{ locale: 'en', language: 'English' }]. + * * @param {ReactElement} props.selector The element responsible for selecting a language. Defaults to Material UI tabs. + */ +export const TranslatableFields = ( + props: TranslatableFieldsProps +): ReactElement => { + const { + defaultLanguage = 'en', + languages, + selector = , + children, + record, + resource, + basePath, + } = props; + const context = useTranslatable({ defaultLanguage, languages }); + const classes = useStyles(props); + + return ( +
+ + {selector} + {typeof children === 'function' + ? children(context) + : languages.map(language => ( + + {children} + + ))} + +
+ ); +}; + +export interface TranslatableFieldsProps extends UseTranslatableOptions { + basePath: string; + children: ReactNode | TranslatableRenderFunction; + classes?: ClassesOverride; + record: Record; + resource: string; + selector?: ReactElement; +} + +export type TranslatableRenderFunction = ( + context: TranslatableContextValue +) => ReactNode; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.default, + }, +})); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx new file mode 100644 index 00000000000..f27f2c30d53 --- /dev/null +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTab.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Tab, { TabProps } from '@material-ui/core/Tab'; +import { useTranslate } from 'ra-core'; +import { capitalize } from 'inflection'; + +/** + * Single tab which select a language TranslatableFields component. + * @see TranslatableFields + */ +export const TranslatableFieldsTab = ( + props: TranslatableFieldsTabProps & TabProps +) => { + const { locale, ...rest } = props; + const translate = useTranslate(); + + return ( + + ); +}; + +interface TranslatableFieldsTabProps { + locale: string; +} diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx new file mode 100644 index 00000000000..371229a88b7 --- /dev/null +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabContent.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { + Children, + cloneElement, + isValidElement, + ReactElement, + ReactNode, +} from 'react'; +import { useTranslatableContext, Record } from 'ra-core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ClassesOverride } from '../types'; +import { Labeled } from '../input'; + +/** + * Default container for a group of translatable fields inside a TranslatableFields components. + * @see TranslatableFields + */ +export const TranslatableFieldsTabContent = ( + props: LanguageTabProps +): ReactElement => { + const { basePath, children, locale, record, resource, ...other } = props; + const { + selectedLanguage, + getInputLabel, + getSource, + } = useTranslatableContext(); + const classes = useStyles(props); + + return ( + + ); +}; + +export type LanguageTabProps = { + basePath: string; + children: ReactNode; + classes: ClassesOverride; + formGroupKeyPrefix?: string; + locale: string; + record: Record; + resource: string; +}; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1, + padding: theme.spacing(1), + backgroundColor: theme.palette.background.default, + }, +})); diff --git a/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx new file mode 100644 index 00000000000..1511e766def --- /dev/null +++ b/packages/ra-ui-materialui/src/field/TranslatableFieldsTabs.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { ReactElement } from 'react'; +import AppBar from '@material-ui/core/AppBar'; +import Tabs, { TabsProps } from '@material-ui/core/Tabs'; +import { useTranslatableContext } from 'ra-core'; +import { TranslatableFieldsTab } from './TranslatableFieldsTab'; +import { AppBarProps } from '../layout'; + +/** + * Default language selector for the TranslatableFields component. Generates a tab for each specified language. + * @see TranslatableFields + */ +export const TranslatableFieldsTabs = ({ + TabsProps: tabsProps, +}: LanguagesTabsProps & AppBarProps): ReactElement => { + const { + languages, + selectLanguage, + selectedLanguage, + } = useTranslatableContext(); + + const handleChange = (event, newLanguage): void => { + selectLanguage(newLanguage); + }; + + return ( + + + {languages.map(locale => ( + + ))} + + + ); +}; + +export interface LanguagesTabsProps { + TabsProps?: TabsProps; +} diff --git a/packages/ra-ui-materialui/src/field/index.ts b/packages/ra-ui-materialui/src/field/index.ts index 35f759ded05..41e310c21a0 100644 --- a/packages/ra-ui-materialui/src/field/index.ts +++ b/packages/ra-ui-materialui/src/field/index.ts @@ -21,6 +21,9 @@ import UrlField, { UrlFieldProps } from './UrlField'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { FieldProps } from './types'; +export * from './TranslatableFields'; +export * from './TranslatableFieldsTabContent'; + export { ArrayField, BooleanField, diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx new file mode 100644 index 00000000000..8200cfd9395 --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { ReactElement, ReactNode } from 'react'; +import { + TranslatableContextProvider, + useTranslatable, + TranslatableContextValue, + UseTranslatableOptions, +} from 'ra-core'; +import { TranslatableInputsTabs } from './TranslatableInputsTabs'; +import { TranslatableInputsTabContent } from './TranslatableInputsTabContent'; + +/** + * Provides a way to edit multiple languages for any inputs passed as children. + * + * @example Basic usage + * + * {({ getSource }) => ( + * <> + * + * + * + * )} + * + * + * @example With a custom language selector + * } + * languages={[ + * { locale: 'en', name: 'English' } + * { locale: 'fr', name: 'Français' } + * ]} + * > + * {({ getSource }) => ( + * + * )} + * + * + * const MyLanguageSelector = () => { + * const { + * languages, + * selectedLanguage, + * selectLanguage, + * } = useTranslatable(availableLanguages, validate); + * + * return ( + * + * ); + * } + * + * * @param props The component props + * * @param {string} props.defaultLanguage The language selected by default (accept the language locale). Default to 'en'. + * * @param {Language[]} props.languages An array of the possible languages in the form: `[{ locale: 'en', language: 'English' }]. + * * @param {ReactElement} props.selector The element responsible for selecting a language. Defaults to Material UI tabs. + */ +export const TranslatableInputs = (props: TranslatableProps): ReactElement => { + const { + defaultLanguage = 'en', + languages, + formGroupKeyPrefix, + selector = ( + + ), + children, + } = props; + const context = useTranslatable({ defaultLanguage, languages }); + + return ( + + {selector} + {typeof children === 'function' + ? children(context) + : languages.map(language => ( + + {children} + + ))} + + ); +}; + +export interface TranslatableProps extends UseTranslatableOptions { + selector?: ReactElement; + children: ReactNode | TranslatableRenderFunction; + formGroupKeyPrefix?: string; +} + +export type TranslatableRenderFunction = ( + context: TranslatableContextValue +) => ReactNode; diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx new file mode 100644 index 00000000000..1f411524e64 --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Tab, { TabProps } from '@material-ui/core/Tab'; +import { useFormGroup, useTranslate } from 'ra-core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ClassesOverride } from '../types'; +import { capitalize } from 'inflection'; + +/** + * Single tab which select a language TranslatableInputs component. + * @see TranslatableInputs + */ +export const TranslatebleInputsTab = ( + props: TranslatebleInputsTabProps & TabProps +) => { + const { formGroupKeyPrefix, locale, ...rest } = props; + const { invalid } = useFormGroup(`${formGroupKeyPrefix}${locale}`); + const classes = useStyles(props); + const translate = useTranslate(); + + return ( + + ); +}; + +const useStyles = makeStyles(theme => ({ + error: { color: theme.palette.error.main }, +})); + +interface TranslatebleInputsTabProps { + classes?: ClassesOverride; + formGroupKeyPrefix?: string; + locale: string; +} diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx new file mode 100644 index 00000000000..e4908bfa3fa --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabContent.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { + Children, + cloneElement, + isValidElement, + ReactElement, + ReactNode, +} from 'react'; +import { FormGroupContextProvider, useTranslatableContext } from 'ra-core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ClassesOverride } from '../types'; + +/** + * Default container for a group of translatable inputs inside a TranslatableInputs components. + * @see TranslatableInputs + */ +export const TranslatableInputsTabContent = ( + props: LanguageTabProps +): ReactElement => { + const { children, formGroupKeyPrefix = '', locale, ...other } = props; + const { + selectedLanguage, + getInputLabel, + getSource, + } = useTranslatableContext(); + const classes = useStyles(props); + + return ( + + + + ); +}; + +export type LanguageTabProps = { + children: ReactNode; + classes?: ClassesOverride; + formGroupKeyPrefix?: string; + locale: string; +}; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1, + padding: theme.spacing(1), + backgroundColor: theme.palette.background.default, + }, +})); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx new file mode 100644 index 00000000000..0552d7d2041 --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTabs.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { ReactElement } from 'react'; +import AppBar from '@material-ui/core/AppBar'; +import Tabs, { TabsProps } from '@material-ui/core/Tabs'; +import { useTranslatableContext } from 'ra-core'; +import { TranslatebleInputsTab } from './TranslatableInputsTab'; +import { AppBarProps } from '../layout'; + +/** + * Default language selector for the TranslatableInputs component. Generates a tab for each specified language. + * @see TranslatableInputs + */ +export const TranslatableInputsTabs = ({ + formGroupKeyPrefix, + TabsProps: tabsProps, +}: TranslatableInputsTabsProps & AppBarProps): ReactElement => { + const { + languages, + selectLanguage, + selectedLanguage, + } = useTranslatableContext(); + + const handleChange = (event, newLanguage): void => { + selectLanguage(newLanguage); + }; + + return ( + + + {languages.map(locale => ( + + ))} + + + ); +}; + +export interface TranslatableInputsTabsProps { + formGroupKeyPrefix?: string; + TabsProps?: TabsProps; +} diff --git a/packages/ra-ui-materialui/src/input/index.ts b/packages/ra-ui-materialui/src/input/index.ts index e9bd5a6a53a..cfd161068b8 100644 --- a/packages/ra-ui-materialui/src/input/index.ts +++ b/packages/ra-ui-materialui/src/input/index.ts @@ -22,6 +22,10 @@ import SelectArrayInput from './SelectArrayInput'; import SelectInput from './SelectInput'; import TextInput from './TextInput'; import sanitizeInputRestProps from './sanitizeInputRestProps'; +export * from './TranslatableInputs'; +export * from './TranslatableInputsTabContent'; +export * from './TranslatableInputsTabs'; +export * from './TranslatableInputsTab'; export { ArrayInput, From 36fa1071d4a9918d0a7bd5e0bafb8e088da18a0c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 12:23:08 +0100 Subject: [PATCH 02/27] Update simple example with translatable tags names --- examples/simple/src/data.js | 38 +++++++++---------- examples/simple/src/posts/PostList.js | 4 +- examples/simple/src/posts/PostShow.js | 4 +- .../simple/src/posts/TagReferenceInput.js | 2 +- examples/simple/src/tags/TagCreate.js | 5 ++- examples/simple/src/tags/TagEdit.js | 5 ++- examples/simple/src/tags/TagList.js | 11 +++++- examples/simple/src/tags/TagShow.js | 11 +++++- 8 files changed, 50 insertions(+), 30 deletions(-) diff --git a/examples/simple/src/data.js b/examples/simple/src/data.js index a11ce43ffab..2ffb0813837 100644 --- a/examples/simple/src/data.js +++ b/examples/simple/src/data.js @@ -380,110 +380,110 @@ export default { tags: [ { id: 1, - name: 'Sport', + name: { en: 'Sport' }, published: 1, }, { id: 2, - name: 'Technology', + name: { en: 'Technology' }, published: false, }, { id: 3, - name: 'Code', + name: { en: 'Code' }, published: true, }, { id: 4, - name: 'Photo', + name: { en: 'Photo' }, published: false, }, { id: 5, - name: 'Music', + name: { en: 'Music' }, published: 1, }, { id: 6, - name: 'Parkour', + name: { en: 'Parkour' }, published: 1, parent_id: 1, }, { id: 7, - name: 'Crossfit', + name: { en: 'Crossfit' }, published: 1, parent_id: 1, }, { id: 8, - name: 'Computing', + name: { en: 'Computing' }, published: 1, parent_id: 2, }, { id: 9, - name: 'Nanoscience', + name: { en: 'Nanoscience' }, published: 1, parent_id: 2, }, { id: 10, - name: 'Blockchain', + name: { en: 'Blockchain' }, published: 1, parent_id: 2, }, { id: 11, - name: 'Node', + name: { en: 'Node' }, published: 1, parent_id: 3, }, { id: 12, - name: 'React', + name: { en: 'React' }, published: 1, parent_id: 3, }, { id: 13, - name: 'Nature', + name: { en: 'Nature' }, published: 1, parent_id: 4, }, { id: 14, - name: 'People', + name: { en: 'People' }, published: 1, parent_id: 4, }, { id: 15, - name: 'Animals', + name: { en: 'Animals' }, published: 1, parent_id: 13, }, { id: 16, - name: 'Moutains', + name: { en: 'Moutains' }, published: 1, parent_id: 13, }, { id: 17, - name: 'Rap', + name: { en: 'Rap' }, published: 1, parent_id: 5, }, { id: 18, - name: 'Rock', + name: { en: 'Rock' }, published: 1, parent_id: 5, }, { id: 19, - name: 'World', + name: { en: 'World' }, published: 1, parent_id: 5, }, diff --git a/examples/simple/src/posts/PostList.js b/examples/simple/src/posts/PostList.js index 26cff684828..1d387f60758 100644 --- a/examples/simple/src/posts/PostList.js +++ b/examples/simple/src/posts/PostList.js @@ -165,7 +165,7 @@ const PostList = props => { headerClassName={classes.hiddenOnSmallScreens} > - + @@ -178,6 +178,6 @@ const PostList = props => { ); }; -const tagSort = { field: 'name', order: 'ASC' }; +const tagSort = { field: 'name.en', order: 'ASC' }; export default PostList; diff --git a/examples/simple/src/posts/PostShow.js b/examples/simple/src/posts/PostShow.js index 7048942f388..784a888becc 100644 --- a/examples/simple/src/posts/PostShow.js +++ b/examples/simple/src/posts/PostShow.js @@ -70,10 +70,10 @@ const PostShow = props => { - + diff --git a/examples/simple/src/posts/TagReferenceInput.js b/examples/simple/src/posts/TagReferenceInput.js index 6e787893033..7f473d0f28b 100644 --- a/examples/simple/src/posts/TagReferenceInput.js +++ b/examples/simple/src/posts/TagReferenceInput.js @@ -31,7 +31,7 @@ const TagReferenceInput = ({ ...props }) => { return (
- + + + )} + /> + ); + + fireEvent.change(queryByDisplayValue('english name'), { + target: { value: 'english name updated' }, + }); + fireEvent.click(getByText('ra.languages.fr')); + fireEvent.change(queryByDisplayValue('french nested field'), { + target: { value: 'french nested field updated' }, + }); + fireEvent.click(getByText('save')); + + expect(save).toHaveBeenCalledWith( + { + id: 123, + name: { + en: 'english name updated', + fr: 'french name', + }, + description: { + en: 'english description', + fr: 'french description', + }, + nested: { + field: { + en: 'english nested field', + fr: 'french nested field updated', + }, + }, + }, + undefined + ); + }); +}); diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx index c8e8c7c0356..5ca53eb6885 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx @@ -67,7 +67,7 @@ export const TranslatableInputs = (props: TranslatableProps): ReactElement => { const { defaultLanguage = 'en', languages, - formGroupKeyPrefix, + formGroupKeyPrefix = '', selector = ( ), From e9f7aa63b9c7b70bdf78e7e8c197fdd5eb39786b Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 16:33:33 +0100 Subject: [PATCH 07/27] Add selector customization test for TranslatableFields --- .../src/field/TranslatableFields.spec.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx index 307cfce5222..b40300c7b27 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx @@ -3,6 +3,7 @@ import expect from 'expect'; import { fireEvent, render } from '@testing-library/react'; import { TranslatableFields } from './TranslatableFields'; import TextField from './TextField'; +import { useTranslatableContext } from 'ra-core'; const record = { id: 123, @@ -60,4 +61,68 @@ describe('', () => { getByLabelText('ra.languages.fr').getAttribute('hidden') ).toBeNull(); }); + + it('should allow to customize the language selector', () => { + const Selector = () => { + const { + languages, + selectLanguage, + selectedLanguage, + } = useTranslatableContext(); + + const handleChange = (event): void => { + console.log(event.target.value); + selectLanguage(event.target.value); + }; + + return ( + + ); + }; + + const { queryByText, getByLabelText } = render( + } + > + + + + + ); + + expect(getByLabelText('en').getAttribute('hidden')).toBeNull(); + expect(getByLabelText('fr').getAttribute('hidden')).toBeDefined(); + + expect(queryByText('english name')).not.toBeNull(); + expect(queryByText('english description')).not.toBeNull(); + expect(queryByText('english nested field')).not.toBeNull(); + + expect(queryByText('french name')).not.toBeNull(); + expect(queryByText('french description')).not.toBeNull(); + expect(queryByText('french nested field')).not.toBeNull(); + + fireEvent.change(getByLabelText('select language'), { + target: { value: 'fr' }, + }); + expect(getByLabelText('en').getAttribute('hidden')).toBeDefined(); + expect(getByLabelText('fr').getAttribute('hidden')).toBeNull(); + }); }); From 715316dc99a8f768c87eb844655c48027e13cbd4 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 17:09:38 +0100 Subject: [PATCH 08/27] Rename getLabel function type --- packages/ra-core/src/i18n/TranslatableContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/i18n/TranslatableContext.ts b/packages/ra-core/src/i18n/TranslatableContext.ts index c3d0791d843..a869db29acf 100644 --- a/packages/ra-core/src/i18n/TranslatableContext.ts +++ b/packages/ra-core/src/i18n/TranslatableContext.ts @@ -5,7 +5,7 @@ export const TranslatableContext = createContext( ); export interface TranslatableContextValue { - getLabel: GetTranslatableInputLabel; + getLabel: GetTranslatableLabel; getSource: GetTranslatableSource; languages: string[]; selectedLanguage: string; @@ -16,5 +16,5 @@ export type GetTranslatableSource = ( field: string, language?: string ) => string; -export type GetTranslatableInputLabel = (field: string) => string; +export type GetTranslatableLabel = (field: string) => string; export type SelectTranslatableLanguage = (language: string) => void; From 04be2657fb001a3f5b1fedeb40bfc4a15df95d4e Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 17:12:03 +0100 Subject: [PATCH 09/27] Remove children render function --- .../src/field/TranslatableFields.tsx | 31 +++++++------------ .../src/input/TranslatableInputs.tsx | 27 ++++++---------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx index 6ee7e0f94fa..8e41df13034 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.tsx @@ -3,7 +3,6 @@ import { ReactElement, ReactNode } from 'react'; import { TranslatableContextProvider, useTranslatable, - TranslatableContextValue, UseTranslatableOptions, Record, } from 'ra-core'; @@ -86,19 +85,17 @@ export const TranslatableFields = (
{selector} - {typeof children === 'function' - ? children(context) - : languages.map(language => ( - - {children} - - ))} + {languages.map(language => ( + + {children} + + ))}
); @@ -106,17 +103,13 @@ export const TranslatableFields = ( export interface TranslatableFieldsProps extends UseTranslatableOptions { basePath: string; - children: ReactNode | TranslatableFieldRenderFunction; + children: ReactNode; classes?: ClassesOverride; record: Record; resource: string; selector?: ReactElement; } -export type TranslatableFieldRenderFunction = ( - context: TranslatableContextValue -) => ReactNode; - const useStyles = makeStyles(theme => ({ root: { flexGrow: 1, diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx index 5ca53eb6885..546f1311d2b 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputs.tsx @@ -3,7 +3,6 @@ import { ReactElement, ReactNode } from 'react'; import { TranslatableContextProvider, useTranslatable, - TranslatableContextValue, UseTranslatableOptions, } from 'ra-core'; import { TranslatableInputsTabs } from './TranslatableInputsTabs'; @@ -78,27 +77,21 @@ export const TranslatableInputs = (props: TranslatableProps): ReactElement => { return ( {selector} - {typeof children === 'function' - ? children(context) - : languages.map(language => ( - - {children} - - ))} + {languages.map(language => ( + + {children} + + ))} ); }; export interface TranslatableProps extends UseTranslatableOptions { selector?: ReactElement; - children: ReactNode | TranslatableInputRenderFunction; + children: ReactNode; formGroupKeyPrefix?: string; } - -export type TranslatableInputRenderFunction = ( - context: TranslatableContextValue -) => ReactNode; From de9042ab70fb0cea40cd2f1fb465c64ca716708a Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Fri, 22 Jan 2021 17:30:06 +0100 Subject: [PATCH 10/27] Use locale instead of language --- .../ra-core/src/i18n/TranslatableContext.ts | 13 ++-- packages/ra-core/src/i18n/useTranslatable.ts | 28 ++++----- .../src/field/TranslatableFields.spec.tsx | 34 +++++------ .../src/field/TranslatableFields.tsx | 58 +++++++----------- .../src/field/TranslatableFieldsTab.tsx | 4 +- .../field/TranslatableFieldsTabContent.tsx | 4 +- .../src/field/TranslatableFieldsTabs.tsx | 20 ++----- .../src/input/TranslatableInputs.spec.tsx | 20 +++---- .../src/input/TranslatableInputs.tsx | 60 +++++++------------ .../src/input/TranslatableInputsTab.tsx | 4 +- .../input/TranslatableInputsTabContent.tsx | 4 +- .../src/input/TranslatableInputsTabs.tsx | 20 ++----- 12 files changed, 111 insertions(+), 158 deletions(-) diff --git a/packages/ra-core/src/i18n/TranslatableContext.ts b/packages/ra-core/src/i18n/TranslatableContext.ts index a869db29acf..bb3fed77657 100644 --- a/packages/ra-core/src/i18n/TranslatableContext.ts +++ b/packages/ra-core/src/i18n/TranslatableContext.ts @@ -7,14 +7,11 @@ export const TranslatableContext = createContext( export interface TranslatableContextValue { getLabel: GetTranslatableLabel; getSource: GetTranslatableSource; - languages: string[]; - selectedLanguage: string; - selectLanguage: SelectTranslatableLanguage; + locales: string[]; + selectedLocale: string; + selectLocale: SelectTranslatableLocale; } -export type GetTranslatableSource = ( - field: string, - language?: string -) => string; +export type GetTranslatableSource = (field: string, locale?: string) => string; export type GetTranslatableLabel = (field: string) => string; -export type SelectTranslatableLanguage = (language: string) => void; +export type SelectTranslatableLocale = (locale: string) => void; diff --git a/packages/ra-core/src/i18n/useTranslatable.ts b/packages/ra-core/src/i18n/useTranslatable.ts index 40fe468c26d..d10e92a048c 100644 --- a/packages/ra-core/src/i18n/useTranslatable.ts +++ b/packages/ra-core/src/i18n/useTranslatable.ts @@ -8,28 +8,28 @@ import useTranslate from './useTranslate'; * Hook supplying the logic to translate a field value in multiple languages. * * @param options The hook options - * @param {string} options.defaultLanguage The locale of the default selected language. Defaults to 'en'. - * @param {Language[]} options.languages An array of the supported languages. Each is an object with a locale and a name property. For example { locale: 'en', name: 'English' }. + * @param {string} options.defaultLocale The locale of the default selected locale. Defaults to 'en'. + * @param {strong[]} options.locales An array of the supported locales. Each is an object with a locale and a name property. For example { locale: 'en', name: 'English' }. * * @returns * An object with following properties and methods: - * - selectedLanguage: The locale of the currently selected language - * - languages: An array of the supported languages + * - selectedLocale: The locale of the currently selected locale + * - locales: An array of the supported locales * - getLabelInput: A function which returns the translated label for the given field * - getSource: A function which returns the source for the given field - * - selectLanguage: A function which set the selected language + * - selectLocale: A function which set the selected locale */ export const useTranslatable = ( options: UseTranslatableOptions ): TranslatableContextValue => { - const { defaultLanguage = 'en', languages } = options; - const [selectedLanguage, setSelectedLanguage] = useState(defaultLanguage); + const { defaultLocale = 'en', locales } = options; + const [selectedLocale, setSelectedLocale] = useState(defaultLocale); const resource = useResourceContext({}); const translate = useTranslate(); const context = useMemo( () => ({ - getSource: (source: string, locale: string = selectedLanguage) => + getSource: (source: string, locale: string = selectedLocale) => `${source}.${locale}`, getLabel: (source: string) => { return translate( @@ -40,17 +40,17 @@ export const useTranslatable = ( }) ); }, - languages, - selectedLanguage, - selectLanguage: setSelectedLanguage, + locales, + selectedLocale, + selectLocale: setSelectedLocale, }), - [languages, resource, selectedLanguage, translate] + [locales, resource, selectedLocale, translate] ); return context; }; export type UseTranslatableOptions = { - defaultLanguage?: string; - languages: string[]; + defaultLocale?: string; + locales: string[]; }; diff --git a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx index b40300c7b27..8ac419e9433 100644 --- a/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx +++ b/packages/ra-ui-materialui/src/field/TranslatableFields.spec.tsx @@ -24,13 +24,13 @@ const record = { }; describe('', () => { - it('should render every fields for every languages', () => { + it('should render every fields for every locales', () => { const { queryByText, getByLabelText, getByText } = render( @@ -39,10 +39,10 @@ describe('', () => { ); expect( - getByLabelText('ra.languages.en').getAttribute('hidden') + getByLabelText('ra.locales.en').getAttribute('hidden') ).toBeNull(); expect( - getByLabelText('ra.languages.fr').getAttribute('hidden') + getByLabelText('ra.locales.fr').getAttribute('hidden') ).toBeDefined(); expect(queryByText('english name')).not.toBeNull(); @@ -53,35 +53,35 @@ describe('', () => { expect(queryByText('french description')).not.toBeNull(); expect(queryByText('french nested field')).not.toBeNull(); - fireEvent.click(getByText('ra.languages.fr')); + fireEvent.click(getByText('ra.locales.fr')); expect( - getByLabelText('ra.languages.en').getAttribute('hidden') + getByLabelText('ra.locales.en').getAttribute('hidden') ).toBeDefined(); expect( - getByLabelText('ra.languages.fr').getAttribute('hidden') + getByLabelText('ra.locales.fr').getAttribute('hidden') ).toBeNull(); }); - it('should allow to customize the language selector', () => { + it('should allow to customize the locale selector', () => { const Selector = () => { const { - languages, - selectLanguage, - selectedLanguage, + locales, + selectLocale, + selectedLocale, } = useTranslatableContext(); const handleChange = (event): void => { console.log(event.target.value); - selectLanguage(event.target.value); + selectLocale(event.target.value); }; return ( - * {languages.map((language) => ( - *