Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New exception class that incorporates support for internationalization #2549

Merged
merged 14 commits into from
Nov 12, 2021

Conversation

lolodomo
Copy link
Contributor

Signed-off-by: Laurent Garnier lg.hc@free.fr

@lolodomo
Copy link
Contributor Author

@cweitkamp for review.

Note that the method getUntranslatedMessage is useful only when used as last argument in a call to updateStatus. Without this method, we could do it by using getLocalizedMessage but this will require to first setup the locale and so to inherit LocaleProvider in the thing handler. So just a little more complex but maybe getUntranslatedMessage also adds some confusion.

First solution with getUntranslatedMessage (you need i18nProvider):

            e.setupI18n(i18nProvider);
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getUntranslatedMessage());

Second solution without getUntranslatedMessage (you need i18nProvider and localeProvider):

            e.setupI18n(i18nProvider, localeProvider.getLocale());
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());

What is your preference ?

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Oct 30, 2021

In fact, I would be in favor of solution 2, that is removing getUntranslatedMessage.

@wborn
Copy link
Member

wborn commented Oct 30, 2021

It seems like a lot of boilerplate code and it would only work with exceptions this way.

Maybe it is also possible to create some kind LocalizedString (with translation key and argument fields). Then a updateStatus(ThingStatus, ThingStatusDetail, LocalizedString) could be added and the framework would use the i18nProvider and localeProvider to translate it. You'd also not have to inject those into a ThingHandler anymore. The LocalizedString could also extend or implement String and then you won't even need the extra method (the framework could do instanceof to see if it can be translated). Then you could also use it in any existing Exception. 😉

@wborn
Copy link
Member

wborn commented Oct 30, 2021

could also extend or implement String

I see String is final and not an interface, probably to prevent tricks like this. 😉

@lolodomo
Copy link
Contributor Author

lolodomo commented Oct 30, 2021

But updateStatus is just one use case amongst many. Another common use case is to use e.getMessage() in a log.

Ok I can create a LocalizedString type and use it as parameter in the constructor. My current method getUntranslatedMessage() will then return it. And a new version of updateStatus taking a LocalizedString as parameter could be added.

But setupI18n() will still be required if you want to use getMessage() or getLocalizedMessage().

@wborn
Copy link
Member

wborn commented Oct 30, 2021

It would be interesting to know what the other @openhab/core-maintainers think the best approach would be, before you start building something.

I recently read that slf4j has localization support, but from the first looks I had at it, it didn't look like it was an easy fit. I also don't know if it works within Karaf, see:

http://www.slf4j.org/localization.html

@J-N-K
Copy link
Member

J-N-K commented Oct 30, 2021

Please keep in mind that Exceptions and their messages are mostly useful for developers. Localization of these messages might make support more difficult as e.g. I can't understand French or Russian exception messages.

@cweitkamp
Copy link
Contributor

cweitkamp commented Oct 30, 2021

From my pov we should focus on translatable exceptions here. In detail: a way to use openHAB based way of retrieving a translated exception message via getLocalizedMessage(). We should keep default behavior for getMessage() by returning an English message.

The status message already will be translated by the framework. I am using it in OpenWeatherMap binding in combination with exceptions by passing the @text/... placeholder to it where it is thrown. e.g. throw new OpenWeatherMapConfigurationException("@text/offline.conf-error-invalid-apikey", e.getCause()); and later updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());.

Your current getUntranslatedMessage() might rather be renamed to getRawMessage().

@lolodomo
Copy link
Contributor Author

When I previously said there were many use cases, I was certainly excessive. In fact, we have the use case with updateStatus and the need to translate for the display in UI and we have the other cases where e.getMessage() should return the english message (used in log for example).

Please keep in mind that Exceptions and their messages are mostly useful for developers. Localization of these messages might make support more difficult as e.g. I can't understand French or Russian exception messages.

The idea is to translate only messages that are displayed in MainUI, in particular the detailed thing status.

@lolodomo
Copy link
Contributor Author

From my pov we should focus on translatable exceptions here. In detail: a way to use openHAB based way of retrieving a translated exception message via getLocalizedMessage(). We should keep default behavior for getMessage() by returning an English message.

That is what I implemented.

The status message already will be translated by the framework. I am using it in OpenWeatherMap binding in combination with exceptions by passing the @text/... placeholder to it where it is thrown.

That is the purpose of getUntranslatedMessage to rebuild the "@text/..." key.

Your current getUntranslatedMessage() might rather be renamed to getRawMessage().

Ok, I am changing it.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Oct 31, 2021

@cweitkamp : all comments handled. Adding unit tests was a good idea ;) I did not add unit tests for the exception created with a throwable cause. If necessary, I can add them.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@cweitkamp cweitkamp added enhancement An enhancement or new feature of the Core i18n/l10n Internationalization/Localization labels Nov 1, 2021
Copy link
Member

