Ctx is undefined in enum item translation function call

Hello team, and thank you in advance for taking the time to read my question.

I have run into the problem described in #1642 for a big oneOf enum with dynamic values I don’t control, in which each item’s “ID” is a number different from it’s “readable name”. I’m trying to use the suggested i18n workaround as I’d like to keep validation for these enum fields (most of them are required and can’t be empty).

I’ve found that when my translator function runs for the enum’s items the ctx argument is undefined. This is a problem because I need to access the uischema in the context, in order to find the “translation” from ID to readable name. However, when translating the title and description for the control, the ctx is populated.

Could you please advice on what might be the issue? Pasting example data below. Thank you!

This is my schema:

{
    "type": "object",
    "properties": {
        "enumWithTranslations": {
            "title": "A HUGE enum (using i18n for labels)",
            "description":
                "A choice from a list of options, where readable labels come from translations",
            "enum": [
                "enum 0",
                "enum 1",
                "enum 2",
                "enum 3",
                "enum 4",
                "enum 5",
                "enum 6",
                "enum 7",
                "enum 8",
                "enum 9",
            ],
        },
    },
}

And my UI schema:

{
    type: "VerticalLayout",
    elements: [
        {
            "type": "Control",
            "scope": "#/properties/enumWithTranslations",
            "i18n": "default",
            "title#0": "label 0",
            "title#1": "label 1",
            "title#2": "label 2",
            "title#3": "label 3",
            "title#4": "label 4",
            "title#5": "label 5",
            "title#6": "label 6",
            "title#7": "label 7",
            "title#8": "label 8",
            "title#9": "label 9",
        },
    ],
}

So, when I log the arguments received in my translator function for the control’s title and description it looks like this

{
    "id": "default.label",
    "defaultMessage": "A HUGE enum (using i18n for labels)",
    "ctx": {
        "schema": {
            "title": "A HUGE enum (using i18n for labels)",
            "description": "A choice from a list of options, where readable labels come from translations",
            "enum": [
                "enum 0",
                "enum 1",
                "enum 2",
                "enum 3",
                "enum 4",
                "enum 5",
                "enum 6",
                "enum 7",
                "enum 8",
                "enum 9"
            ]
        },
        "uischema": {
            "type": "Control",
            "scope": "#/properties/enumWithTranslations",
            "i18n": "default",
            "title#0": "label 0",
            "title#1": "label 1",
            "title#2": "label 2",
            "title#3": "label 3",
            "title#4": "label 4",
            "title#5": "label 5",
            "title#6": "label 6",
            "title#7": "label 7",
            "title#8": "label 8",
            "title#9": "label 9"
        },
        "path": "enumWithTranslations",
        "errors": []
    }
}

But for the enum’s items, it looks like this:

{
    "id": "default.enum 0",
    "defaultMessage": "enum 0",
    "ctx": undefined
}

Hi @aspin,

Thanks for the report. This is an oversight in the core framework. The enum translations are invoked without handing over context, see here and here.

The binding should be enhanced to hand over context like the schema and uischema too.

If you like you can open an issue against the repository and/or contribute a fix for this.

Until then you can workaround the issue by using a custom enum renderer. The custom enum renderer can 100% reuse the existing one. Before invoking the existing enum renderer you can manually translate the enum labels. To do this inject the translator and hand over the context yourself as you have the current schema and uischema in hand. See this guide on how to reuse existing controls.

1 Like

Hey @sdirix thank you for your quick response, and apologies for my delay. I finally got time to revisit this matter this week.

I am actually using custom renderers for this so this wouldn’t be an issue. However, I’m not sure I understand what you mean by

inject the translator and hand over the context yourself

Do you mean mapping over EnumProps['options'] and passing them into the translate function and then pass them into the custom enum renderer?

Since I don’t have real i18n needs here, I’ve also tried doing a manual “lookup” for the labels on the uischema within the custom renderer, to avoid “polluting” the implementation of my translate function, and the results were similar.

Unless you meant something else I’ll probably go for approach B to keep the translate function clean. I’ll look into filing and issue and trying on a fix for the underlying i18n ctx issue for enum values.

Thanks again!


Just for the sake of clarity, here’s a simplified version of what I’ve done:

Schema

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
    "size": {
      "title": "An enumeration with i18n",
      "description": "This enum uses i18n to translate its values to readable labels",
      "enum": ["f00", "b4r", "b4z"]
    }
}

UI Schema

{
    "type": "VerticalLayout",
    "elements": [
        {
            "type": "Control",
            "scope": "#/properties/size",
            "f00": "Label foo",
            "b4r": "Label bar",
            "b4z": "Label baz"
        }
    ]
}

Approach A: using translate

// my custom Enum renderer

const EnumCellControl = (props: EnumCellProps) => {
  const {i18n} = useJSONForms()

    const labeledOptions = useMemo(() => {
        const translate = i18n?.translate

        if (!translate) {
            return options
        }

        return options?.map((option) => ({
            value: option.value,
            label: translate(`enum.${option.value}`, option.value, {schema, uischema}),
        }))
    }, [i18n, options, schema, uischema])

    return (
        <Select
            options={labeledOptions}
            // ... other props
        />
    )
// my translator function

    const translator: Translator = (id, defaultMessage, ctx: I18nContext) => {
        if (ctx === undefined || defaultMessage === undefined) {
            return defaultMessage
        }

        const [prefix, suffix] = id.split('.') // ['enum', 'f00']

        if (prefix === 'enum') {
            return (
                ctx.uischema?.[suffix] ?? ctx.schema?.[suffix] ?? defaultMessage
            )
        }

       // ...
}

Approach B: manual “lookup”

const EnumCellControl = (props: EnumCellProps) => {
    const {options, uischema} = props
    
    const optionsWithI18n = useMemo(
        () => labelValuePairsForEnum(options, uischema.i18n),
        [options, uischema.i18n],
    )

    return (
        <Select
            options={optionsWithI18n}
            // ... other props
        />
    )
}

const labelValuePairsForEnum = (
    options: EnumCellProps['options'],
    uischema: EnumCellProps['uischema'],
): EnumCellProps['options'] => {
    if (!uischema) {
        return options
    }

    return options?.map((option) => {
        return {
            value: option.value,
            // finding the "label" indexed as the value in the uischema, or defaulting to using the value itself
            label: uischema[option.value] ?? option.value,
        }
    })
}

Hi @aspin,

I just assumed you wanted to use the translator approach, feel free to skip it :+1:

Both approaches from you look good! So use whatever is more aligned with your requirements :wink: