Skip to content
Open
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
40 changes: 40 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Migration guide

## Migrating to JSON Forms 3.8

### `Translator` type changed from overloaded signatures to a generic conditional type

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)).

If you were previously assigning a function directly to the `Translator` type, this may no longer compile:

```ts
// No longer compiles
const t: Translator = (id, defaultMessage) => defaultMessage ?? id;
```

Use the new `createTranslator` helper instead:

```ts
import { createTranslator } from '@jsonforms/core';

const t = createTranslator((id, defaultMessage) => defaultMessage ?? id);
```

This also replaces the `as Translator` workaround that was previously needed under strict TypeScript settings.

#### Vue: `Translator` return type in Options API

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.
This is because Vue's ref unwrapping loses the generic parameter of the new conditional type.

To fix this, use `as string` when you know a default message is always provided:

```ts
// Before (may now return string | undefined)
return this.t(label, label);

// After
return this.t(label, label) as string;
```

This does not affect the Composition API where `Translator` is accessed directly from a `ComputedRef`.

## Migrating to JSON Forms 3.7

### Angular support now targets Angular 19 to 21
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,17 @@ export const addI18nKeyToPrefix = (
return `${i18nKeyPrefix}.${key}`;
};

export const defaultTranslator: Translator = (
_id: string,
defaultMessage: string | undefined
) => defaultMessage;
export const createTranslator = (
fn: (
id: string,
defaultMessage: string | undefined,
values?: any
) => string | undefined
): Translator => fn as Translator;

export const defaultTranslator: Translator = createTranslator(
(_id, defaultMessage) => defaultMessage
);

