Skip to content

Commit 8adeea2

Browse files
committed
Rework i18n support in JSON Forms core
Labels, descriptions and error messages can now be translated via special translation functions handed over to JSON Forms. The translations are automatically handed within the default mapping functions and are therefore available in all renderer sets. This includes a key-determination algorithm, allowing to either rely on using labels as keys or specifying 'i18n' keys in UI Schema options or directly within the JSON Schema. Errors are handled separately to allow for maximum flexibility. Includes test cases for the most common mapping functions. Also AJV is set to non-strict by default to not throw errors when handing over JSON Schemas containing 'i18n' keys.
1 parent 81e4dd6 commit 8adeea2

File tree

20 files changed

+929
-247
lines changed

20 files changed

+929
-247
lines changed

packages/angular/src/jsonforms-root.component.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
Component,
2727
EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges
2828
} from '@angular/core';
29-
import { Actions, JsonFormsRendererRegistryEntry, JsonSchema, UISchemaElement, UISchemaTester, ValidationMode } from '@jsonforms/core';
29+
import { Actions, JsonFormsI18nState, JsonFormsRendererRegistryEntry, JsonSchema, UISchemaElement, UISchemaTester, ValidationMode } from '@jsonforms/core';
3030
import Ajv, { ErrorObject } from 'ajv';
3131
import { JsonFormsAngularService, USE_STATE_VALUE } from './jsonforms.service';
3232
@Component({
@@ -42,11 +42,11 @@ export class JsonForms implements OnChanges, OnInit {
4242
@Input() renderers: JsonFormsRendererRegistryEntry[];
4343
@Input() uischemas: { tester: UISchemaTester; uischema: UISchemaElement; }[];
4444
@Output() dataChange = new EventEmitter<any>();
45-
@Input() locale: string;
4645
@Input() readonly: boolean;
4746
@Input() validationMode: ValidationMode;
4847
@Input() ajv: Ajv;
4948
@Input() config: any;
49+
@Input() i18n: JsonFormsI18nState;
5050
@Output() errors = new EventEmitter<ErrorObject[]>();
5151

5252
private previousData:any;
@@ -67,7 +67,7 @@ export class JsonForms implements OnChanges, OnInit {
6767
validationMode: this.validationMode
6868
},
6969
uischemas: this.uischemas,
70-
i18n: { locale: this.locale, localizedSchemas: undefined, localizedUISchemas: undefined },
70+
i18n: this.i18n,
7171
renderers: this.renderers,
7272
config: this.config,
7373
readonly: this.readonly
@@ -97,7 +97,7 @@ export class JsonForms implements OnChanges, OnInit {
9797
const newUiSchema = changes.uischema;
9898
const newRenderers = changes.renderers;
9999
const newUischemas = changes.uischemas;
100-
const newLocale = changes.locale;
100+
const newI18n = changes.i18n;
101101
const newReadonly = changes.readonly;
102102
const newValidationMode = changes.validationMode;
103103
const newAjv = changes.ajv;
@@ -121,8 +121,10 @@ export class JsonForms implements OnChanges, OnInit {
121121
this.jsonformsService.setUiSchemas(newUischemas.currentValue);
122122
}
123123

124-
if (newLocale && !newLocale.isFirstChange()) {
125-
this.jsonformsService.setLocale(newLocale.currentValue);
124+
if (newI18n && !newI18n.isFirstChange()) {
125+
this.jsonformsService.updateI18n(
126+
Actions.updateI18n(newI18n.currentValue?.locale, newI18n.currentValue?.translate, newI18n.currentValue?.translateError)
127+
);
126128
}
127129

128130
if (newReadonly && !newReadonly.isFirstChange()) {

packages/angular/src/jsonforms.service.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,16 @@ import {
3434
JsonFormsState,
3535
JsonFormsSubStates,
3636
JsonSchema,
37-
LocaleActions,
37+
I18nActions,
3838
RankedTester,
3939
setConfig,
4040
SetConfigAction,
4141
UISchemaActions,
4242
UISchemaElement,
4343
uischemaRegistryReducer,
4444
UISchemaTester,
45-
ValidationMode
45+
ValidationMode,
46+
updateI18n
4647
} from '@jsonforms/core';
4748
import { BehaviorSubject, Observable } from 'rxjs';
4849
import { JsonFormsBaseRenderer } from './base.renderer';
@@ -56,9 +57,10 @@ export class JsonFormsAngularService {
5657
private _state: JsonFormsSubStates;
5758
private state: BehaviorSubject<JsonFormsState>;
5859

59-
init(initialState: JsonFormsSubStates = { core: { data: undefined, schema: undefined, uischema: undefined } }) {
60+
init(initialState: JsonFormsSubStates = { core: { data: undefined, schema: undefined, uischema: undefined, validationMode: 'ValidateAndShow' } }) {
6061
this._state = initialState;
6162
this._state.config = configReducer(undefined, setConfig(this._state.config));
63+
this._state.i18n = i18nReducer(this._state.i18n, updateI18n(this._state.i18n?.locale, this._state.i18n?.translate, this._state.i18n?.translateError));
6264
this.state = new BehaviorSubject({ jsonforms: this._state });
6365
const data = initialState.core.data;
6466
const schema = initialState.core.schema ?? generateJsonSchema(data);
@@ -117,11 +119,11 @@ export class JsonFormsAngularService {
117119
this.updateSubject();
118120
}
119121

120-
updateLocale<T extends LocaleActions>(localeAction: T): T {
121-
const localeState = i18nReducer(this._state.i18n, localeAction);
122-
this._state.i18n = localeState;
122+
updateI18n<T extends I18nActions>(i18nAction: T): T {
123+
const i18nState = i18nReducer(this._state.i18n, i18nAction);
124+
this._state.i18n = i18nState;
123125
this.updateSubject();
124-
return localeAction;
126+
return i18nAction;
125127
}
126128

127129
updateCore<T extends CoreActions>(coreAction: T): T {

packages/core/src/actions/actions.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { generateDefaultUISchema, generateJsonSchema } from '../generators';
2929

3030
import { RankedTester } from '../testers';
3131
import { UISchemaTester, ValidationMode } from '../reducers';
32+
import { ErrorTranslator, Translator } from '../i18n';
3233

3334
export const INIT: 'jsonforms/INIT' = 'jsonforms/INIT';
3435
export const UPDATE_CORE: 'jsonforms/UPDATE_CORE' = `jsonforms/UPDATE_CORE`;
@@ -51,10 +52,10 @@ export const SET_VALIDATION_MODE: 'jsonforms/SET_VALIDATION_MODE' =
5152
'jsonforms/SET_VALIDATION_MODE';
5253

5354
export const SET_LOCALE: 'jsonforms/SET_LOCALE' = `jsonforms/SET_LOCALE`;
54-
export const SET_LOCALIZED_SCHEMAS: 'jsonforms/SET_LOCALIZED_SCHEMAS' =
55-
'jsonforms/SET_LOCALIZED_SCHEMAS';
56-
export const SET_LOCALIZED_UISCHEMAS: 'jsonforms/SET_LOCALIZED_UISCHEMAS' =
57-
'jsonforms/SET_LOCALIZED_UISCHEMAS';
55+
export const SET_TRANSLATOR: 'jsonforms/SET_TRANSLATOR' =
56+
'jsonforms/SET_TRANSLATOR';
57+
export const UPDATE_I18N: 'jsonforms/UPDATE_I18N' =
58+
'jsonforms/UPDATE_I18N';
5859

5960
export const ADD_DEFAULT_DATA: 'jsonforms/ADD_DEFAULT_DATA' = `jsonforms/ADD_DEFAULT_DATA`;
6061
export const REMOVE_DEFAULT_DATA: 'jsonforms/REMOVE_DEFAULT_DATA' = `jsonforms/REMOVE_DEFAULT_DATA`;
@@ -275,33 +276,21 @@ export const unregisterUISchema = (
275276
};
276277
};
277278

278-
export type LocaleActions =
279+
export type I18nActions =
279280
| SetLocaleAction
280-
| SetLocalizedSchemasAction
281-
| SetLocalizedUISchemasAction;
281+
| SetTranslatorAction
282+
| UpdateI18nAction
282283

283284
export interface SetLocaleAction {
284285
type: 'jsonforms/SET_LOCALE';
285-
locale: string;
286+
locale: string | undefined;
286287
}
287288

288-
export const setLocale = (locale: string): SetLocaleAction => ({
289+
export const setLocale = (locale: string | undefined): SetLocaleAction => ({
289290
type: SET_LOCALE,
290291
locale
291292
});
292293

293-
export interface SetLocalizedSchemasAction {
294-
type: 'jsonforms/SET_LOCALIZED_SCHEMAS';
295-
localizedSchemas: Map<string, JsonSchema>;
296-
}
297-
298-
export const setLocalizedSchemas = (
299-
localizedSchemas: Map<string, JsonSchema>
300-
): SetLocalizedSchemasAction => ({
301-
type: SET_LOCALIZED_SCHEMAS,
302-
localizedSchemas
303-
});
304-
305294
export interface SetSchemaAction {
306295
type: 'jsonforms/SET_SCHEMA';
307296
schema: JsonSchema;
@@ -312,16 +301,37 @@ export const setSchema = (schema: JsonSchema): SetSchemaAction => ({
312301
schema
313302
});
314303

315-
export interface SetLocalizedUISchemasAction {
316-
type: 'jsonforms/SET_LOCALIZED_UISCHEMAS';
317-
localizedUISchemas: Map<string, UISchemaElement>;
304+
export interface SetTranslatorAction {
305+
type: 'jsonforms/SET_TRANSLATOR';
306+
translator?: Translator;
307+
errorTranslator?: ErrorTranslator;
308+
}
309+
310+
export const setTranslator = (
311+
translator?: Translator,
312+
errorTranslator?: ErrorTranslator
313+
): SetTranslatorAction => ({
314+
type: SET_TRANSLATOR,
315+
translator,
316+
errorTranslator
317+
});
318+
319+
export interface UpdateI18nAction {
320+
type: 'jsonforms/UPDATE_I18N';
321+
locale: string | undefined;
322+
translator: Translator | undefined;
323+
errorTranslator: ErrorTranslator | undefined;
318324
}
319325

320-
export const setLocalizedUISchemas = (
321-
localizedUISchemas: Map<string, UISchemaElement>
322-
): SetLocalizedUISchemasAction => ({
323-
type: SET_LOCALIZED_UISCHEMAS,
324-
localizedUISchemas
326+
export const updateI18n = (
327+
locale: string | undefined,
328+
translator: Translator | undefined,
329+
errorTranslator: ErrorTranslator | undefined
330+
): UpdateI18nAction => ({
331+
type: UPDATE_I18N,
332+
locale,
333+
translator,
334+
errorTranslator
325335
});
326336

327337
export interface SetUISchemaAction {

packages/core/src/i18n/i18nTypes.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ErrorObject } from 'ajv';
2+
import { JsonSchema, UISchemaElement } from '../models';
3+
4+
export type Translator = {
5+
(id: string, defaultMessage: string, values?: any): string;
6+
(id: string, defaultMessage: undefined, values?: any): string | undefined;
7+
}
8+
9+
export type ErrorTranslator = (error: ErrorObject, translate: Translator, uischema?: UISchemaElement) => string;
10+
11+
export interface JsonFormsI18nState {
12+
locale: string;
13+
translate: Translator;
14+
translateError: ErrorTranslator;
15+
}
16+
17+
export type i18nJsonSchema = JsonSchema & {i18n?: string};

packages/core/src/i18n/i18nUtil.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ErrorObject } from 'ajv';
2+
import { UISchemaElement } from '../models';
3+
import { formatErrorMessage } from '../util';
4+
import { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes';
5+
6+
export const getI18nKey = (
7+
schema: i18nJsonSchema | undefined,
8+
uischema: UISchemaElement | undefined,
9+
key: string
10+
): string | undefined => {
11+
if (uischema?.options?.i18n) {
12+
return `${uischema.options.i18n}.${key}`;
13+
}
14+
if (schema?.i18n) {
15+
return `${schema.i18n}.${key}`;
16+
}
17+
return undefined;
18+
};
19+
20+
export const defaultTranslator: Translator = (_id: string, defaultMessage: string | undefined) => defaultMessage;
21+
22+
export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
23+
// check whether there is a special keyword message
24+
const keyInSchemas = getI18nKey(
25+
error.parentSchema,
26+
uischema,
27+
`error.${error.keyword}`
28+
);
29+
const specializedKeywordMessage = keyInSchemas && t(keyInSchemas, undefined);
30+
if (specializedKeywordMessage !== undefined) {
31+
return specializedKeywordMessage;
32+
}
33+
34+
// check whether there is a generic keyword message
35+
const genericKeywordMessage = t(`error.${error.keyword}`, undefined);
36+
if (genericKeywordMessage !== undefined) {
37+
return genericKeywordMessage;
38+
}
39+
40+
// check whether there is a customization for the default message
41+
const messageCustomization = t(error.message, undefined);
42+
if (messageCustomization !== undefined) {
43+
return messageCustomization;
44+
}
45+
46+
// rewrite required property messages (if they were not customized) as we place them next to the respective input
47+
if (error.keyword === 'required') {
48+
return t('is a required property', 'is a required property');
49+
}
50+
51+
return error.message;
52+
};
53+
54+
/**
55+
* Returns the determined error message for the given errors.
56+
* All errors must correspond to the given schema and uischema.
57+
*/
58+
export const getCombinedErrorMessage = (
59+
errors: ErrorObject[],
60+
et: ErrorTranslator,
61+
t: Translator,
62+
schema?: i18nJsonSchema,
63+
uischema?: UISchemaElement
64+
) => {
65+
if (errors.length > 0 && t) {
66+
// check whether there is a special message which overwrites all others
67+
const keyInSchemas = getI18nKey(schema, uischema, 'error.custom');
68+
const specializedErrorMessage = keyInSchemas && t(keyInSchemas, undefined);
69+
if (specializedErrorMessage !== undefined) {
70+
return specializedErrorMessage;
71+
}
72+
}
73+
return formatErrorMessage(
74+
errors.map(error => et(error, t, uischema))
75+
);
76+
};

packages/core/src/i18n/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './i18nTypes';
2+
export * from './i18nUtil';

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ export * from './util';
3434

3535
export * from './Helpers';
3636
export * from './store';
37+
export * from './i18n';

packages/core/src/reducers/core.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const initState: JsonFormsCore = {
7777
errors: [],
7878
validator: undefined,
7979
ajv: undefined,
80-
validationMode: 'ValidateAndShow'
80+
validationMode: 'ValidateAndShow',
8181
};
8282

8383
const reuseAjvForSchema = (ajv: Ajv, schema: JsonSchema): Ajv => {
@@ -154,7 +154,7 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
154154
errors: e,
155155
validator: v,
156156
ajv: thisAjv,
157-
validationMode
157+
validationMode,
158158
};
159159
}
160160
case UPDATE_CORE: {
@@ -184,17 +184,17 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
184184
state.ajv !== thisAjv ||
185185
state.errors !== errors ||
186186
state.validator !== validator ||
187-
state.validationMode !== validationMode;
187+
state.validationMode !== validationMode
188188
return stateChanged
189189
? {
190190
...state,
191-
data: state.data === action.data ? state.data : action.data,
192-
schema: state.schema === action.schema ? state.schema : action.schema,
193-
uischema: state.uischema === action.uischema ? state.uischema : action.uischema,
194-
ajv: thisAjv === state.ajv ? state.ajv : thisAjv,
191+
data: action.data,
192+
schema: action.schema,
193+
uischema: action.uischema,
194+
ajv: thisAjv,
195195
errors: isEqual(errors, state.errors) ? state.errors : errors,
196-
validator: validator === state.validator ? state.validator : validator,
197-
validationMode: validationMode === state.validationMode ? state.validationMode : validationMode
196+
validator: validator,
197+
validationMode: validationMode,
198198
}
199199
: state;
200200
}

0 commit comments

Comments
 (0)