From 375ec18c15ce1752f7b604c4fe1ac77bd426bc78 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Tue, 24 May 2022 15:51:22 +0000 Subject: [PATCH 1/2] Move i18n key specification from UI Schema options to ControlElement * The i18n base key can no longer be specified as part of the UI Schema `options` * Add optional `i18n` property to `ControlElement` * Adapt i18n key resolution * Add explicit tests for i18n key extraction from UI Schema and Json Schema Fix #1942 --- packages/core/src/i18n/i18nUtil.ts | 7 ++-- packages/core/src/models/uischema.ts | 8 +++++ packages/core/test/i18n/i18nUtil.test.ts | 42 +++++++++++++++++++++++- packages/core/test/util/renderer.test.ts | 2 +- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/core/src/i18n/i18nUtil.ts b/packages/core/src/i18n/i18nUtil.ts index f00b14dce..9f724ed97 100644 --- a/packages/core/src/i18n/i18nUtil.ts +++ b/packages/core/src/i18n/i18nUtil.ts @@ -1,5 +1,5 @@ import { ErrorObject } from 'ajv'; -import { UISchemaElement } from '../models'; +import { isControlElement, UISchemaElement } from '../models'; import { getControlPath } from '../reducers'; import { formatErrorMessage } from '../util'; import { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes'; @@ -8,7 +8,10 @@ export const getI18nKeyPrefixBySchema = ( schema: i18nJsonSchema | undefined, uischema: UISchemaElement | undefined ): string | undefined => { - return uischema?.options?.i18n ?? schema?.i18n ?? undefined; + if (uischema && isControlElement(uischema) && uischema.i18n) { + return uischema.i18n; + } + return schema?.i18n ?? undefined; }; /** diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index b0c3de039..88835fa5d 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -213,6 +213,11 @@ export interface ControlElement extends UISchemaElement, Scopable { * An optional label that will be associated with the control */ label?: string | boolean | LabelDescription; + /** + * The i18n key for the control. It is used to identify the string that needs to be translated. + * It is suffixed with `.label`, `.description` and `.error.` to derive the corresponding message keys for the control. + */ + i18n?: string; } /** @@ -244,6 +249,9 @@ export interface Categorization extends UISchemaElement { elements: (Category | Categorization)[]; } +export const isControlElement = (element: UISchemaElement): element is ControlElement => + element.type === 'Control'; + export const isGroup = (layout: Layout): layout is GroupLayout => layout.type === 'Group'; diff --git a/packages/core/test/i18n/i18nUtil.test.ts b/packages/core/test/i18n/i18nUtil.test.ts index 4edc94eef..d2a1c5db3 100644 --- a/packages/core/test/i18n/i18nUtil.test.ts +++ b/packages/core/test/i18n/i18nUtil.test.ts @@ -24,7 +24,7 @@ */ import test from 'ava'; -import { transformPathToI18nPrefix } from '../../src'; +import { ControlElement, getI18nKeyPrefixBySchema, i18nJsonSchema, transformPathToI18nPrefix } from '../../src'; test('transformPathToI18nPrefix returns root when empty', t => { t.is(transformPathToI18nPrefix(''), 'root'); @@ -46,3 +46,43 @@ test('transformPathToI18nPrefix removes array indices', t => { t.is(transformPathToI18nPrefix('foo1.23.b2ar3.1.5.foo'), 'foo1.b2ar3.foo'); t.is(transformPathToI18nPrefix('3'), 'root'); }); + +test('getI18nKeyPrefixBySchema gets key from uischema over schema', t => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + i18n: 'controlFoo' + }; + const schema: i18nJsonSchema = { + type: 'string', + i18n: 'schemaFoo' + } + t.is(getI18nKeyPrefixBySchema(schema, control), 'controlFoo'); +}); + +test('getI18nKeyPrefixBySchema gets schema key for missing uischema key', t => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + }; + const schema: i18nJsonSchema = { + type: 'string', + i18n: 'schemaFoo' + } + t.is(getI18nKeyPrefixBySchema(schema, control), 'schemaFoo'); +}); + +test('getI18nKeyPrefixBySchema returns undefined for missing uischema and schema keys', t => { + const control: ControlElement = { + type: 'Control', + scope: '#/properties/foo', + }; + const schema: i18nJsonSchema = { + type: 'string', + } + t.is(getI18nKeyPrefixBySchema(schema, control), undefined); +}); + +test('getI18nKeyPrefixBySchema returns undefined for undefined parameters', t => { + t.is(getI18nKeyPrefixBySchema(undefined, undefined), undefined); +}); \ No newline at end of file diff --git a/packages/core/test/util/renderer.test.ts b/packages/core/test/util/renderer.test.ts index 1a1953efd..08f080e8e 100644 --- a/packages/core/test/util/renderer.test.ts +++ b/packages/core/test/util/renderer.test.ts @@ -1344,7 +1344,7 @@ test('mapStateToControlProps - i18n - translation via UI Schema i18n key', t => }; const state: JsonFormsState = createState(coreUISchema); state.jsonforms.i18n = defaultJsonFormsI18nState; - ownProps.uischema = {...ownProps.uischema, options: {i18n: 'my-key'}}; + ownProps.uischema = {...ownProps.uischema, i18n: 'my-key'}; state.jsonforms.i18n.translate = (key: string, defaultMessage: string | undefined) => { switch(key){ case 'my-key.label': return 'my label'; From 141eb26e6877d5f879395683977f6855347127c8 Mon Sep 17 00:00:00 2001 From: Lucas Koehler Date: Thu, 2 Jun 2022 07:36:55 +0000 Subject: [PATCH 2/2] Add Internationalizable interface and type guard --- packages/core/src/i18n/i18nUtil.ts | 4 ++-- packages/core/src/models/uischema.ts | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/core/src/i18n/i18nUtil.ts b/packages/core/src/i18n/i18nUtil.ts index 9f724ed97..172d5918e 100644 --- a/packages/core/src/i18n/i18nUtil.ts +++ b/packages/core/src/i18n/i18nUtil.ts @@ -1,5 +1,5 @@ import { ErrorObject } from 'ajv'; -import { isControlElement, UISchemaElement } from '../models'; +import { isInternationalized, UISchemaElement } from '../models'; import { getControlPath } from '../reducers'; import { formatErrorMessage } from '../util'; import { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes'; @@ -8,7 +8,7 @@ export const getI18nKeyPrefixBySchema = ( schema: i18nJsonSchema | undefined, uischema: UISchemaElement | undefined ): string | undefined => { - if (uischema && isControlElement(uischema) && uischema.i18n) { + if (isInternationalized(uischema)) { return uischema.i18n; } return schema?.i18n ?? undefined; diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index 88835fa5d..b93897f30 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -36,6 +36,15 @@ export interface Scopable { scope: string; } +/** + * Interface for describing an UI schema element that can provide an internationalization base key. + * If defined, this key is suffixed to derive applicable message keys for the UI schema element. + * For example, such suffixes are `.label` or `.description` to derive the corresponding message keys for a control element. + */ +export interface Internationalizable { + i18n?: string; +} + /** * A rule that may be attached to any UI schema element. */ @@ -207,17 +216,12 @@ export interface LabelElement extends UISchemaElement { * A control element. The scope property of the control determines * to which part of the schema the control should be bound. */ -export interface ControlElement extends UISchemaElement, Scopable { +export interface ControlElement extends UISchemaElement, Scopable, Internationalizable { type: 'Control'; /** * An optional label that will be associated with the control */ label?: string | boolean | LabelDescription; - /** - * The i18n key for the control. It is used to identify the string that needs to be translated. - * It is suffixed with `.label`, `.description` and `.error.` to derive the corresponding message keys for the control. - */ - i18n?: string; } /** @@ -249,8 +253,9 @@ export interface Categorization extends UISchemaElement { elements: (Category | Categorization)[]; } -export const isControlElement = (element: UISchemaElement): element is ControlElement => - element.type === 'Control'; +export const isInternationalized = (element: unknown): element is Required => { + return typeof element === 'object' && element !== null && typeof (element as Internationalizable).i18n === 'string'; +} export const isGroup = (layout: Layout): layout is GroupLayout => layout.type === 'Group';