Skip to content

Commit

Permalink
[knx] Improve localization
Browse files Browse the repository at this point in the history
- indroduce localization of error messages
- add new strings for common exceptions
- provide helper functions for translation
- add test cases

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
  • Loading branch information
holgerfriedrich committed Sep 28, 2022
1 parent 24d5f2d commit 0fecab5
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.openhab.binding.knx.internal.KNXTypeMapper;
import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
import org.openhab.binding.knx.internal.handler.GroupAddressListener;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
Expand Down Expand Up @@ -289,9 +290,8 @@ private void disconnect(@Nullable Exception e) {
private synchronized void disconnect(@Nullable Exception e, Optional<ThingStatusDetail> detail) {
releaseConnection();
if (e != null) {
final String message = e.getLocalizedMessage();
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, detail.orElse(ThingStatusDetail.COMMUNICATION_ERROR),
message != null ? message : "");
KNXTranslationProvider.I18N.getLocalizedException(e));
} else {
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
}
Expand Down Expand Up @@ -406,8 +406,9 @@ public void linkClosed(@Nullable CloseEvent closeEvent) {
return;
}
if (!link.isOpen() && CloseEvent.USER_REQUEST != closeEvent.getInitiator()) {
final String reason = closeEvent.getReason();
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
closeEvent.getReason());
KNXTranslationProvider.I18N.get(reason));
logger.debug("KNX link has been lost (reason: {} on object {})", closeEvent.getReason(),
closeEvent.getSource().toString());
scheduleReconnectJob();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import org.openhab.binding.knx.internal.handler.DeviceThingHandler;
import org.openhab.binding.knx.internal.handler.IPBridgeThingHandler;
import org.openhab.binding.knx.internal.handler.SerialBridgeThingHandler;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
Expand All @@ -29,6 +32,7 @@
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

Expand All @@ -46,6 +50,12 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {

private NetworkAddressService networkAddressService;

@Activate
public KNXHandlerFactory(final @Reference TranslationProvider translationProvider,
final @Reference LocaleProvider localeProvider) {
KNXTranslationProvider.I18N.setProvider(localeProvider, translationProvider);
}

@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.openhab.binding.knx.internal.client.DeviceInspector.Result;
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.config.DeviceConfig;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
Expand Down Expand Up @@ -164,7 +165,8 @@ private void pollDeviceStatus() {
} catch (KNXException e) {
logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
KNXTranslationProvider.I18N.getLocalizedException(e));
}
}

Expand Down Expand Up @@ -194,7 +196,8 @@ protected void attachToClient() {
} catch (KNXFormatException e) {
logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
KNXTranslationProvider.I18N.getLocalizedException(e));
}
getClient().registerGroupAddressListener(this);
scheduleReadJobs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.client.NoOpClient;
import org.openhab.binding.knx.internal.config.IPBridgeConfiguration;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
Expand Down Expand Up @@ -87,11 +88,11 @@ public void initializeLater() {
} catch (KnxSecureException e) {
logger.debug("{}, {}", thing.getUID(), e.toString());

String message = e.getLocalizedMessage();
if (message == null) {
message = e.getClass().getSimpleName();
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message);
Throwable cause = e.getCause();
if (cause == null)
cause = e;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
KNXTranslationProvider.I18N.getLocalizedException(cause));
return;
}

Expand Down Expand Up @@ -120,15 +121,15 @@ public void initializeLater() {
if (!securityAvailable) {
logger.warn("Bridge {} missing security configuration for secure tunnel", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration missing for secure tunnel");
"@text/error.knx-secure-tunnel-config-missing");
return;
}
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
&& (secureTunnel.userKey.length == 16));
if (!tunnelOk) {
logger.warn("Bridge {} incomplete security configuration for secure tunnel", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration for secure tunnel is incomplete");
"@text/error.knx-secure-tunnel-config-incomplete");
return;
}