@wborn wborn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, but I still don't think this is a good approach. Exceptions should remain simple information carriers and you should not inject complex objects like Bundle or TranslationProvider into them. Another approach could for instance be to have a I18nExceptionFactory instead that uses the Bundle and TranslationProvider to create this exception with an English and localized message.

@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 1, 2021

Sorry, but I still don't think this is a good approach. Exceptions should remain simple information carriers and you should not inject complex objects like Bundle or TranslationProvider into them. Another approach could for instance be to have a I18nExceptionFactory instead that uses the Bundle and TranslationProvider to create this exception with an English and localized message.

If you mean a I18nExceptionFactory service inheriting TranslationProvider and LocalProvider, this is possible but the problem I see with this solution is that you have to pass this factory to all your binding classes that need to throw an exception (to create the exception with the factory).

To use a service only on the higher level (thing handler), I imagine more a I18nExceptionTranslationProvider providing three methods:
String getMessage(I18nException e)
String getLocalizedMessage(I18nException e)
String getRawMessage(I18nException e)
Of course, in this case, e.getMessage or e.getLocalizedMessage will return a null string.

@wborn
Copy link
Member

wborn commented Nov 1, 2021

Yes some other approach that keeps it all very developer friendly is also fine with me. As long as it does not inject these complex objects into the exception. The issue with that is that Exceptions can end up anywhere in your application. Then if some code stores these exceptions into a variable, the complex objects cannot be garbage collected once they are no longer used. It would also be nice if you can still create tests and check thrown exceptions without also having to create all sorts of providers/factories. 🙂

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 4, 2021

Ok, I think I have a solution. I am going to create an interface ExceptionTranslationProvider with a method getMessage(exception, bundle, locale) + the corresponding service (I18nProviderImpl).

@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 4, 2021

Or maybe I could simply add bundle, translationProvider and locale as parameters to the methods getMessage and getLocalizedMessage. It would avoid "injection" In the exception object.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 4, 2021

This is now changed.
@wborn : is it now as you like ?

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 7, 2021

Any feedback ?

@wborn
Copy link
Member

wborn commented Nov 11, 2021

Yes I see you have addressed my main concern! 👍

Another concern I have with this approach is that it promotes using one meaningless exception type for everything (similar to always throwing RuntimeException when any exception occurs). Which is not how exceptions should be used. You should throw a particular type of exception so you can properly handle them depending on what kind of exception is thrown.

That could for instance be solved by making the class abstract so it forces you to think about exception handling and create your own exception types. But this would result in more code and make it less convenient to use. But I think it is good to think this through as it can have consequences when many add-ons start using this.

We could also add a few more common exception classes that extend from the abstract class which would make it more easy to use and which also shows how to use it. Many add-ons for instance have to deal with connection or configuration exceptions.

I also noticed that it is now a checked exception because it extends from Exception instead of RuntimeException. This may unintenionally result in more code/throws when exceptions have to propagate through several classes.

@lolodomo
Copy link
Contributor Author

I agree that an abstract class would be better. And if it is better to extend RuntimeException, that is fine for me. I will apply these 2 changes.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

@wborn : done

@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 11, 2021

We could also add a few more common exception classes that extend from the abstract class which would make it more easy to use and which also shows how to use it. Many add-ons for instance have to deal with connection or configuration exceptions

Yes, that is also a good idea. I just want to avoid blocking this PR because we do not agree quickly on a list of exception classes.
So I propose ConnectionException, CommunicationException and ConfigurationException. Is it ok ?

…ption

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
@lolodomo
Copy link
Contributor Author

lolodomo commented Nov 11, 2021

Classes ConnectionException, CommunicationException and ConfigurationException added.

Maybe ConnectionException should rather extend CommunicationException ?

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Copy link
Member

@wborn wborn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PR is in a good shape to get merged now. We can always further tune this in follow-up PRs. I see there is still an unresolved comment by @cweitkamp. If you think it is resolved you can merge this PR (unless you have more comments 😉).

Copy link
Contributor

@cweitkamp cweitkamp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks.

@cweitkamp cweitkamp added this to the 3.2 milestone Nov 12, 2021
@cweitkamp cweitkamp merged commit 998ce26 into openhab:main Nov 12, 2021
@cweitkamp
Copy link
Contributor

@lolodomo May I ask you to write a short abstract for the developer documentation? Thanks.

@lolodomo lolodomo deleted the I18nException branch November 12, 2021 20:08
splatch pushed a commit to ConnectorIO/copybara-hab-core that referenced this pull request Jul 12, 2023
…ion (openhab#2549)

* New exception class that incorporates support for internationalization
* Add ConnectionException, CommunicationException and ConfigurationException

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
GitOrigin-RevId: 998ce26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An enhancement or new feature of the Core i18n/l10n Internationalization/Localization
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants