Skip to content

Commit 79fb30f

Browse files
authored
Use default calculated i18n keys
If there is no 'i18n' key in schema or ui schema a default calculated key based on the data path of the respective control is used. Additionally fixes the issue that the 'required' message of localized errors were overwritten by JSON Forms.
1 parent 0e4d910 commit 79fb30f

File tree

7 files changed

+183
-96
lines changed

7 files changed

+183
-96
lines changed

packages/core/src/i18n/i18nUtil.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,60 @@
11
import { ErrorObject } from 'ajv';
22
import { UISchemaElement } from '../models';
3+
import { getControlPath } from '../reducers';
34
import { formatErrorMessage } from '../util';
45
import { i18nJsonSchema, ErrorTranslator, Translator } from './i18nTypes';
56

7+
export const getI18nKeyPrefixBySchema = (
8+
schema: i18nJsonSchema | undefined,
9+
uischema: UISchemaElement | undefined
10+
): string | undefined => {
11+
return uischema?.options?.i18n ?? schema?.i18n ?? undefined;
12+
};
13+
14+
/**
15+
* Transforms a given path to a prefix which can be used for i18n keys.
16+
* Returns 'root' for empty paths and removes array indices
17+
*/
18+
export const transformPathToI18nPrefix = (path: string) => {
19+
return (
20+
path
21+
?.split('.')
22+
.filter(segment => !/^\d+$/.test(segment))
23+
.join('.') || 'root'
24+
);
25+
};
26+
27+
export const getI18nKeyPrefix = (
28+
schema: i18nJsonSchema | undefined,
29+
uischema: UISchemaElement | undefined,
30+
path: string | undefined
31+
): string | undefined => {
32+
return (
33+
getI18nKeyPrefixBySchema(schema, uischema) ??
34+
transformPathToI18nPrefix(path)
35+
);
36+
};
37+
638
export const getI18nKey = (
739
schema: i18nJsonSchema | undefined,
840
uischema: UISchemaElement | undefined,
41+
path: string | undefined,
942
key: string
1043
): 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;
44+
return `${getI18nKeyPrefix(schema, uischema, path)}.${key}`;
1845
};
1946

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

2249
export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
2350
// check whether there is a special keyword message
24-
const keyInSchemas = getI18nKey(
51+
const i18nKey = getI18nKey(
2552
error.parentSchema,
2653
uischema,
54+
getControlPath(error),
2755
`error.${error.keyword}`
2856
);
29-
const specializedKeywordMessage = keyInSchemas && t(keyInSchemas, undefined);
57+
const specializedKeywordMessage = t(i18nKey, undefined);
3058
if (specializedKeywordMessage !== undefined) {
3159
return specializedKeywordMessage;
3260
}
@@ -44,7 +72,7 @@ export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
4472
}
4573