export const defaultErrorTranslator: ErrorTranslator = (error, t, uischema) => {
// check whether there is a special keyword message
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/store/i18nTypes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { ErrorObject } from 'ajv';
import type { JsonSchema, UISchemaElement } from '../models';

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

export type ErrorTranslator = (
error: ErrorObject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
THE SOFTWARE.
*/
import { registerExamples } from '../register';
import { JsonSchema7, Translator } from '@jsonforms/core';
import { createTranslator, JsonSchema7, Translator } from '@jsonforms/core';

export const data = {
article: {
Expand Down Expand Up @@ -265,9 +265,9 @@ export const uischema = {
],
};

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

registerExamples([
{
Expand Down
10 changes: 7 additions & 3 deletions packages/examples/src/examples/arraysI18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
THE SOFTWARE.
*/
import { registerExamples } from '../register';
import { ArrayTranslationEnum, Translator } from '@jsonforms/core';
import {
ArrayTranslationEnum,
createTranslator,
Translator,
} from '@jsonforms/core';
import get from 'lodash/get';

export const schema = {
Expand Down Expand Up @@ -88,9 +92,9 @@ export const translations = {
'Are you sure you want to delete this comment?',
},
};
export const translate: Translator = (key: string, defaultMessage: string) => {
export const translate: Translator = createTranslator((key, defaultMessage) => {
return get(translations, key) ?? defaultMessage;
};
});

registerExamples([
{
Expand Down
6 changes: 3 additions & 3 deletions packages/examples/src/examples/categorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { Translator } from '@jsonforms/core';
import { createTranslator, Translator } from '@jsonforms/core';
import get from 'lodash/get';
import { registerExamples } from '../register';

Expand Down Expand Up @@ -292,9 +292,9 @@ export const translations = {
label: 'Address',
},
};
export const translate: Translator = (key: string, defaultMessage: string) => {
export const translate: Translator = createTranslator((key, defaultMessage) => {
return get(translations, key) ?? defaultMessage;
};
});

registerExamples([
{
Expand Down
9 changes: 3 additions & 6 deletions packages/examples/src/examples/enumI18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
THE SOFTWARE.
*/
import { registerExamples } from '../register';
import { Translator } from '@jsonforms/core';
import { createTranslator, Translator } from '@jsonforms/core';
import get from 'lodash/get';

export const schema = {
Expand Down Expand Up @@ -116,12 +116,9 @@ export const translations: Record<string, string> = {
'status.rejected': 'Declined',
};

export const translate: Translator = (
key: string,
defaultMessage: string | undefined
) => {
export const translate: Translator = createTranslator((key, defaultMessage) => {
return get(translations, key) ?? defaultMessage;
};
});

registerExamples([
{
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/examples/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
AnyAction,
Dispatch,
Translator,
createTranslator,
} from '@jsonforms/core';
import get from 'lodash/get';
import localize from 'ajv-i18n/localize';
Expand Down Expand Up @@ -99,9 +100,9 @@ export const translations = {
},
additionalInformationLabel: 'Additional Information',
};
export const translate: Translator = (key: string, defaultMessage: string) => {
export const translate: Translator = createTranslator((key, defaultMessage) => {
return get(translations, key) ?? defaultMessage;
};
});

registerExamples([
{
Expand Down
5 changes: 4 additions & 1 deletion packages/material-renderers/test/renderers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
JsonFormsCore,
JsonSchema,
TesterContext,
createTranslator,
Translator,
UISchemaElement,
} from '@jsonforms/core';
Expand Down Expand Up @@ -58,4 +59,6 @@ export const createTesterContext = (
return { rootSchema, config };
};

export const testTranslator: Translator = (key: string) => 'translator.' + key;
export const testTranslator: Translator = createTranslator(
(key) => 'translator.' + key
);
4 changes: 2 additions & 2 deletions packages/vue-vuetify/src/controls/DateControlRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,14 @@ const controlRenderer = defineComponent({
? this.appliedOptions.cancelLabel
: 'Cancel';

return this.t(label, label);
return this.t(label, label) as string;
},
okLabel(): string {
const label =
typeof this.appliedOptions.okLabel == 'string'
? this.appliedOptions.okLabel
: 'OK';
return this.t(label, label);
return this.t(label, label) as string;
},
showActions(): boolean {
return this.appliedOptions.showActions === true;
Expand Down
4 changes: 2 additions & 2 deletions packages/vue-vuetify/src/controls/DateTimeControlRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -584,14 +584,14 @@ const controlRenderer = defineComponent({
? this.appliedOptions.cancelLabel
: 'Cancel';

return this.t(label, label);
return this.t(label, label) as string;
},
okLabel(): string {
const label =
typeof this.appliedOptions.okLabel == 'string'
? this.appliedOptions.okLabel
: 'OK';
return this.t(label, label);
return this.t(label, label) as string;
},
showActions(): boolean {
return this.appliedOptions.showActions === true;
Expand Down
6 changes: 3 additions & 3 deletions packages/vue-vuetify/src/controls/TimeControlRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -365,22 +365,22 @@ const controlRenderer = defineComponent({
? this.appliedOptions.clearLabel
: 'Clear';

return this.t(label, label);
return this.t(label, label) as string;
},
cancelLabel(): string {
const label =
typeof this.appliedOptions.cancelLabel == 'string'
? this.appliedOptions.cancelLabel
: 'Cancel';

return this.t(label, label);
return this.t(label, label) as string;
},
okLabel(): string {
const label =
typeof this.appliedOptions.okLabel == 'string'
? this.appliedOptions.okLabel
: 'OK';
return this.t(label, label);
return this.t(label, label) as string;
},
showActions(): boolean {
return this.appliedOptions.showActions === true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { clearAllIds, type Translator } from '@jsonforms/core';
import { clearAllIds, createTranslator } from '@jsonforms/core';
import ListWithDetailRenderer from '../../../src/additional/ListWithDetailRenderer.vue';
import { entry as listWithDetailRendererEntry } from '../../../src/additional/ListWithDetailRenderer.entry';
import { mountJsonForms } from '../util';
Expand Down Expand Up @@ -30,12 +30,12 @@ describe('ListWithDetailRenderer.vue', () => {
// clear all ids to guarantee that the snapshots will always be generated with the same ids
clearAllIds();
wrapper = mountJsonForms(data, schema, renderers, uischema, undefined, {
translate: ((id, defaultMessage) => {
translate: createTranslator((id, defaultMessage) => {
if (id.endsWith('addAriaLabel')) {
return 'MyAdd';
}
return defaultMessage;
}) as Translator,
}),
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { clearAllIds, type Translator } from '@jsonforms/core';
import { clearAllIds, createTranslator } from '@jsonforms/core';
import ArrayControlRenderer from '../../../src/complex/ArrayControlRenderer.vue';
import { entry as arrayControlRendererEntry } from '../../../src/complex/ArrayControlRenderer.entry';
import { mountJsonForms } from '../util';
Expand Down Expand Up @@ -29,12 +29,12 @@ describe('ArrayControlRenderer.vue', () => {
// clear all ids to guarantee that the snapshots will always be generated with the same ids
clearAllIds();
wrapper = mountJsonForms(data, schema, renderers, uischema, undefined, {
translate: ((id, defaultMessage) => {
translate: createTranslator((id, defaultMessage) => {
if (id.endsWith('addAriaLabel')) {
return 'MyAdd';
}
return defaultMessage;
}) as Translator,
}),
});
});

Expand Down
6 changes: 3 additions & 3 deletions packages/vue-vuetify/tests/unit/complex/OneOfRenderer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { clearAllIds, type Translator } from '@jsonforms/core';
import { clearAllIds, createTranslator } from '@jsonforms/core';
import OneOfControlRenderer from '../../../src/complex/OneOfRenderer.vue';
import { entry as oneOfControlRendererEntry } from '../../../src/complex/OneOfRenderer.entry';
import { mountJsonForms } from '../util';
Expand Down Expand Up @@ -40,12 +40,12 @@ describe('OneOfRenderer.vue', () => {
// clear all ids to guarantee that the snapshots will always be generated with the same ids
clearAllIds();
wrapper = mountJsonForms(data, schema, renderers, uischema, undefined, {
translate: ((id, defaultMessage) => {
translate: createTranslator((id, defaultMessage) => {
if (id.endsWith('clearDialogAccept')) {
return 'Do the clear!';
}
return defaultMessage;
}) as Translator,
}),
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { clearAllIds, type Translator } from '@jsonforms/core';
import { clearAllIds, createTranslator } from '@jsonforms/core';
import ArrayLayoutRenderer from '../../../src/layouts/ArrayLayoutRenderer.vue';
import { entry as arrayLayoutRendererEntry } from '../../../src/layouts/ArrayLayoutRenderer.entry';
import { mountJsonForms } from '../util';
Expand Down Expand Up @@ -30,12 +30,12 @@ describe('ArrayLayoutRenderer.vue', () => {
// clear all ids to guarantee that the snapshots will always be generated with the same ids
clearAllIds();
wrapper = mountJsonForms(data, schema, renderers, uischema, undefined, {
translate: ((id, defaultMessage) => {
translate: createTranslator((id, defaultMessage) => {
if (id.endsWith('addAriaLabel')) {
return 'MyAdd';
}
return defaultMessage;
}) as Translator,
}),
});
});

Expand Down
Loading