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 initial i18n documentation #222

Merged
merged 9 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
243 changes: 243 additions & 0 deletions content/docs/i18n.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
---
id: i18n
title: i18n
description: Translate jsonforms
---

import I18nExample from '../../src/components/docs/i18n';

:::note

Please note that the renderer sets themselves are not yet translatable.
You can find the current status here:
https://github.com/eclipsesource/jsonforms/issues/1826

:::

The translate functionality of JSONForms is integrated into the core component.
In order to translate JSONForms, you need to set a translation function and provide it to the JSONForms object:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
```ts
const translator = (key, defaultMessage) => {
console.log(`Key: ${key}, Default Message: ${defaultMessage}`);
return defaultMessage;
};

const [locale, setLocale] = useState<'de'|'en'>('de');

<JsonForms
i18n={{locale: locale, translate: translator}}
...
/>
```

The i18n prop consist of three components: `locale`, `translate`, `translateError`.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

## `locale`

Allows to specify the current locale of the application.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Allows to specify the current locale of the application.
Specifies the current locale of the application.

This can be used by renderers to render locale specific UI elements (for example formatting of dates).
TheZoker marked this conversation as resolved.
Show resolved Hide resolved


## `translate`

Allows to provide a translation function, which handles the actual translation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Allows to provide a translation function, which handles the actual translation.
Provides a translation function handling the actual translation.


:::caution

The translate function should be side effect free and should be stable (memoized) to avoid unnecessary re-renderings, i.e. the translate function should only change if there are new translations.

:::

TheZoker marked this conversation as resolved.
Show resolved Hide resolved

The translate function has the following parameters:

### `key`

The key is used to identify the string, that needs to be translated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The key is used to identify the string, that needs to be translated.
The key is used to identify the string that needs to be translated.

It can be set using the `UI Schema`, the `JSON Schema` or is generated by default based on the property path.

#### UI Schema option

The key can be set via the `i18n` UI Schema option:
```json
{
"type": "Control",
"label": "name",
"scope": "#/properties/name",
"options": {
"i18n": "customName"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed. "i18n" is now a direct child property of the Control, i.e. a sibling to scope, etc. See eclipsesource/jsonforms#1944

}
```

Therefore the translation will be invoked with `customName.label` & `customName.description`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Therefore the translation will be invoked with `customName.label` & `customName.description`.
Therefore, the translation will be invoked with `customName.label` & `customName.description`.


#### JSON Schema

The key can also be set with the custom JSON Schema `i18n` option:
```json
{
"name": {
"type": "string",
"i18n": "myCustomName"
}
}
```

Therefore the translation will be invoked with `myCustomName.label` & `myCustomName.description`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Therefore the translation will be invoked with `myCustomName.label` & `myCustomName.description`.
Therefore, the translation will be invoked with `myCustomName.label` & `myCustomName.description`.


#### Default: Property path

Is none of the above is set, the property path will be used as the key.
e.g. `name`.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

```
{
properties: {
firstName: {
type: "string"
},
address: {
type: "object",
properties: {
street: {
type: "string"
}
}
}
comments: {
type: "array",
items: {
type: "object",
properties: {
message: {
type: "string"
},
}
}
}
}
}
```

The paths in the above example would be:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The paths in the above example would be:
The paths in the above example are:

- `firstName.label` & `firstName.description`
- `address.street.label` & `address.street.description`
- `comments.message.label` & `comments.message.description` (the path for arrays will not contain indices)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check whether address.label, address.description and comments.label, comments.description is also called?


### `defaultMessage`

The default message is provided by JSONForms and can act as a fallback or could be translated.

:::note

