Skip to content

Commit

Permalink
Add initial i18n documentation (#222)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Dirix <sdirix@eclipsesource.com>
Co-authored-by: LukasBoll <ga24xeg@mytum.de>
Co-authored-by: Lucas Koehler <lkoehler@eclipsesource.com>
  • Loading branch information
4 people authored Oct 13, 2022
1 parent 0f91562 commit c5fe90f
Show file tree
Hide file tree
Showing 4 changed files with 535 additions and 1 deletion.
357 changes: 357 additions & 0 deletions content/docs/i18n.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
---
id: i18n
title: i18n
description: Translate jsonforms
---

import { I18nExample, ValuesTable } 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 JSON Forms is integrated into the core component.
In order to translate JSON Forms, you need to set a translation function and provide it to the JSON Forms component:
```ts
const translator = (key, defaultMessage) => {
console.log(`Key: ${key}, Default Message: ${defaultMessage}`);
return defaultMessage;
};

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

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

The `i18n` prop consists of three components: `locale`, `translate` and `translateError`.

## `locale`

Specifies the current locale of the application.
This can be used by renderers to render locale specific UI elements, for example for locale-aware formatting of numbers.


## `translate`

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.

:::

The type of the translate is

```ts
(key: string, defaultMessage: string | undefined, context: any) => string | undefined)
```

with the following parameters:

### `key`

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",
"i18n": "customName"
}
```

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`.

#### Default: Property path

If none of the above is set, the property path will be used as the key.

```json
{
"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 are:
- `firstName.label` & `firstName.description`
- `address.label` & `address.description`
- `address.street.label` & `address.street.description`
- `comments.message.label` & `comments.message.description` (the path for arrays will not contain indices)

### `defaultMessage`

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

:::note

If the `defaultMessage` is `undefined`, you should also return `undefined` if there is no translation for the given key.
Returning an empty string (or something similar) instead may result in undesired behavior.
JSON Forms will use `undefined` when the message could be skipped or another more generic key could be tried.

:::

### `context`

`context` can contain additional information for the current translation. The following parameters can be provided:

<ValuesTable/>

Schema translations provide all properties, while UI schema translations only provide the `uischema` property.

## `translateError`

The `translateError` function is called whenever a single message is to be extracted from an AJV error object.

The type of the `translateError` function is

```ts
(error: ErrorObject, translate: Translator, uischema?: UISchemaElement) => string
```

* The `error` is the AJV error object
* `translate` is the i18n `translate` function handed over to JSON Forms
* In cases where a UI Schema Element can be correlated to an `error` it will also be handed over
* The `translateError` function always returns a `string`

Usually this method does not need to be customized as JSON Forms already provides a sensible default implementation.
A reason to customize this method could be to integrate third party frameworks like `ajv-i18n`.

For more information about how errors can be customized, see the following section:

## Error Customizations

For each control a list of errors is determined.
This section describes the default behavior of JSON forms to offer translation support for them.

### `<i18nkey>.error.custom`

Before invoking the `translateError` function, JSON Forms will check whether a `<i18nkey>.error.custom` translation exists.
This is useful if there are many JSON Schema validaton keywords defined for a single property, but a single cohesive message shall be displayed.

If no `<i18nkey>.error.custom` message is returned by the `translate` function, `translateError` will be called for each AJV error and the results combined.

The default implementation of `translateError` will invoke `translate` multiple times to determine the best message for the given error. Therefore it's usually not necessary to customize `translateError` itself.
By default error it works like this:

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

### Evaluation order

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

Example keys: `name.error.required`, `address.street.error.pattern`

The default `translateError` will first look for a concrete message for a specific field and a specific error type.

#### `error.<keyword>`

Example keys: `error.required`, `error.pattern`

After checking field specific translations, `translateError` will then look for form-wide translations of errors, independent of each respective field.
This is useful to customize for example `required` or `pattern` messages for all properties.

#### error message

At last the default `translateError` implementation will check whether the `message` of the error object has a specific translation.

#### Default AJV error message

If none of the above apply, the `message` provided by the AJV error object will be used.

### Example

Consider the following schema for an object attribute:
```js
{
phone: {
type: "string",
minLength: 10,
pattern: "^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$"
}
}
```
The order in which the error keys are evaluated is the following:
- `phone.error.custom`: if this is set, the `pattern` and `minLength` errors will be ignores and just this message is used
- `phone.error.pattern` & `phone.error.minLength`
- `error.pattern` & `error.minLength`

## Enum translation

Enum translations are based on the respective entries:
- For JSON Schema `enum`, the stringified value is used.
- For JSON Schema `oneOf` enums which consist of (`title`, `const`) pairs a specialized `i18n` key or `title` is used.

Therefore, in order to translate enum values, an additional key is checked for each enum entry.
For example:
Let's assume we have an enum attribute `gender`, which looks like this:

```js
{
gender: {
type: "string",
enum: ["male", "female", "other"]
}
}
```

In this case the `translate` function will be invoked with the keys `gender.male`, `gender.female` and `gender.other` to translate these enum values. In case `gender` had an `i18n` property, it would be used instead, i.e. `<i18nkey>.male` etc.

Let's assume we have a `oneOf` enum attribute gender which looks like this:

```js
{
gender: {
oneOf: [
{
title: "Male",
const: 0
},
{
title: "Female",
const: "f",
i18n: "fem"
},
{
const: null
}
]
}
}
```

Here the requested keys are:

* `gender.Male` - property path + `title` of the `oneOf` entry
* `fem` - direct usage of the `i18n` property for the `oneOf` entry
* `null` - the `title` attribute is missing, therefore the `null` value is stringified to `'null'`.

## UI Schema Translation

The UI schema has the elements `group`, `category` and `label`, which can also be translated.

If a i18n-key is provided in a `group` or `category` element, `<i18n>.label` will be used as key.
If no i18n key is provided, the value of the `label`-property is used as key.
In case neither a i18n-key nor a label is provided, `<property-path>.label` will be used as default key.

The `label` UI schema element will use `<i18n>.text` as key, if provided.
If no i18n key is provided, the value of the `text`-property is used as key.


Let's assume we have the following UI schema:

```js
const uischema = {
type: 'VerticalLayout',
elements: [
{
type: 'Control',
scope: '#/properties/user',
options: {
detail: {
type: 'Group',
i18n: 'i18nUser',
}
}
},
{
type: 'Control',
scope: '#/properties/address',
options: {
detail: {
type: 'Group',
label: 'labelAddress',
}
}
},
{
type: 'Control',
scope: '#/properties/address'
},
{
type: 'Label',
i18n: 'i18nLabel'
},
{
type: 'Label',
text: 'textLabel'
},
]
};
```

Here the requested keys are:

* `i18nUser.label` - i18n + `label`
* `labelAddress` - direct usage of the `label` property
* `address.label` - property path + `label`
* `i18nLabel.text` - i18n + `text`
* `textLabel` - direct usage of the `text` property

## Access translation in custom renderer sets

If you want to directly access the i18n properties within a custom renderer, you can use the JSON Forms context for that:

```js
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
Loading

0 comments on commit c5fe90f

Please sign in to comment.