IllegalStateException for field with custom converter

I already commented before on Translation of entity values - #10 от пользователя darktron1337 - CUBA.Platform
but didn’t get a reply there.

The embedded option you provide is not an option for us. We don’t know what languages will be used at run-time and adding an extra column for every language in general is smelly.

I tried multiple strategies today with a jpa AttributeConverter/Embeddables/Non-persistent/… but I can’t get it working in Cuba.
my latest attempt fails with:
java.lang.IllegalStateException: Can’t find range class ‘com.company.sample.entity.LocalizedString’ for property ‘sample$Customer.description’
(see code below, attributeconverter) because the type is not known to Cuba.

This is a pretty huge deal for us, and we need a solution. We also need front-end support (web). I tried looking for the component that is used in the entity designer for localization, but that is not included in the framework. The documentation is lacking in regards to making custom components. Id like to see an example on how to make something like that component (inputfield and button leading to a popup)

Can you please provide support for a decent implementation and/or a timeline on build-in support.
Here my latest attempt. (preview messes up syntax highlighting for some reason)

public class TranslatedString {

	private String isoCode, message;

	public TranslatedString(final String isoCode, final String message) {
		this.isoCode = isoCode;
		this.message = message;
	}

	public String getIsoCode() {
		return isoCode;
	}

	public String getMessage() {
		return message;
	}
}

public class LocalizedString {
    private Map<Locale, String> translations = new HashMap<>();
    private Locale currentlocale;

    public LocalizedString(Map<Locale, String> translations, Locale locale) {
        currentlocale = locale;
        this.translations = Maps.newHashMap(translations);
    }

    public LocalizedString(Map<Locale, String> translations) {
        this(translations, null);
    }

    public String getCurrentLocaleMessage() {
        return translations.get(currentlocale);
    }

    public String getMessage(Locale locale) {
        return translations.get(locale);
    }

    public void setCurrentLocale(final Locale locale) {
        this.currentlocale = locale;
    }

    public Map<Locale, String> getTranslations() {
        return translations;
    }
}


@Converter
public class LocalizedStringToJsonConverter implements AttributeConverter<LocalizedString, String> {
	@Override
	public String convertToDatabaseColumn(final LocalizedString localized) {
		final Gson gson = new GsonBuilder().create();
		return gson.toJson(localized);
	}

	@Override
	public LocalizedString convertToEntityAttribute(final String json) {
		final Gson gson = new GsonBuilder().create();
		return gson.fromJson(json, LocalizedString.class);
	}
}

FYI,
I already have an other solution implemented using separate entities but I prefer a simpler solution.

    @NamePattern("#getCurrentLocaleMessage|currentLocaleMessage")
    @Listeners("octo_LocalizableMessageEntityListener")
    @Table(name = "OCTO_LOCALIZABLE_MESSAGE")
    @Entity(name = "octo$LocalizableMessage")
    public class LocalizableMessage extends StandardEntity {
        private static final long serialVersionUID = -9077224899403719439L;
    
        @OnDeleteInverse(DeletePolicy.DENY)
        @Composition
        @OnDelete(DeletePolicy.CASCADE)
        @OneToMany(mappedBy = "localizableMessage")
        protected List<MessageTranslationtranslations = new ArrayList<>();
    
        @Transient
        @MetaProperty
        protected String currentLocaleIsocode;
    
    
        public String getCurrentLocaleIsocode() {
            return currentLocaleIsocode;
        }
    
    
        public void setTranslations(List<MessageTranslationtranslations) {
            this.translations = translations;
        }
    
        public List<MessageTranslationgetTranslations() {
            return translations;
        }
    
        @MetaProperty(related = "translations")
        public String getCurrentLocaleMessage() {
            Optional<MessageTranslationcurrent = translations.stream().filter(t -t.getLanguage().getIsoCode().equals(currentLocaleIsocode)).findFirst();
            return current.isPresent() ? current.get().getMessage() : "Undefined";
        }
    
        public void setCurrentLocaleIsocode(String currentLocaleIsocode) {
            this.currentLocaleIsocode = currentLocaleIsocode;
        }
    }
    
    
    @NamePattern("%s|message")
    @Table(name = "OCTO_MESSAGE_TRANSLATION")
    @Entity(name = "octo$MessageTranslation")
    public class MessageTranslation extends StandardEntity {
    	private static final long serialVersionUID = -8652870869079877576L;
    
    	@NotNull
    	@ManyToOne(fetch = FetchType.LAZY, optional = false)
    	@JoinColumn(name = "LANGUAGE_ISOCODE")
    	protected Language language;
    
    	@Column(name = "MESSAGE")
    	protected String message;
    
    	@NotNull
    	@ManyToOne(fetch = FetchType.LAZY, optional = false)
    	@JoinColumn(name = "LOCALIZABLE_MESSAGE_ID")
    	protected LocalizableMessage localizableMessage;
    
    	public MessageTranslation() {
    	}
    
    	public MessageTranslation(Language language, String message) {
    		this.language = language;
    		this.message = message;
    	}
    
    	public void setLocalizableMessage(LocalizableMessage localizableMessage) {
    		this.localizableMessage = localizableMessage;
    	}
    
    	public LocalizableMessage getLocalizableMessage() {
    		return localizableMessage;
    	}
    
    	public void setMessage(String message) {
    		this.message = message;
    	}
    
    	public String getMessage() {
    		return message;
    	}
    
    	public void setLanguage(Language language) {
    		this.language = language;
    	}
    
    	public Language getLanguage() {
    		return language;
    	}
    }