If the defaultMessage is `undefined` it should not be modified and still return as `undefined` (and not an empty string or similar).
JSON Forms will use `undefined when the message could be skipped or another generic key could be tried.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

:::

### `values`

The values can contain additional context, if the key of the string is not enough to determine the correct translation.
For example all error translations will have the error as part of the values object.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

TheZoker marked this conversation as resolved.
Show resolved Hide resolved
## Error Customizations

TODO: Description
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

#### `<i18nkey>.error.custom`
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

TheZoker marked this conversation as resolved.
Show resolved Hide resolved
If `error.custom` is set, just a single error will be output, no matter how many errors are present.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
The `translate` function is called for `error.custom`
The `translateError` function is called for each error.
By default it behaves like this:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

## `translateError`

The `translateError` is called for every AJV error.
`translateError` is an optional parameter to the i18n object. Usually it is not expected to customize this option.
By default error it works like this:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

`<i18nkey>` in the sections below refers to the key of the field (see `key` section above).

### Evaluation order

#### `<i18nkey>.error.<keyword>`

e.g. `name.error.required`
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

If no `error.custom` is set, the translator will look for a concrete message for a specific field and a specific error.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
In the example above, the function will look for a `required` error on the `name` field.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

#### `error.<keyword>`

e.g. `error.required`
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

If no specific field error message is set, the translator will then look for an error message, that is independent of the field.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
In the example case above, the function will look for a `required` error translation.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

#### Default error translator
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

Next the translator will look for a default message, which is provided by the core framework.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

#### Default AJV error message

If none of the above are set and no default error is available, the error message provided by AJV will be output.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

### Example

If we have the following schema:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
```js
{
phone: {
type: "string",
minLength: 10,
pattern: "^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$"
}
}
```
And the order in which the error keys are evaluated is the following:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
- `phone.error.custom`: if this is set, the `pattern` and `minLength` error will be ignore and just this message is output
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
- `phone.error.pattern` & `phone.error.minLength`
- `error.pattern` & `error.minLength`

## Enum translation

Enum translations are a little different then the other control translations.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
In order to translate enum values, you need to add one additional attribute to the translation for every value.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
For example:
Let's assume we have a field `Gender`, which has three possible values:
- `Male`
- `Female`
- `Other`
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

If we now want to translate these, we need to reference them in the translation like this:
```js
gender: {
label: 'Geschlecht',
description: 'Das Geschlecht auswählen',
Male: 'Männlich',
Female: 'Weiblich',
Other: 'Divers'
},
```

So the `label` attribute translates the `label` of the field, just like with any other control field. The same with the `description` attribute.
All the other options translate the possible values of the enum.
TheZoker marked this conversation as resolved.
Show resolved Hide resolved

## Access translation in custom renderer sets

If you want to access the translation function within a custom renderer set, you can use the JSONForms context for that:
TheZoker marked this conversation as resolved.
Show resolved Hide resolved
```
const ctx = useJsonForms();

const locale = ctx.i18n.locale;
const translate = ctx.i18n.translate;
const translateError = ctx.i18n.translateError;
```

With this you can for example change phone number patterns based on the current locale for validation.

## Example

<I18nExample />
2 changes: 1 addition & 1 deletion content/pages/assets/support.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
h3 {
.comparison_container h3 {
font-size: 2rem;
padding: 0;
}
Expand Down
142 changes: 142 additions & 0 deletions src/components/docs/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useMemo, useState } from 'react';
import get from 'lodash/get';
import {
materialCells,
materialRenderers,
} from '@jsonforms/material-renderers';
import Button from '@mui/material/Button';
import { JsonForms } from '@jsonforms/react';

const i18n = {
schema: {
properties: {
firstName: {
type: "string"
},
lastName: {
type: "string"
},
email: {
type: "string"
},
gender: {
type: "string",
enum: [
"Male",
"Female",
"Other"
]
}
},
required: [
"email"
]
},
uischema: {
type: "VerticalLayout",
elements: [
{
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/firstName"
},
{
type: "Control",
scope: "#/properties/lastName"
}
]
},
{
type: "Control",
scope: "#/properties/email"
},
{
type: "Control",
scope: "#/properties/gender"
}
]
}
}

export const en = {
firstName: {
label: 'First Name',
description: 'The first name of the person',
},
lastName: {
label: 'Last Name',
},
email: {
label: 'Email'
},
gender: {
label: 'Gender',
Male: 'Male',
Female: 'Female',
Other: 'Diverse'
},
error: {
required: 'This field is required',
},
};

export const de = {
firstName: {
label: 'Vorname',
description: 'Der Vorname der Person',
},
lastName: {
label: 'Nachname',
},
email: {
label: 'Email'
},
gender: {
label: 'Geschlecht',
Male: 'Männlich',
Female: 'Weiblich',
Other: 'Divers'
},
error: {
required: 'Dieses Feld muss ausgefüllt werden.',
},
};

const I18nExample = () => {
const [locale, setLocale] = useState('de');

const createTranslator = (locale) => (key, defaultMessage) => {
return get(locale === 'en' ? en : de, key) ?? defaultMessage;
};

const translation = useMemo(() => createTranslator(locale), [locale]);

const switchLocale = () => {
if(locale === 'en') {
setLocale('de');
} else {
setLocale('en');
}
};

return (
<div>
<JsonForms
schema={i18n.schema}
uischema={i18n.uischema}
i18n={{locale: locale, translate: translation}}
renderers={materialRenderers}
cells={materialCells}
/>
<Button onClick={switchLocale} color='primary' variant='contained'>
Switch language
</Button>
<br />
Current language: {locale}
</div>
);
};

export default I18nExample;
Loading