Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LabelPrefix context to better guess correct input labels #7710

Merged
merged 19 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions examples/simple/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,17 @@ export const messages = {
posts: {
name: 'Post |||| Posts',
fields: {
average_note: 'Average note',
body: 'Body',
category: 'Category',
comments: 'Comments',
commentable: 'Commentable',
commentable_short: 'Com.',
created_at: 'Created at',
notifications: 'Notifications recipients',
nb_view: 'Nb views',
password: 'Password (if protected post)',
pictures: 'Related Pictures',
published_at: 'Published at',
teaser: 'Teaser',
tags: 'Tags',
title: 'Title',
views: 'Views',
authors: 'Authors',
},
},
comments: {
name: 'Comment |||| Comments',
fields: {
body: 'Body',
created_at: 'Created at',
post_id: 'Posts',
author: {
name: 'Author',
},
post_id: 'Post',
},
},
users: {
Expand Down
7 changes: 6 additions & 1 deletion examples/simple/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ export default {
nb_view: 'Nb de vues',
password: 'Mot de passe (si protégé)',
pictures: 'Photos associées',
'pictures.url': 'URL',
'pictures.metas.authors': 'Auteurs',
'pictures.metas.authors.name': 'Nom',
authors: 'Auteurs',
'authors.user_id': 'Nom',
'authors.role': 'Rôle',
published_at: 'Publié le',
teaser: 'Description',
tags: 'Catégories',
title: 'Titre',
views: 'Vues',
authors: 'Auteurs',
},
},
comments: {
Expand Down
3 changes: 1 addition & 2 deletions examples/simple/src/posts/PostEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const PostEdit = () => {
source="user_id"
reference="users"
>
<AutocompleteInput label="User" />
<AutocompleteInput />
</ReferenceInput>
<FormDataConsumer>
{({
Expand Down Expand Up @@ -181,7 +181,6 @@ const PostEdit = () => {
},
]}
{...rest}
label="Role"
/>
) : null
}
Expand Down
30 changes: 18 additions & 12 deletions packages/ra-core/src/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { FormProvider, FieldValues, UseFormProps } from 'react-hook-form';
import { FormGroupsProvider } from './FormGroupsProvider';
import { RaRecord } from '../types';
import { useRecordContext, OptionalRecordContextProvider } from '../controller';
import { useResourceContext } from '../core';
import { LabelPrefixContextProvider } from '../util';
import { ValidateForm } from './getSimpleValidationResolver';
import { useAugmentedForm } from './useAugmentedForm';

Expand Down Expand Up @@ -38,22 +40,25 @@ import { useAugmentedForm } from './useAugmentedForm';
export const Form = (props: FormProps) => {
const { children, id, className, noValidate = false } = props;
const record = useRecordContext(props);
const resource = useResourceContext(props);
const { form, formHandleSubmit } = useAugmentedForm(props);

return (
<OptionalRecordContextProvider value={record}>
<FormProvider {...form}>
<FormGroupsProvider>
<form
onSubmit={formHandleSubmit}
noValidate={noValidate}
id={id}
className={className}
>
{children}
</form>
</FormGroupsProvider>
</FormProvider>
<LabelPrefixContextProvider prefix={`resources.${resource}.fields`}>
<FormProvider {...form}>
<FormGroupsProvider>
<form
onSubmit={formHandleSubmit}
noValidate={noValidate}
id={id}
className={className}
>
{children}
</form>
</FormGroupsProvider>
</FormProvider>
</LabelPrefixContextProvider>
</OptionalRecordContextProvider>
);
};
Expand All @@ -71,6 +76,7 @@ export interface FormOwnProps {
formRootPathname?: string;
id?: string;
record?: Partial<RaRecord>;
resource?: string;
onSubmit?: (data: FieldValues) => any | Promise<any>;
warnWhenUnsavedChanges?: boolean;
}
1 change: 1 addition & 0 deletions packages/ra-core/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export * from './useSetLocale';
export * from './useTranslatable';
export * from './useTranslatableContext';
export * from './useTranslate';
export * from './useTranslateLabel';
export * from './useI18nProvider';
18 changes: 5 additions & 13 deletions packages/ra-core/src/i18n/useTranslatable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useState, useMemo } from 'react';
import { useResourceContext } from '../core';
import { getFieldLabelTranslationArgs } from '../util';
import { TranslatableContextValue } from './TranslatableContext';
import { useLocaleState } from './useLocaleState';
import { useTranslate } from './useTranslate';
import { useTranslateLabel } from './useTranslateLabel';

/**
* Hook supplying the logic to translate a field value in multiple languages.
Expand All @@ -27,26 +26,19 @@ export const useTranslatable = (
const { defaultLocale = localeFromUI, locales } = options;
const [selectedLocale, setSelectedLocale] = useState(defaultLocale);
const resource = useResourceContext({});
const translate = useTranslate();
const translateLabel = useTranslateLabel();

const context = useMemo<TranslatableContextValue>(
() => ({
getSource: (source: string, locale: string = selectedLocale) =>
`${source}.${locale}`,
getLabel: (source: string, label?: string) => {
return translate(
...getFieldLabelTranslationArgs({
source,
resource,
label,
})
);
},
getLabel: (source: string, label?: string) =>
translateLabel({ source, resource, label }) as string,
locales,
selectedLocale,
selectLocale: setSelectedLocale,
}),
[locales, resource, selectedLocale, translate]
[locales, resource, selectedLocale, translateLabel]
);

return context;
Expand Down
42 changes: 42 additions & 0 deletions packages/ra-core/src/i18n/useTranslateLabel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCallback, ReactElement } from 'react';

import { useTranslate } from './useTranslate';
import { useLabelPrefix, getFieldLabelTranslationArgs } from '../util';
import { useResourceContext } from '../core';

export const useTranslateLabel = () => {
const translate = useTranslate();
const prefix = useLabelPrefix();
const resourceFromContext = useResourceContext();

return useCallback(
({
source,
label,
resource,
}: {
source?: string;
label?: string | false | ReactElement;
resource?: string;
}) => {
if (label === false || label === '') {
return null;
}

if (label && typeof label !== 'string') {
return label;
}

return translate(
...getFieldLabelTranslationArgs({
label: label as string,
prefix,
resource,
resourceFromContext,
source,
})
);
},
[prefix, resourceFromContext, translate]
);
};
21 changes: 8 additions & 13 deletions packages/ra-core/src/util/FieldTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import * as React from 'react';
import { ReactElement, memo } from 'react';

import { useTranslate } from '../i18n';
import getFieldLabelTranslationArgs from './getFieldLabelTranslationArgs';
import { useResourceContext } from '../core/useResourceContext';
import { useTranslateLabel } from '../i18n';

export interface FieldTitleProps {
isRequired?: boolean;
Expand All @@ -13,9 +11,8 @@ export interface FieldTitleProps {
}

export const FieldTitle = (props: FieldTitleProps) => {
const { source, label, isRequired } = props;
const resource = useResourceContext(props);
const translate = useTranslate();
const { source, label, resource, isRequired } = props;
const translateLabel = useTranslateLabel();

if (label === false || label === '') {
return null;
Expand All @@ -27,13 +24,11 @@ export const FieldTitle = (props: FieldTitleProps) => {

return (
<span>
{translate(
...getFieldLabelTranslationArgs({
label: label as string,
resource,
source,
})
)}
{translateLabel({
label,
resource,
source,
})}
{isRequired && <span aria-hidden="true">&thinsp;*</span>}
</span>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/ra-core/src/util/LabelPrefixContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react';

export const LabelPrefixContext = createContext<string>('');
18 changes: 18 additions & 0 deletions packages/ra-core/src/util/LabelPrefixContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';
import { LabelPrefixContext } from './LabelPrefixContext';
import { useLabelPrefix } from './useLabelPrefix';

export const LabelPrefixContextProvider = ({
prefix,
concatenate = true,
children,
}) => {
const oldPrefix = useLabelPrefix();
const newPrefix =
oldPrefix && concatenate ? `${oldPrefix}.${prefix}` : prefix;
return (
<LabelPrefixContext.Provider value={newPrefix}>
{children}
</LabelPrefixContext.Provider>
);
};
72 changes: 62 additions & 10 deletions packages/ra-core/src/util/getFieldLabelTranslationArgs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ describe('getFieldLabelTranslationArgs', () => {
it('should return empty span by default', () =>
expect(getFieldLabelTranslationArgs()).toEqual(['']));

it('should return the label when given', () =>
it('should return the label when given', () => {
expect(
getFieldLabelTranslationArgs({
label: 'foo',
resource: 'posts',
source: 'title',
})
).toEqual(['foo', { _: 'foo' }]));
).toEqual(['foo', { _: 'foo' }]);
});

it('should return the source and resource as translate key', () => {
expect(
getFieldLabelTranslationArgs({
resource: 'posts',
source: 'title',
})
).toEqual([`resources.posts.fields.title`, { _: 'Title' }]);
});

it('should return the humanized source when given', () => {
expect(
Expand All @@ -28,37 +38,79 @@ describe('getFieldLabelTranslationArgs', () => {
source: 'title_with_underscore',
})
).toEqual([
`resources.posts.fields.title_with_underscore`,
'resources.posts.fields.title_with_underscore',
{ _: 'Title with underscore' },
]);

expect(
getFieldLabelTranslationArgs({
resource: 'posts',
source: 'title.with.dots',
})
).toEqual([
'resources.posts.fields.title.with.dots',
{ _: 'Title with dots' },
]);

expect(
getFieldLabelTranslationArgs({
resource: 'posts',
source: 'titleWithCamelCase',
})
).toEqual([
`resources.posts.fields.titleWithCamelCase`,
'resources.posts.fields.titleWithCamelCase',
{ _: 'Title with camel case' },
]);
});

it('should return the source and resource as translate key', () => {
it('should ignore the source part corresponding to the parent in an iterator', () => {
expect(
getFieldLabelTranslationArgs({
resource: 'posts',
source: 'book.authors.2.categories.3.identifier.name',
})
).toEqual([
'resources.posts.fields.book.authors.categories.identifier.name',
{ _: 'Identifier name' },
]);
});

it('should prefer the resource over the prefix', () => {
expect(
getFieldLabelTranslationArgs({
resource: 'books',
prefix: 'resources.posts.fields',
source: 'title',
})
).toEqual([`resources.posts.fields.title`, { _: 'Title' }]);
).toEqual([`resources.books.fields.title`, { _: 'Title' }]);
});

it('should accept use the parentSource to build the translation key if provided', () => {
it('should prefer the resource over the resourceFromContext', () => {
expect(
getFieldLabelTranslationArgs({
resource: 'posts',
source: 'url',
parentSource: 'backlinks',
resourceFromContext: 'books',
source: 'title',
})
).toEqual([`resources.posts.fields.title`, { _: 'Title' }]);
});

it('should prefer the prefix over the resourceFromContext', () => {
expect(
getFieldLabelTranslationArgs({
prefix: 'resources.posts.fields',
resourceFromContext: 'books',
source: 'title',
})
).toEqual([`resources.posts.fields.title`, { _: 'Title' }]);
});

it('should use the resourceFromContext when the resource and prefix are missing', () => {
expect(
getFieldLabelTranslationArgs({
resourceFromContext: 'books',
source: 'title',
})
).toEqual([`resources.posts.fields.backlinks.url`, { _: 'Url' }]);
).toEqual([`resources.books.fields.title`, { _: 'Title' }]);
});
});
Loading