Expand All @@ -150,22 +151,21 @@ public void initializeLater() {
if (!securityAvailable) {
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration missing for secure routing");
"@text/error.knx-secure-routing-config-missing");
return;
}
if (secureRouting.backboneGroupKey.length != 16) {
// failed to read shared backbone group key from config
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
logger.warn("Bridge {} invalid security configuration for secure routing", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"backboneGroupKey required for secure routing; please configure");
"@text/error.knx-secure-routing-backbonegroupkey-invalid");
return;
}
logger.debug("KNX secure routing needs a few seconds to establish connection");
} else {
logger.debug("Bridge {} unknown connection type", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format(
"Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'",
connectionTypeString));
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
MessageFormat.format("@text/knx-unknown-ip-connection-type", connectionTypeString));
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.i18n;

import java.text.MessageFormat;
import java.util.Locale;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

/**
* This class provides translations. It is a helper class for i18n / localization efforts.
*
* @implNote It is implemented as a static singelton, enforced by the single-element enum pattern.
* @apiNote @set() must be called to provide tanslation service, otherwise all functions will return untranslated text.
* Thread safety is ensured.
* @author Holger Friedrich - Initial contribution
*
*/
@NonNullByDefault
public enum KNXTranslationProvider {
I18N;

private @Nullable LocaleProvider localeProvider;
private @Nullable TranslationProvider translationProvider;
private Bundle bundle;

private KNXTranslationProvider() {
localeProvider = null;
translationProvider = null;
bundle = FrameworkUtil.getBundle(this.getClass());
}

/**
* get translated text
*
* @param text text to be translated, may contain placeholders \{n\} for the n-th optional argument of this function
* @param arguments any optional arguments, will be inserted
* @return translated text with subsitutions if translationprovide is set and provides a translation, otherwise
* returns original text with substitutions
*/
public String get(final String text, @Nullable Object @Nullable... arguments) {
// ensure thread safety: calls to set(..) should not lead to race condition
final TranslationProvider translationProvider = this.translationProvider;
final LocaleProvider localeProvider = this.localeProvider;
if (translationProvider != null) {
// localeProvider might be null, but if not, getLoacle will return NonNull Locale
// locale cannot be cached, as getLocale() will return different result once locale is changed by user
final Locale locale = (localeProvider != null) ? localeProvider.getLocale() : Locale.getDefault();
final String res = translationProvider.getText(bundle, text, text, locale, arguments);
if (res != null)
return res;
}
// translating not possibe, we still have the original text without any subsititutions
if ((arguments == null) || (arguments.length == 0)) {
return text;
}
// else execute pattern subsitution in untranslated text
return MessageFormat.format(text, arguments);
}

/**
* get exception in user readable (and possibly localized) form
*
* @param e any exception
* @return localized message in form <description (translated)> (<class name>, <e.getLocalizedMessage (not
* translated)>), empty string for null. May possibly change in further releases.
*/
public String getLocalizedException(final @Nullable Throwable e) {
if (e == null) {
return "";
}
StringBuffer res = new StringBuffer();
final String exName = e.getClass().getSimpleName();
final String key = "exception." + exName;
final String translatedDescription = KNXTranslationProvider.I18N.get(key);
Boolean foundTranslation = !key.equals(translatedDescription);
// detailed message cannot be translated, e.getLocalizedMessage will likely return English
String detail = e.getLocalizedMessage();
if (detail == null) {
detail = "";
}

if (foundTranslation) {
res.append(translatedDescription);
res.append(" (");
res.append(exName);
if (!detail.isBlank()) {
res.append(", ");
res.append(detail);
}
res.append(")");
} else {
res.append(exName);
if (!detail.isBlank()) {
res.append(", ");
res.append(detail);
}
}
return res.toString();
}

/**
* Set translation providers. To be called to make any translation work.
*
* @param localeProvider openHAB locale provider, can be generated via \@Activate / \@Reference LocaleProvider in
* handler factory
* @param translationProvider openHAB locale provider, can be generated via \@Activate / \@Reference
* TranslationProvider in handler factory
*/
public void setProvider(@Nullable LocaleProvider localeProvider,
@Nullable TranslationProvider translationProvider) {
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,29 @@ channel-type.config.knx.single.ga.description = The group address(es) in Group A

thing-type.config.knx.serial.group.knxsecure.label = KNX secure
thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation.

# binding specific strings

error.knx-secure-routing-backbonegroupkey-invalid = backbonegroupkey invalid, please check if it is specified correctly
error.knx-secure-routing-config-missing = Security configuration missing for secure routing
error.knx-secure-tunnel-config-incomplete = Security configuration for secure tunnel is incomplete
error.knx-secure-tunnel-config-missing = Security configuration for secure tunnel is missing
error.knx-unknown-ip-connection-type = Unknown IP connection type: {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'.

# user readable description of exceptions

exception.KNXAckTimeoutException = Communication timeout, missing response
exception.KNXConnectionClosedException = Connection closed
exception.KNXDisconnectException = Disconnected
exception.KNXException = KNX exception
exception.KNXFormatException = Data format not valid
exception.KNXIllegalArgumentException = Illegal argument
exception.KNXInvalidResponseException = Invalid response from KNX device
exception.KNXLinkClosedException = Link closed, cannot communicate
exception.KNXMLException = Error processing XML data
exception.KNXPortClosedException = Port closed, cannot communicate
exception.KnxPropertyException = Error accessing KNX property
exception.KNXRemoteException = KNX device indicates error
exception.KnxRuntimeException = Runtime error
exception.KnxSecureException = Error processing KNX secure data
exception.KNXTimeoutException = Communication timeout
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,30 @@ channel-type.config.knx.single.ga.description = Die Gruppenadresse(n) in Gruppen

thing-type.config.knx.serial.group.knxsecure.label = KNX Secure
thing-type.config.knx.serial.group.knxsecure.description = Einstellungen für KNX Secure. Benötigt eine KNX Anlage mit aktiven KNX Secure Funktionen.


# binding specific strings

error.knx-secure-routing-backbonegroupkey-invalid = backbonegroupkey ungültig, bitte die Schreibeweise prüfen
error.knx-secure-routing-config-missing = KNX Secure Konfiguration für Secure Routing fehlt
error.knx-secure-tunnel-config-incomplete = KNX Secure Konfiguration für Secure Tunnel ist unvollständig
error.knx-secure-tunnel-config-missing = KNX Secure Konfiguration für Secure Tunnel fehlt
error.knx-unknown-ip-connection-type = Ungültiger KNX Verbindungstyp {0}. Zulässige Werte sind 'TUNNEL', 'ROUTER', 'SECURETUNNEL' oder 'SECUREROUTER'.

# user readable description of exceptions

exception.KNXAckTimeoutException = Kommunikationsfehler, Gerät antwortet icht
exception.KNXConnectionClosedException = Verbindung geschlossen
exception.KNXDisconnectException = Nicht verbunden
exception.KNXException = KNX Ausnahmefehler
exception.KNXFormatException = Datenformat ungültig
exception.KNXIllegalArgumentException = Ungültiges Argument
exception.KNXInvalidResponseException = Unerwartete Geräteantwort
exception.KNXLinkClosedException = Verbindung unterbrochen
exception.KNXMLException = Fehler beim Verarbeiten der Daten
exception.KNXPortClosedException = Port geschlosssen, keine Verbindung
exception.KnxPropertyException = Fehler beimm Lesen der Geräteeigenschaft
exception.KNXRemoteException = Gerät meldet Fehler
exception.KnxRuntimeException = Lufzeitfehler
exception.KnxSecureException = KNX Secure Fehler
exception.KNXTimeoutException = Zeitüberschreitung
Loading

0 comments on commit 0fecab5

Please sign in to comment.