Skip to content

Commit

Permalink
Add i18n to react material array renderers
Browse files Browse the repository at this point in the history
Part of #1826

Co-authored-by: Lucas Koehler <lkoehler@eclipsesource.com>
  • Loading branch information
LukasBoll and lucas-koehler committed Mar 6, 2023
1 parent 5e8c860 commit b4166e5
Show file tree
Hide file tree
Showing 13 changed files with 233 additions and 28 deletions.
38 changes: 38 additions & 0 deletions packages/core/src/i18n/arrayTranslations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface ArrayDefaultTranslation {
key: ArrayTranslationEnum,
default: (variable?: string) => string
}

export enum ArrayTranslationEnum{
addTooltip = 'addTooltip',
addAriaLabel = 'addAriaLabel',
removeTooltip = 'removeTooltip',
upAriaLabel = 'upAriaLabel',
downAriaLabel = 'downAriaLabel',
noSelection = 'noSelection',
removeAriaLabel = 'removeAriaLabel',
noDataMessage = 'noDataMessage',
deleteDialogTitle = 'deleteDialogTitle',
deleteDialogMessage = 'deleteDialogMessage',
deleteDialogAccept = 'deleteDialogAccept',
deleteDialogDecline = 'deleteDialogDecline'
}

export type ArrayTranslations = {
[key in ArrayTranslationEnum]?: string
}

export const arrayDefaultTranslations: ArrayDefaultTranslation[] = [
{key: ArrayTranslationEnum.addTooltip, default: (input) => input?`Add to ${input}`:'Add'},
{key: ArrayTranslationEnum.addAriaLabel, default: (input) => input?`Add to ${input}`:'Add'},
{key: ArrayTranslationEnum.removeTooltip, default: () => 'Delete'},
{key: ArrayTranslationEnum.upAriaLabel, default: (input) => `Move ${input} up`},
{key: ArrayTranslationEnum.downAriaLabel, default: (input) => `Move ${input} down`},
{key: ArrayTranslationEnum.removeAriaLabel, default: () => 'Delete button'},
{key: ArrayTranslationEnum.noDataMessage, default: () => 'No Data'},
{key: ArrayTranslationEnum.noSelection, default: () => 'No Selection'},
{key: ArrayTranslationEnum.deleteDialogTitle, default: () => 'Confirm Deletion'},
{key: ArrayTranslationEnum.deleteDialogMessage, default: () => 'Are you sure you want to delete the selected entry?'},
{key: ArrayTranslationEnum.deleteDialogAccept, default: () => 'Yes'},
{key: ArrayTranslationEnum.deleteDialogDecline, default: () => 'No'}
]
22 changes: 22 additions & 0 deletions packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isInternationalized, Labelable, UISchemaElement } from '../models';
import { getControlPath } from '../reducers';
import { formatErrorMessage } from '../util';
import type { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes';
import { ArrayDefaultTranslation, ArrayTranslations } from './arrayTranslations'

export const getI18nKeyPrefixBySchema = (
schema: i18nJsonSchema | undefined,
Expand Down Expand Up @@ -47,6 +48,13 @@ export const getI18nKey = (
return `${getI18nKeyPrefix(schema, uischema, path)}.${key}`;
};

export const addI18nKeyToPrefix = (
i18nKeyPrefix: string,
key: string
): string => {
return `${i18nKeyPrefix}.${key}`;
};

export const defaultTranslator: Translator = (_id: string, defaultMessage: string | undefined) => defaultMessage;

export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
Expand Down Expand Up @@ -123,3 +131,17 @@ export const deriveLabelForUISchemaElement = (uischema: Labelable<boolean>, t: T
const i18nKey = typeof i18nKeyPrefix === 'string' ? `${i18nKeyPrefix}.label` : stringifiedLabel;
return t(i18nKey, stringifiedLabel, { uischema: uischema });
}

export const getArrayTranslations = (
t: Translator,
defaultTranslations: ArrayDefaultTranslation [],
i18nKeyPrefix: string,
label: string
): ArrayTranslations => {
const translations:ArrayTranslations = {};
defaultTranslations.forEach((controlElement)=>{
const key = addI18nKeyToPrefix(i18nKeyPrefix, controlElement.key);
translations[controlElement.key]=t(key, controlElement.default(label));
})
return translations;
}
1 change: 1 addition & 0 deletions packages/core/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './i18nTypes';
export * from './i18nUtil';
export * from './arrayTranslations'
26 changes: 19 additions & 7 deletions packages/core/src/util/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ import { composePaths, composeWithUi } from './path';
import { CoreActions, update } from '../actions';
import type { ErrorObject } from 'ajv';
import type { JsonFormsState } from '../store';
import { deriveLabelForUISchemaElement, getCombinedErrorMessage, getI18nKey, getI18nKeyPrefix, getI18nKeyPrefixBySchema, Translator } from '../i18n';
import { deriveLabelForUISchemaElement, getCombinedErrorMessage, getI18nKey, getI18nKeyPrefix, getI18nKeyPrefixBySchema, getArrayTranslations, Translator } from '../i18n';
import { arrayDefaultTranslations, ArrayTranslations } from '../i18n/arrayTranslations';

const isRequired = (
schema: JsonSchema,
Expand Down Expand Up @@ -370,6 +371,8 @@ export interface StatePropsOfControl extends StatePropsOfScopedRenderer {
*/
required?: boolean;

i18nKeyPrefix?: string;

// TODO: renderers?
}

Expand Down Expand Up @@ -469,6 +472,7 @@ export const mapStateToControlProps = (
const schema = resolvedSchema ?? rootSchema;
const t = getTranslator()(state);
const te = getErrorTranslator()(state);
const i18nKeyPrefix = getI18nKeyPrefix(schema, uischema, path);
const i18nLabel = t(getI18nKey(schema, uischema, path, 'label'), label, {schema, uischema, path, errors} );
const i18nDescription = t(getI18nKey(schema, uischema, path, 'description'), description, {schema, uischema, path, errors});
const i18nErrorMessage = getCombinedErrorMessage(errors, te, t, schema, uischema, path);
Expand All @@ -487,7 +491,8 @@ export const mapStateToControlProps = (
schema,
config: getConfig(state),
cells: ownProps.cells || state.jsonforms.cells,
rootSchema
rootSchema,
i18nKeyPrefix
};
};

Expand Down Expand Up @@ -703,14 +708,15 @@ export const mapStateToArrayControlProps = (
const resolvedSchema = Resolve.schema(schema, 'items', props.rootSchema);
const childErrors = getSubErrorsAt(path, resolvedSchema)(state);


return {
...props,
path,
uischema,
schema: resolvedSchema,
childErrors,
renderers: ownProps.renderers || getRenderers(state),
cells: ownProps.cells || getCells(state)
cells: ownProps.cells || getCells(state),
};
};

Expand Down Expand Up @@ -771,7 +777,7 @@ export const mapDispatchToArrayControlProps = (
return array;
})
);
}
},
});