4674
// rewrite required property messages (if they were not customized) as we place them next to the respective input
47-
if (error.keyword === 'required') {
75+
if (error.keyword === 'required' && error.message?.startsWith('must have required property')) {
4876
return t('is a required property', 'is a required property');
4977
}
5078

@@ -53,19 +81,20 @@ export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
5381

5482
/**
5583
* Returns the determined error message for the given errors.
56-
* All errors must correspond to the given schema and uischema.
84+
* All errors must correspond to the given schema, uischema or path.
5785
*/
5886
export const getCombinedErrorMessage = (
5987
errors: ErrorObject[],
6088
et: ErrorTranslator,
6189
t: Translator,
6290
schema?: i18nJsonSchema,
63-
uischema?: UISchemaElement
91+
uischema?: UISchemaElement,
92+
path?: string
6493
) => {
6594
if (errors.length > 0 && t) {
6695
// 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);
96+
const customErrorKey = getI18nKey(schema, uischema, path, 'error.custom');
97+
const specializedErrorMessage = t(customErrorKey, undefined);
6998
if (specializedErrorMessage !== undefined) {
7099
return specializedErrorMessage;
71100
}

packages/core/src/reducers/reducers.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ import { ControlElement, UISchemaElement } from '../models';
2727
import {
2828
coreReducer,
2929
errorAt,
30-
errorsAt,
31-
getControlPath,
32-
JsonFormsCore,
3330
subErrorsAt,
34-
ValidationMode
3531
} from './core';
3632
import { defaultDataReducer } from './default-data';
3733
import { rendererReducer } from './renderers';
@@ -40,7 +36,6 @@ import {
4036
findMatchingUISchema,
4137
JsonFormsUISchemaRegistryEntry,
4238
uischemaRegistryReducer,
43-
UISchemaTester
4439
} from './uischemas';
4540
import {
4641
fetchErrorTranslator,
@@ -57,19 +52,6 @@ import get from 'lodash/get';
5752
import { fetchTranslator } from '.';
5853
import { ErrorTranslator, Translator } from '../i18n';
5954

60-
export {
61-
rendererReducer,
62-
cellReducer,
63-
coreReducer,
64-
i18nReducer,
65-
configReducer,
66-
UISchemaTester,
67-
uischemaRegistryReducer,
68-
findMatchingUISchema,
69-
JsonFormsUISchemaRegistryEntry
70-
};
71-
export { JsonFormsCore, ValidationMode };
72-
7355
export const jsonFormsReducerConfig = {
7456
core: coreReducer,
7557
renderers: rendererReducer,
@@ -128,8 +110,6 @@ export const getErrorAt = (instancePath: string, schema: JsonSchema) => (
128110
return errorAt(instancePath, schema)(state.jsonforms.core);
129111
};
130112

131-
export { errorsAt, getControlPath };
132-
133113
export const getSubErrorsAt = (instancePath: string, schema: JsonSchema) => (
134114
state: JsonFormsState
135115
) => subErrorsAt(instancePath, schema)(state.jsonforms.core);

packages/core/src/util/cell.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import {
5555
} from './renderer';
5656
import { JsonFormsState } from '../store';
5757
import { JsonSchema } from '../models';
58-
import { i18nJsonSchema } from '..';
58+
import { getI18nKeyPrefix } from '../i18n';
5959

6060
export { JsonFormsCellRendererRegistryEntry };
6161

@@ -202,14 +202,14 @@ export const defaultMapStateToEnumCellProps = (
202202
enumToEnumOptionMapper(
203203
e,
204204
getTranslator()(state),
205-
props.uischema?.options?.i18n ?? (props.schema as i18nJsonSchema).i18n
205+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
206206
)
207207
) ||
208208
(props.schema.const && [
209209
enumToEnumOptionMapper(
210210
props.schema.const,
211211
getTranslator()(state),
212-
props.uischema?.options?.i18n ?? (props.schema as i18nJsonSchema).i18n
212+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
213213
)
214214
]);
215215
return {
@@ -235,7 +235,7 @@ export const mapStateToOneOfEnumCellProps = (
235235
oneOfToEnumOptionMapper(
236236
oneOfSubSchema,
237237
getTranslator()(state),
238-
props.uischema?.options?.i18n
238+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
239239
)
240240
);
241241
return {

packages/core/src/util/renderer.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,7 @@ import { isVisible } from './runtime';
5555
import { CoreActions, update } from '../actions';
5656
import { ErrorObject } from 'ajv';
5757
import { JsonFormsState } from '../store';
58-
import { getCombinedErrorMessage, getI18nKey, i18nJsonSchema, Translator } from '../i18n';
59-
60-
export { JsonFormsRendererRegistryEntry, JsonFormsCellRendererRegistryEntry };
58+
import { getCombinedErrorMessage, getI18nKey, getI18nKeyPrefix, Translator } from '../i18n';
6159

6260
const isRequired = (
6361
schema: JsonSchema,
@@ -195,7 +193,7 @@ export const enumToEnumOptionMapper = (
195193
export const oneOfToEnumOptionMapper = (
196194
e: any,
197195
t?: Translator,
198-
uiSchemaI18nKey?: string
196+
fallbackI18nKey?: string
199197
): EnumOption => {
200198
let label =
201199
e.title ??
@@ -204,8 +202,8 @@ export const oneOfToEnumOptionMapper = (
204202
// prefer schema keys as they can be more specialized
205203
if (e.i18n) {
206204
label = t(e.i18n, label);
207-
} else if (uiSchemaI18nKey) {
208-
label = t(`${uiSchemaI18nKey}.${label}`, label);
205+
} else if (fallbackI18nKey) {
206+
label = t(`${fallbackI18nKey}.${label}`, label);
209207
} else {
210208
label = t(label, label);
211209
}
@@ -464,9 +462,9 @@ export const mapStateToControlProps = (
464462
const schema = resolvedSchema ?? rootSchema;
465463
const t = getTranslator()(state);
466464
const te = getErrorTranslator()(state);
467-
const i18nLabel = t(getI18nKey(schema, uischema, 'label') ?? label, label);
468-
const i18nDescription = t(getI18nKey(schema, uischema, 'description') ?? description, description);
469-
const i18nErrorMessage = getCombinedErrorMessage(errors, te, t, schema, uischema);
465+
const i18nLabel = t(getI18nKey(schema, uischema, path, 'label'), label);
466+
const i18nDescription = t(getI18nKey(schema, uischema, path, 'description'), description);
467+
const i18nErrorMessage = getCombinedErrorMessage(errors, te, t, schema, uischema, path);
470468

471469
return {
472470
data,
@@ -518,14 +516,14 @@ export const mapStateToEnumControlProps = (
518516
enumToEnumOptionMapper(
519517
e,
520518
getTranslator()(state),
521-
props.uischema?.options?.i18n ?? (props.schema as i18nJsonSchema).i18n
519+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
522520
)
523521
) ||
524522
(props.schema.const && [
525523
enumToEnumOptionMapper(
526524
props.schema.const,
527525
getTranslator()(state),
528-
props.uischema?.options?.i18n ?? (props.schema as i18nJsonSchema).i18n
526+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
529527
)
530528
]);
531529
return {
@@ -551,7 +549,7 @@ export const mapStateToOneOfEnumControlProps = (
551549
oneOfToEnumOptionMapper(
552550
oneOfSubSchema,
553551
getTranslator()(state),
554-
props.uischema?.options?.i18n
552+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
555553
)
556554
);
557555
return {
@@ -579,14 +577,14 @@ export const mapStateToMultiEnumControlProps = (
579577
oneOfToEnumOptionMapper(
580578
oneOfSubSchema,
581579
state.jsonforms.i18n?.translate,
582-
props.uischema?.options?.i18n
580+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
583581
)
584582
)) ||
585583
items?.enum?.map(e =>
586584
enumToEnumOptionMapper(
587585
e,
588586
state.jsonforms.i18n?.translate,
589-
props.uischema?.options?.i18n ?? (props.schema as i18nJsonSchema).i18n
587+
getI18nKeyPrefix(props.schema, props.uischema, props.path)
590588
)
591589
);
592590
return {
@@ -923,7 +921,7 @@ export interface StatePropsOfCombinator extends OwnPropsOfControl {
923921
data: any;
924922
}
925923

926-
const mapStateToCombinatorRendererProps = (
924+
export const mapStateToCombinatorRendererProps = (
927925
state: JsonFormsState,
928926
ownProps: OwnPropsOfControl,
929927
keyword: CombinatorKeyword
@@ -1052,6 +1050,7 @@ export const mapStateToArrayLayoutProps = (
10521050
getErrorTranslator()(state),
10531051
getTranslator()(state),
10541052
undefined,
1053+
undefined,
10551054
undefined
10561055
);
10571056

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2017-2021 EclipseSource Munich
5+
https://github.com/eclipsesource/jsonforms
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
*/
25+
import test from 'ava';
26+
27+
import { transformPathToI18nPrefix } from '../../src';
28+
29+
test('transformPathToI18nPrefix returns root when empty', t => {
30+
t.is(transformPathToI18nPrefix(''), 'root');
31+
});
32+
33+
test('transformPathToI18nPrefix does not modify non-array paths', t => {
34+
t.is(transformPathToI18nPrefix('foo'), 'foo');
35+
t.is(transformPathToI18nPrefix('foo.bar'), 'foo.bar');
36+
t.is(transformPathToI18nPrefix('bar3.foo2'), 'bar3.foo2');
37+
});
38+
39+
test('transformPathToI18nPrefix removes array indices', t => {
40+
t.is(transformPathToI18nPrefix('foo.2.bar'), 'foo.bar');
41+
t.is(transformPathToI18nPrefix('foo.234324234.bar'), 'foo.bar');
42+
t.is(transformPathToI18nPrefix('foo.0.bar'), 'foo.bar');
43+
t.is(transformPathToI18nPrefix('foo.0.bar.1.foobar'), 'foo.bar.foobar');
44+
t.is(transformPathToI18nPrefix('3.foobar'), 'foobar');
45+
t.is(transformPathToI18nPrefix('foobar.3'), 'foobar');
46+
t.is(transformPathToI18nPrefix('foo1.23.b2ar3.1.5.foo'), 'foo1.b2ar3.foo');
47+
t.is(transformPathToI18nPrefix('3'), 'root');
48+
});

packages/core/test/reducers/core.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import test from 'ava';
2626
import Ajv from 'ajv';
2727
import { coreReducer } from '../../src/reducers';
28-
import { init, update, updateErrors } from '../../src/actions';
28+
import { init, setSchema, setValidationMode, update, updateCore, updateErrors } from '../../src/actions';
2929
import { JsonSchema } from '../../src/models/jsonSchema';
3030
import {
3131
errorAt,
@@ -34,9 +34,8 @@ import {
3434
subErrorsAt
3535
} from '../../src/reducers/core';
3636

37-
import { createAjv, updateCore } from '../../src';
38-
import { setSchema, setValidationMode } from '../../lib';
3937
import { cloneDeep } from 'lodash';
38+
import { createAjv } from '../../src/util/validator';
4039

4140
test('core reducer should support v7', t => {
4241
const schema: JsonSchema = {

0 commit comments

Comments
 (0)