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

Rework i18n support in JSON Forms core #1825

Merged
merged 1 commit into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions packages/angular-material/example/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const itemTester: UISchemaTester = (_schema, schemaPath, _path) => {
[schema]="selectedExample.schema"
[uischema]="selectedExample.uischema"
[renderers]="renderers"
[locale]="currentLocale"
[i18n]="i18n"
[uischemas]="uischemas"
[readonly]="readonly"
[config]="config"
Expand All @@ -86,7 +86,9 @@ export class AppComponent {
readonly renderers = angularMaterialRenderers;
readonly examples = getExamples();
selectedExample: ExampleDescription;
currentLocale = 'en-US';
i18n = {
locale: 'en-US'
}
private readonly = false;
data: any;
uischemas: { tester: UISchemaTester; uischema: UISchemaElement; }[] = [
Expand All @@ -102,7 +104,7 @@ export class AppComponent {
}

changeLocale(locale: string) {
this.currentLocale = locale;
this.i18n.locale = locale;
}

toggleReadonly() {
Expand Down
6 changes: 0 additions & 6 deletions packages/angular-material/test/number-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,6 @@ describe(
getJsonFormsService(component).init({
core: state, i18n: {
locale: 'en',
localizedSchemas: undefined,
localizedUISchemas: undefined
}
});
getJsonFormsService(component).updateCore(
Expand All @@ -168,8 +166,6 @@ describe(
getJsonFormsService(component).init({
core: state, i18n: {
locale: 'en',
localizedSchemas: undefined,
localizedUISchemas: undefined
},config: {
useGrouping: false
},
Expand All @@ -196,8 +192,6 @@ describe(
getJsonFormsService(component).init({
core: state, i18n: {
locale: 'en',
localizedSchemas: undefined,
localizedUISchemas: undefined
},config: {
useGrouping: true
},
Expand Down
25 changes: 17 additions & 8 deletions packages/angular/src/jsonforms-root.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@
THE SOFTWARE.
*/
import {
Component,
EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges
Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges
} from '@angular/core';
import { Actions, JsonFormsRendererRegistryEntry, JsonSchema, UISchemaElement, UISchemaTester, ValidationMode } from '@jsonforms/core';
import { Actions, JsonFormsI18nState, JsonFormsRendererRegistryEntry, JsonSchema, UISchemaElement, UISchemaTester, ValidationMode } from '@jsonforms/core';
import Ajv, { ErrorObject } from 'ajv';
import { JsonFormsAngularService, USE_STATE_VALUE } from './jsonforms.service';
@Component({
Expand All @@ -42,11 +41,11 @@ export class JsonForms implements OnChanges, OnInit {
@Input() renderers: JsonFormsRendererRegistryEntry[];
@Input() uischemas: { tester: UISchemaTester; uischema: UISchemaElement; }[];
@Output() dataChange = new EventEmitter<any>();
@Input() locale: string;
@Input() readonly: boolean;
@Input() validationMode: ValidationMode;
@Input() ajv: Ajv;
@Input() config: any;
@Input() i18n: JsonFormsI18nState;
@Output() errors = new EventEmitter<ErrorObject[]>();

private previousData:any;
Expand All @@ -67,7 +66,7 @@ export class JsonForms implements OnChanges, OnInit {
validationMode: this.validationMode
},
uischemas: this.uischemas,
i18n: { locale: this.locale, localizedSchemas: undefined, localizedUISchemas: undefined },
i18n: this.i18n,
renderers: this.renderers,
config: this.config,
readonly: this.readonly
Expand All @@ -87,6 +86,14 @@ export class JsonForms implements OnChanges, OnInit {
this.initialized = true;
}

ngDoCheck(): void {
// we can't use ngOnChanges as then nested i18n changes will not be detected
// the update will result in a no-op when the parameters did not change
this.jsonformsService.updateI18n(
Actions.updateI18n(this.i18n?.locale, this.i18n?.translate, this.i18n?.translateError)
);
}

// tslint:disable-next-line: cyclomatic-complexity
ngOnChanges(changes: SimpleChanges): void {
if (!this.initialized) {
Expand All @@ -97,7 +104,7 @@ export class JsonForms implements OnChanges, OnInit {
const newUiSchema = changes.uischema;
const newRenderers = changes.renderers;
const newUischemas = changes.uischemas;
const newLocale = changes.locale;
const newI18n = changes.i18n;
const newReadonly = changes.readonly;
const newValidationMode = changes.validationMode;
const newAjv = changes.ajv;
Expand All @@ -121,8 +128,10 @@ export class JsonForms implements OnChanges, OnInit {
this.jsonformsService.setUiSchemas(newUischemas.currentValue);
}

if (newLocale && !newLocale.isFirstChange()) {
this.jsonformsService.setLocale(newLocale.currentValue);
if (newI18n && !newI18n.isFirstChange()) {
this.jsonformsService.updateI18n(
Actions.updateI18n(newI18n.currentValue?.locale, newI18n.currentValue?.translate, newI18n.currentValue?.translateError)
);
}

if (newReadonly && !newReadonly.isFirstChange()) {
Expand Down
22 changes: 13 additions & 9 deletions packages/angular/src/jsonforms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ import {
JsonFormsState,
JsonFormsSubStates,
JsonSchema,
LocaleActions,
I18nActions,
RankedTester,
setConfig,
SetConfigAction,
UISchemaActions,
UISchemaElement,
uischemaRegistryReducer,
UISchemaTester,
ValidationMode
ValidationMode,
updateI18n
} from '@jsonforms/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { JsonFormsBaseRenderer } from './base.renderer';
Expand All @@ -56,9 +57,10 @@ export class JsonFormsAngularService {
private _state: JsonFormsSubStates;
private state: BehaviorSubject<JsonFormsState>;

init(initialState: JsonFormsSubStates = { core: { data: undefined, schema: undefined, uischema: undefined } }) {
init(initialState: JsonFormsSubStates = { core: { data: undefined, schema: undefined, uischema: undefined, validationMode: 'ValidateAndShow' } }) {
this._state = initialState;
this._state.config = configReducer(undefined, setConfig(this._state.config));
this._state.i18n = i18nReducer(this._state.i18n, updateI18n(this._state.i18n?.locale, this._state.i18n?.translate, this._state.i18n?.translateError));
this.state = new BehaviorSubject({ jsonforms: this._state });
const data = initialState.core.data;
const schema = initialState.core.schema ?? generateJsonSchema(data);
Expand Down Expand Up @@ -117,16 +119,18 @@ export class JsonFormsAngularService {
this.updateSubject();
}

updateLocale<T extends LocaleActions>(localeAction: T): T {
const localeState = i18nReducer(this._state.i18n, localeAction);
this._state.i18n = localeState;
this.updateSubject();
return localeAction;
updateI18n<T extends I18nActions>(i18nAction: T): T {
const i18nState = i18nReducer(this._state.i18n, i18nAction);
if (i18nState !== this._state.i18n) {
this._state.i18n = i18nState;
this.updateSubject();
}
return i18nAction;
}

updateCore<T extends CoreActions>(coreAction: T): T {
const coreState = coreReducer(this._state.core, coreAction);
if(coreState !== this._state.core) {
if (coreState !== this._state.core) {
this._state.core = coreState;
this.updateSubject();
}
Expand Down
68 changes: 39 additions & 29 deletions packages/core/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { generateDefaultUISchema, generateJsonSchema } from '../generators';

import { RankedTester } from '../testers';
import { UISchemaTester, ValidationMode } from '../reducers';
import { ErrorTranslator, Translator } from '../i18n';

export const INIT: 'jsonforms/INIT' = 'jsonforms/INIT';
export const UPDATE_CORE: 'jsonforms/UPDATE_CORE' = `jsonforms/UPDATE_CORE`;
Expand All @@ -51,10 +52,10 @@ export const SET_VALIDATION_MODE: 'jsonforms/SET_VALIDATION_MODE' =
'jsonforms/SET_VALIDATION_MODE';

export const SET_LOCALE: 'jsonforms/SET_LOCALE' = `jsonforms/SET_LOCALE`;
export const SET_LOCALIZED_SCHEMAS: 'jsonforms/SET_LOCALIZED_SCHEMAS' =
'jsonforms/SET_LOCALIZED_SCHEMAS';
export const SET_LOCALIZED_UISCHEMAS: 'jsonforms/SET_LOCALIZED_UISCHEMAS' =
'jsonforms/SET_LOCALIZED_UISCHEMAS';
export const SET_TRANSLATOR: 'jsonforms/SET_TRANSLATOR' =
'jsonforms/SET_TRANSLATOR';
export const UPDATE_I18N: 'jsonforms/UPDATE_I18N' =
'jsonforms/UPDATE_I18N';

export const ADD_DEFAULT_DATA: 'jsonforms/ADD_DEFAULT_DATA' = `jsonforms/ADD_DEFAULT_DATA`;
export const REMOVE_DEFAULT_DATA: 'jsonforms/REMOVE_DEFAULT_DATA' = `jsonforms/REMOVE_DEFAULT_DATA`;
Expand Down Expand Up @@ -275,33 +276,21 @@ export const unregisterUISchema = (
};
};

export type LocaleActions =
export type I18nActions =
| SetLocaleAction
| SetLocalizedSchemasAction
| SetLocalizedUISchemasAction;
| SetTranslatorAction
| UpdateI18nAction

export interface SetLocaleAction {
type: 'jsonforms/SET_LOCALE';
locale: string;
locale: string | undefined;
}

export const setLocale = (locale: string): SetLocaleAction => ({
export const setLocale = (locale: string | undefined): SetLocaleAction => ({
type: SET_LOCALE,
locale
});

export interface SetLocalizedSchemasAction {
type: 'jsonforms/SET_LOCALIZED_SCHEMAS';
localizedSchemas: Map<string, JsonSchema>;
}

export const setLocalizedSchemas = (
localizedSchemas: Map<string, JsonSchema>
): SetLocalizedSchemasAction => ({
type: SET_LOCALIZED_SCHEMAS,
localizedSchemas
});

export interface SetSchemaAction {
type: 'jsonforms/SET_SCHEMA';
schema: JsonSchema;
Expand All @@ -312,16 +301,37 @@ export const setSchema = (schema: JsonSchema): SetSchemaAction => ({
schema
});

export interface SetLocalizedUISchemasAction {
type: 'jsonforms/SET_LOCALIZED_UISCHEMAS';
localizedUISchemas: Map<string, UISchemaElement>;
export interface SetTranslatorAction {
type: 'jsonforms/SET_TRANSLATOR';
translator?: Translator;
errorTranslator?: ErrorTranslator;
}

export const setTranslator = (
translator?: Translator,
errorTranslator?: ErrorTranslator
): SetTranslatorAction => ({
type: SET_TRANSLATOR,
translator,
errorTranslator
});

export interface UpdateI18nAction {
type: 'jsonforms/UPDATE_I18N';
locale: string | undefined;
translator: Translator | undefined;
errorTranslator: ErrorTranslator | undefined;
}

export const setLocalizedUISchemas = (
localizedUISchemas: Map<string, UISchemaElement>
): SetLocalizedUISchemasAction => ({
type: SET_LOCALIZED_UISCHEMAS,
localizedUISchemas
export const updateI18n = (
locale: string | undefined,
translator: Translator | undefined,
errorTranslator: ErrorTranslator | undefined
): UpdateI18nAction => ({
type: UPDATE_I18N,
locale,
translator,
errorTranslator
});

export interface SetUISchemaAction {
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/i18n/i18nTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ErrorObject } from 'ajv';
import { JsonSchema, UISchemaElement } from '../models';

export type Translator = {
(id: string, defaultMessage: string, values?: any): string;
(id: string, defaultMessage: undefined, values?: any): string | undefined;
}

export type ErrorTranslator = (error: ErrorObject, translate: Translator, uischema?: UISchemaElement) => string;

export interface JsonFormsI18nState {
locale?: string;
translate?: Translator;
translateError?: ErrorTranslator;
}

export type i18nJsonSchema = JsonSchema & {i18n?: string};
76 changes: 76 additions & 0 deletions packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ErrorObject } from 'ajv';
import { UISchemaElement } from '../models';
import { formatErrorMessage } from '../util';
import { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes';

export const getI18nKey = (
schema: i18nJsonSchema | undefined,
uischema: UISchemaElement | undefined,
key: string
): string | undefined => {
if (uischema?.options?.i18n) {
return `${uischema.options.i18n}.${key}`;
}
if (schema?.i18n) {
return `${schema.i18n}.${key}`;
}
return undefined;
};

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

export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
// check whether there is a special keyword message
const keyInSchemas = getI18nKey(
error.parentSchema,
uischema,
`error.${error.keyword}`
);
const specializedKeywordMessage = keyInSchemas && t(keyInSchemas, undefined);
if (specializedKeywordMessage !== undefined) {
return specializedKeywordMessage;
}

// check whether there is a generic keyword message
const genericKeywordMessage = t(`error.${error.keyword}`, undefined);
if (genericKeywordMessage !== undefined) {
return genericKeywordMessage;
}

// check whether there is a customization for the default message
const messageCustomization = t(error.message, undefined);
if (messageCustomization !== undefined) {
return messageCustomization;
}

// rewrite required property messages (if they were not customized) as we place them next to the respective input
if (error.keyword === 'required') {
return t('is a required property', 'is a required property');
}

return error.message;
};

/**
* Returns the determined error message for the given errors.
* All errors must correspond to the given schema and uischema.
*/
export const getCombinedErrorMessage = (
errors: ErrorObject[],
et: ErrorTranslator,
t: Translator,
schema?: i18nJsonSchema,
uischema?: UISchemaElement
) => {
if (errors.length > 0 && t) {
// check whether there is a special message which overwrites all others
const keyInSchemas = getI18nKey(schema, uischema, 'error.custom');
const specializedErrorMessage = keyInSchemas && t(keyInSchemas, undefined);
if (specializedErrorMessage !== undefined) {
return specializedErrorMessage;
}
}
return formatErrorMessage(
errors.map(error => et(error, t, uischema))
);
};
2 changes: 2 additions & 0 deletions packages/core/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './i18nTypes';
export * from './i18nUtil';
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export * from './util';

export * from './Helpers';
export * from './store';
export * from './i18n';
Loading