Skip to content

Commit 31b9a7a

Browse files
core: improve Translator type for strict TypeScript compatibility (#2562)
Fix #2528 - Replace overloaded Translator type with a generic conditional type - Add createTranslator helper to allow implementation without type assertions under strictFunctionTypes and strictNullChecks. - createTranslator always returns default value for undefined translation
1 parent 207fb1b commit 31b9a7a

File tree

16 files changed

+104
-46
lines changed

16 files changed

+104
-46
lines changed

MIGRATION.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@
22

33
## Migrating to JSON Forms 3.8
44

5+
### `Translator` type changed from overloaded signatures to a generic conditional type
6+
7+
The `Translator` type was changed to improve compatibility with TypeScript's `strictFunctionTypes` and `strictNullChecks` compiler options (see [#2528](https://github.com/eclipsesource/jsonforms/issues/2528)).
8+
9+
If you were previously assigning a function directly to the `Translator` type, this may no longer compile:
10+
11+
```ts
12+
// No longer compiles
13+
const t: Translator = (id, defaultMessage) => defaultMessage ?? id;
14+
```
15+
16+
Use the new `createTranslator` helper instead:
17+
18+
```ts
19+
import { createTranslator } from '@jsonforms/core';
20+
21+
const t = createTranslator((id, defaultMessage) => defaultMessage ?? id);
22+
```
23+
24+
This also replaces the `as Translator` workaround that was previously needed under strict TypeScript settings.
25+
26+
#### Vue: `Translator` return type in Options API
27+
28+
If you have custom Vue renderers that access a `Translator` via `this` (Options API), the return type may no longer narrow to `string` even when a `defaultMessage` is provided.
29+
This is because Vue's ref unwrapping loses the generic parameter of the new conditional type.
30+
31+
To fix this, use `as string` when you know a default message is always provided:
32+
33+
```ts
34+
// Before (may now return string | undefined)
35+
return this.t(label, label);
36+
37+
// After
38+
return this.t(label, label) as string;
39+
```
40+
41+
This does not affect the Composition API where `Translator` is accessed directly from a `ComputedRef`.
42+
543
### Angular material removes hammerjs
644

745
The angular material package no longer depends or imports the `hammerjs` package.

packages/core/src/i18n/i18nUtil.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,25 @@ export const addI18nKeyToPrefix = (
6565
return `${i18nKeyPrefix}.${key}`;
6666
};
6767

68-
export const defaultTranslator: Translator = (
69-
_id: string,
70-
defaultMessage: string | undefined
71-
) => defaultMessage;
68+
export const createTranslator =
69+
(
70+
fn: (
71+
id: string,
72+
defaultMessage: string | undefined,
73+
values?: any
74+
) => string | undefined
75+
): Translator =>
76+
(id: string, defaultMessage?: string, values?: any) => {
77+
const translation = fn(id, defaultMessage, values);
78+
if (translation === undefined) {
79+
return defaultMessage;
80+
}
81+
return translation;
82+
};
83+
84+
export const defaultTranslator: Translator = createTranslator(
85+
(_id, defaultMessage) => defaultMessage
86+
);
7287

7388
export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
7489
// check whether there is a special keyword message

packages/core/src/store/i18nTypes.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { ErrorObject } from 'ajv';
22
import type { JsonSchema, UISchemaElement } from '../models';
33

4-
export type Translator = {
5-
(id: string, defaultMessage: string, values?: any): string;
6-
(id: string, defaultMessage: undefined, values?: any): string | undefined;
7-
(id: string, defaultMessage?: string, values?: any): string | undefined;
8-
};
4+
export type Translator = <D extends string | undefined = undefined>(
5+
id: string,
6+
defaultMessage?: D,
7+
values?: any
8+
) => D extends string ? string : string | undefined;
99

1010
export type ErrorTranslator = (
1111
error: ErrorObject,

packages/examples/src/examples/arrays-with-translated-custom-element-label.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
THE SOFTWARE.
2424
*/
2525
import { registerExamples } from '../register';
26-
import { JsonSchema7, Translator } from '@jsonforms/core';
26+
import { createTranslator, JsonSchema7, Translator } from '@jsonforms/core';
2727

2828
export const data = {
2929
article: {
@@ -265,9 +265,9 @@ export const uischema = {
265265
],
266266
};
267267

268-
export const translate: Translator = (key: string) => {
268+
export const translate: Translator = createTranslator((key) => {
269269
return 'translator.' + key;
270-
};
270+
});
271271

272272
registerExamples([
273273
{

packages/examples/src/examples/arraysI18n.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
THE SOFTWARE.
2424
*/
2525
import { registerExamples } from '../register';
26-
import { ArrayTranslationEnum, Translator } from '@jsonforms/core';
26+
import {
27+
ArrayTranslationEnum,
28+
createTranslator,
29+
Translator,
30+
} from '@jsonforms/core';
2731
import get from 'lodash/get';
2832

2933
export const schema = {
@@ -88,9 +92,9 @@ export const translations = {
8892
'Are you sure you want to delete this comment?',
8993
},
9094
};
91-
export const translate: Translator = (key: string, defaultMessage: string) => {
95+
export const translate: Translator = createTranslator((key, defaultMessage) => {
9296
return get(translations, key) ?? defaultMessage;
93-
};
97+
});
9498

9599
registerExamples([
96100
{

packages/examples/src/examples/categorization.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import { Translator } from '@jsonforms/core';
25+
import { createTranslator, Translator } from '@jsonforms/core';
2626
import get from 'lodash/get';
2727
import { registerExamples } from '../register';
2828

@@ -292,9 +292,9 @@ export const translations = {
292292
label: 'Address',
293293
},
294294
};
295-
export const translate: Translator = (key: string, defaultMessage: string) => {
295+
export const translate: Translator = createTranslator((key, defaultMessage) => {
296296
return get(translations, key) ?? defaultMessage;
297-
};
297+
});
298298

299299
registerExamples([
300300
{

packages/examples/src/examples/enumI18n.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
THE SOFTWARE.
2424
*/
2525
import { registerExamples } from '../register';
26-
import { Translator } from '@jsonforms/core';
26+
import { createTranslator, Translator } from '@jsonforms/core';
2727
import get from 'lodash/get';
2828

2929
export const schema = {
@@ -116,12 +116,9 @@ export const translations: Record<string, string> = {
116116
'status.rejected': 'Declined',
117117
};
118118

119-
export const translate: Translator = (
120-
key: string,
121-
defaultMessage: string | undefined
122-
) => {
119+
export const translate: Translator = createTranslator((key, defaultMessage) => {
123120
return get(translations, key) ?? defaultMessage;
124-
};
121+
});
125122

126123
registerExamples([
127124
{

packages/examples/src/examples/i18n.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
AnyAction,
3131
Dispatch,
3232
Translator,
33+
createTranslator,
3334
} from '@jsonforms/core';
3435
import get from 'lodash/get';
3536
import localize from 'ajv-i18n/localize';
@@ -99,9 +100,9 @@ export const translations = {
99100
},
100101
additionalInformationLabel: 'Additional Information',
101102
};
102-
export const translate: Translator = (key: string, defaultMessage: string) => {
103+
export const translate: Translator = createTranslator((key, defaultMessage) => {
103104
return get(translations, key) ?? defaultMessage;
104-
};
105+
});
105106

106107
registerExamples([
107108
{

packages/material-renderers/test/renderers/util.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
JsonFormsCore,
2929
JsonSchema,
3030
TesterContext,
31+
createTranslator,
3132
Translator,
3233
UISchemaElement,
3334
} from '@jsonforms/core';
@@ -58,4 +59,6 @@ export const createTesterContext = (
5859
return { rootSchema, config };
5960
};
6061

61-
export const testTranslator: Translator = (key: string) => 'translator.' + key;
62+
export const testTranslator: Translator = createTranslator(
63+
(key) => 'translator.' + key
64+
);

packages/vue-vuetify/src/controls/DateControlRenderer.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,14 +384,14 @@ const controlRenderer = defineComponent({
384384
? this.appliedOptions.cancelLabel
385385
: 'Cancel';
386386
387-
return this.t(label, label);
387+
return this.t(label, label) as string;
388388
},
389389
okLabel(): string {
390390
const label =
391391
typeof this.appliedOptions.okLabel == 'string'
392392
? this.appliedOptions.okLabel
393393
: 'OK';
394-
return this.t(label, label);
394+
return this.t(label, label) as string;
395395
},
396396
showActions(): boolean {
397397
return this.appliedOptions.showActions === true;

0 commit comments

Comments
 (0)