export interface DispatchPropsOfMultiEnumControl {
Expand Down Expand Up @@ -1011,6 +1017,7 @@ export const mapStateToOneOfProps = (

export interface StatePropsOfArrayLayout extends StatePropsOfControlWithDetail {
data: number;
translations: ArrayTranslations;
minItems?: number;
}
/**
Expand All @@ -1029,16 +1036,19 @@ export const mapStateToArrayLayoutProps = (
schema,
uischema,
errors,
i18nKeyPrefix,
label,
...props
} = mapStateToControlWithDetailProps(state, ownProps);


const resolvedSchema = Resolve.schema(schema, 'items', props.rootSchema);

const t = getTranslator()(state);
// TODO Does not consider 'i18n' keys which are specified in the ui schemas of the sub errors
const childErrors = getCombinedErrorMessage(
getSubErrorsAt(path, resolvedSchema)(state),
getErrorTranslator()(state),
getTranslator()(state),
t,
undefined,
undefined,
undefined
Expand All @@ -1050,12 +1060,14 @@ export const mapStateToArrayLayoutProps = (
childErrors;
return {
...props,
label,
path,
uischema,
schema: resolvedSchema,
data: props.data ? props.data.length : 0,
errors: allErrors,
minItems: schema.minItems
minItems: schema.minItems,
translations: getArrayTranslations(t,arrayDefaultTranslations,i18nKeyPrefix, label)
};
};

Expand Down
106 changes: 106 additions & 0 deletions packages/examples/src/examples/arraysI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
The MIT License
Copyright (c) 2017-2019 EclipseSource Munich
https://github.com/eclipsesource/jsonforms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { registerExamples } from '../register';
import { ArrayTranslationEnum, Translator } from '@jsonforms/core';
import { get } from 'lodash';

export const schema = {
type: 'object',
properties: {
comments: {
type: 'array',
items: {
type: 'object',
properties: {
date: {
type: 'string',
format: 'date'
},
message: {
type: 'string',
maxLength: 5
},
enum: {
type: 'string',
const: 'foo'
}
}
}
},
}
};

export const uischema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/comments',
options: {
showSortButtons: true,
}
}
]
};

export const data = {
comments: [
{
date: new Date(2001, 8, 11).toISOString().substr(0, 10),
message: 'This is an example message'
},
{
date: new Date().toISOString().substr(0, 10),
message: 'Get ready for booohay'
}
]
};

export const translations = {
comments: {
[ArrayTranslationEnum.noDataMessage]: 'Be the first to write a comment',
[ArrayTranslationEnum.addTooltip]: 'Add a Comment',
[ArrayTranslationEnum.deleteDialogAccept]: 'Delete!',
[ArrayTranslationEnum.deleteDialogDecline]: 'Cancel!',
[ArrayTranslationEnum.deleteDialogMessage]: 'Are you sure you want to delete this comment?'
}
};
export const translate: Translator = (key: string, defaultMessage: string) => {
return get(translations, key) ?? defaultMessage;
};

registerExamples([
{
name: 'array-i18n',
label: 'Array (i18n)',
data,
schema,
uischema,
i18n: {
translate: translate,
locale: 'en'
}
}
]);
2 changes: 2 additions & 0 deletions packages/examples/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as oneOf from './examples/oneOf';
import * as oneOfArray from './examples/oneOfArray';
import * as anyOfOneOfAllOfResolve from './examples/anyOf-oneOf-allOf-resolve';
import * as array from './examples/arrays';
import * as arrayI18n from './examples/arraysI18n';
import * as nestedArray from './examples/nestedArrays';
import * as arrayWithDetail from './examples/arrays-with-detail';
import * as arrayWithDetailAndRule from './examples/arrays-with-detail-and-rule';
Expand Down Expand Up @@ -97,6 +98,7 @@ export {
anyOfOneOfAllOfResolve,
stringArray,
array,
arrayI18n,
nestedArray,
arrayWithDetail,
arrayWithDetailAndRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export const MaterialListWithDetailRenderer = ({
renderers,
cells,
config,
rootSchema
rootSchema,
translations
}: ArrayLayoutProps) => {
const [selectedIndex, setSelectedIndex] = useState(undefined);
const handleRemoveItem = useCallback(
Expand Down Expand Up @@ -92,7 +93,7 @@ export const MaterialListWithDetailRenderer = ({
path,
undefined,
uischema,
rootSchema
rootSchema,
),
[uischemas, schema, uischema.scope, path, uischema, rootSchema]
);
Expand All @@ -105,6 +106,7 @@ export const MaterialListWithDetailRenderer = ({
return (
<Hidden xsUp={!visible}>
<ArrayLayoutToolbar
translations={translations}
label={computeLabel(
label,
required,
Expand Down Expand Up @@ -146,7 +148,7 @@ export const MaterialListWithDetailRenderer = ({
path={composePaths(path, `${selectedIndex}`)}
/>
) : (
<Typography variant='h6'>No Selection</Typography>
<Typography variant='h6'>{translations.noSelection}</Typography>
)}
</Grid>
</Grid>
Expand Down
14 changes: 9 additions & 5 deletions packages/material-renderers/src/complex/DeleteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ export interface DeleteDialogProps {
onClose(): void;
onConfirm(): void;
onCancel(): void;
title: string,
message: string,
acceptText: string,
declineText: string
}

export interface WithDeleteDialogSupport {
openDeleteDialog(path: string, data: number): void;
}

export const DeleteDialog = React.memo(({ open, onClose, onConfirm, onCancel }: DeleteDialogProps) => {
export const DeleteDialog = React.memo(({ open, onClose, onConfirm, onCancel, title, message, acceptText, declineText }: DeleteDialogProps) => {
return (
<Dialog
open={open}
Expand All @@ -53,19 +57,19 @@ export const DeleteDialog = React.memo(({ open, onClose, onConfirm, onCancel }:
aria-describedby='alert-dialog-confirmdelete-description'
>
<DialogTitle id='alert-dialog-confirmdelete-title'>
{'Confirm Deletion'}
{title}
</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-confirmdelete-description'>
Are you sure you want to delete the selected entry?
{message}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color='primary'>
No
{declineText}
</Button>
<Button onClick={onConfirm} color='primary'>
Yes
{acceptText}
</Button>
</DialogActions>
</Dialog>
Expand Down
Loading

0 comments on commit b4166e5

Please sign in to comment.