Please use triple back quot ``` for code blocks or </> button on editor panel.

Hi Tom,

Could you provide a small test project containing your experiments and an explanation what works and what doesn’t. We would help you to fix it and it would be a common ground for other experiments, including visual components.

Hi,
I finally had time to create a sample. You can find it at
https://gitlab.com/tom.monnier/cuba-multilangual-experiment

The application allows registering cats and dogs. (I’m gonna make :heavy_dollar_sign::heavy_dollar_sign::heavy_dollar_sign: on this)

approach 1: using tables
For dogs, the breed is localized. this works but has some drawbacks, one is having to register a datasource at the dog level in every screen. If the number of localizable messages gets high this is gonna be annoying, and it’s easy to forget.
There is an LocalizableEntityMessageListener that sets the user language before detach.

approach 2: jpa custom converter
For cats, their favourite food is localized. (fish/poisson/vis/isda)

This code is commented out in the Cat entity because deployment of the war fails with the IllegalstateException this ticket is created for. Also, this type is simply not accepted in studio. There would be a need to be a able to register custom datatypes in studio. Or more specifically, Cuba should support jpa AttributeConverters.

Solution 2 is most suited for my needs, and feels simpler.

My ideal solution would be:

  • transparant, except for adding a property to an entity, no extra actions should be taken/
  • linked to a dedicated ui component for editing the values
  • integrated in the session, user’s current language
  • integrated in reporting, as a parameter (user language != report reader language)

Hopefully, you can help me out here. As said this is an important requirement for me.
Regards,
Tom

Hi Tom,

First of all, converters and custom datatypes are supported by CUBA framework and Studio. See for example the docs.

So in your case a datatype may look like this:

@JavaClass(LocalizedString.class)
public class LocalizedStringDatatype implements Datatype<LocalizedString> {

    @Override
    public String format(@Nullable Object value) {
        if (value == null)
            return "";
        return new LocalizedStringToJsonConverter().convertToDatabaseColumn((LocalizedString) value);
    }

    @Override
    public String format(@Nullable Object value, Locale locale) {
        return format(value);
    }

    @Nullable
    @Override
    public LocalizedString parse(@Nullable String value) throws ParseException {
        if (value == null)
            return null;
        return new LocalizedStringToJsonConverter().convertToEntityAttribute(value);
    }

    @Nullable
    @Override
    public LocalizedString parse(@Nullable String value, Locale locale) throws ParseException {
        return parse(value);
    }
}

The datatype must be registered in metadata.xml and defined on the entity attribute using @MetaProperty annotation:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">

    <datatypes>
        <datatype id="localizedString" class="com.company.multilangual.approach2.LocalizedStringDatatype"/>
    </datatypes>
@Column(name = "DESCRIPTION", columnDefinition = "varchar(410)")
@Convert(converter = LocalizedStringToJsonConverter.class)
@MetaProperty(datatype = "localizedString")
protected LocalizedString favouriteFood;

Unfortunately, the datatype alone is not enough to display the values in UI in a sensible manner. It will just convert between JSON and object representation.
So I’ve created a straightforward implementation of UI using field/cell generators which allows a user to view and edit a translated value in the current locale. A more convenient “no boilerplate” solution can be created by implementing a specific UI components. But it’s a more difficult task and it depends on how it should look from the user’s perspective.

Please look at my PR: https://gitlab.com/tom.monnier/cuba-multilangual-experiment/merge_requests/1