]> git.basschouten.com Git - openhab-addons.git/commitdiff
[knx] Improve localization (#13293)
authorHolger Friedrich <holgerfriedrich@users.noreply.github.com>
Tue, 11 Oct 2022 19:18:17 +0000 (21:18 +0200)
committerGitHub <noreply@github.com>
Tue, 11 Oct 2022 19:18:17 +0000 (21:18 +0200)
* [knx] Improve localization

- introduce 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>
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProviderTest.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedLocaleProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedTranslationProvider.java [new file with mode: 0644]

index 397d777fe8870d6e09740ffe03cdd6b953b202ee..36556c59abc85936cb71356a49a8bdea318eae67 100644 (file)
@@ -28,6 +28,7 @@ import org.eclipse.jdt.annotation.Nullable;
 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;
@@ -289,9 +290,8 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
     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);
         }
@@ -406,8 +406,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
             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();
index 3539144e945f14e88ddbed9acbffb62fc2925527..913786aeea9481d95200cba139c33eeeca5b5335 100644 (file)
@@ -20,7 +20,10 @@ import java.util.Collection;
 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;
@@ -29,6 +32,7 @@ import org.openhab.core.thing.ThingUID;
 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;
 
@@ -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);
index 27dcef9e42db7e03eade98c3fa812de2b2f95a4a..913e91480950d54f57abe113d8e5170af421f38c 100644 (file)
@@ -25,6 +25,7 @@ import org.openhab.binding.knx.internal.client.DeviceInspector;
 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;
@@ -164,7 +165,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
         } 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));
         }
     }
 
@@ -194,7 +196,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
         } 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();
index 83f0e0fc0f54c4f0253131b834e360be5949909f..b362cc3dab60fdc067fa6eab5c6494998a13ec42 100644 (file)
@@ -23,6 +23,7 @@ import org.openhab.binding.knx.internal.client.IPClient;
 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;
@@ -87,11 +88,12 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
         } catch (KnxSecureException e) {
             logger.debug("{}, {}", thing.getUID(), e.toString());
 
-            String message = e.getLocalizedMessage();
-            if (message == null) {
-                message = e.getClass().getSimpleName();
+            Throwable cause = e.getCause();
+            if (cause == null) {
+                cause = e;
             }
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    KNXTranslationProvider.I18N.getLocalizedException(cause));
             return;
         }
 
@@ -120,7 +122,7 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
             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)
@@ -128,7 +130,7 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
             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;
             }
 
@@ -150,22 +152,21 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
             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;
         }
 
diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProvider.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProvider.java
new file mode 100644 (file)
index 0000000..47b504b
--- /dev/null
@@ -0,0 +1,128 @@
+/**
+ * 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 singleton, 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, getLocale 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 possible, we still have the original text without any subsititutions
+        if (arguments == null || arguments.length == 0) {
+            return text;
+        }
+        // else execute pattern substitution 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 Throwable e) {
+        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;
+    }
+}
index 631a9bb7adb09640c609b605b33a3a7490928aba..93efd991625e67244e47ecdbf347ff8eac227787 100644 (file)
@@ -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
diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProviderTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/KNXTranslationProviderTest.java
new file mode 100644 (file)
index 0000000..78a684a
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * 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 static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+import tuwien.auto.calimero.KNXException;
+import tuwien.auto.calimero.link.KNXLinkClosedException;
+
+/**
+ *
+ * @author Holger Friedrich - initial contribution
+ *
+ */
+@NonNullByDefault
+public class KNXTranslationProviderTest {
+    static final String UNKNOWN = "unknown text";
+    static final String UNKNOWN_PATTERN = "unknown text {0}";
+    static final String UNKNOWN_FIVE = "unknown text 5";
+    static final String UNKNOWN_NULL = "unknown text null";
+    static final String KNX_BINDING_KEY = "binding.knx.name";
+    static final String KNX_BINDING_VALUE = "KNX Binding";
+    static final String CONN_TYPE_PATTERN_KEY = "error.knx-unknown-ip-connection-type";
+    static final String CONN_TYPE_PATTERN_VALUE = "Unknown IP connection type: {0}.";
+    static final String CONN_TYPE_FIVE_VALUE = "Unknown IP connection type: 5.";
+    static final String CONN_TYPE_NULL_VALUE = "Unknown IP connection type: null.";
+
+    @Test
+    public void testGetBeforeInit() {
+        // initial state, should not crash and preferrably return original strings (w. pattern substitution)
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN));
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN, 5));
+        assertEquals(UNKNOWN_NULL, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, null, null));
+        assertEquals(UNKNOWN_FIVE, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, 5));
+        // KNXTranslationProvider.I18N.get(..., null) would cause a compiler warning,
+        // but using a null object of a defined type, it is possible to invoke with null value
+        String s = null;
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN, s));
+        assertEquals(UNKNOWN_NULL, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, s));
+    }
+
+    @Test
+    public void testSetProvider() {
+        // initial state, should not crash
+        KNXTranslationProvider.I18N.setProvider(null, null);
+        assertNotNull(KNXTranslationProvider.I18N.get(UNKNOWN));
+
+        // use mockup classes with known dictionary
+        KNXTranslationProvider.I18N.setProvider(new MockedLocaleProvider(), new MockedTranslationProvider());
+        assertEquals(KNX_BINDING_VALUE, KNXTranslationProvider.I18N.get(KNX_BINDING_KEY));
+        assertEquals(CONN_TYPE_FIVE_VALUE, KNXTranslationProvider.I18N.get(CONN_TYPE_PATTERN_KEY, 5));
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN));
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN, 5));
+        assertEquals(UNKNOWN_NULL, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, null, null));
+        assertEquals(UNKNOWN_FIVE, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, 5));
+        // KNXTranslationProvider.I18N.get(..., null) would cause a compiler warning,
+        // but using a null object of a defined type, it is possible to invoke with null value
+        String s = null;
+        assertEquals(UNKNOWN, KNXTranslationProvider.I18N.get(UNKNOWN, s));
+        assertEquals(UNKNOWN_NULL, KNXTranslationProvider.I18N.get(UNKNOWN_PATTERN, s));
+
+        assertEquals(CONN_TYPE_NULL_VALUE, KNXTranslationProvider.I18N.get(CONN_TYPE_PATTERN_KEY, s));
+        assertEquals(CONN_TYPE_PATTERN_VALUE, KNXTranslationProvider.I18N.get(CONN_TYPE_PATTERN_KEY));
+
+        // no locale, should work as fallback to default locale
+        KNXTranslationProvider.I18N.setProvider(null, new MockedTranslationProvider());
+        assertEquals(KNXTranslationProvider.I18N.get(KNX_BINDING_KEY), KNX_BINDING_VALUE);
+
+        // no translations, should return initial string
+        KNXTranslationProvider.I18N.setProvider(new MockedLocaleProvider(), null);
+        assertEquals(KNX_BINDING_KEY, KNXTranslationProvider.I18N.get(KNX_BINDING_KEY));
+
+        // initial state, dictionary should be gone
+        KNXTranslationProvider.I18N.setProvider(null, null);
+        assertEquals(KNX_BINDING_KEY, KNXTranslationProvider.I18N.get(KNX_BINDING_KEY));
+    }
+
+    @Test
+    public void testGetLocalizedException() {
+        // initial state, should not crash
+        KNXTranslationProvider.I18N.setProvider(null, null);
+
+        final Exception e = new KNXException("error 1");
+        final Exception se = new KNXLinkClosedException("connection closed", e);
+        assertNotNull(KNXTranslationProvider.I18N.getLocalizedException(e));
+        assertNotNull(KNXTranslationProvider.I18N.getLocalizedException(se));
+        assertEquals("KNXException, error 1", KNXTranslationProvider.I18N.getLocalizedException(e));
+
+        // use mockup classes with known dictionary
+        KNXTranslationProvider.I18N.setProvider(new MockedLocaleProvider(), new MockedTranslationProvider());
+        // exception which is not part off the dictionary
+        assertEquals("KNXLinkClosedException, connection closed",
+                KNXTranslationProvider.I18N.getLocalizedException(se));
+        // exception which can be translated
+        assertEquals("Translated KNX Exception (KNXException, error 1)",
+                KNXTranslationProvider.I18N.getLocalizedException(e));
+    }
+}
diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedLocaleProvider.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedLocaleProvider.java
new file mode 100644 (file)
index 0000000..d247a9b
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * 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.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.i18n.LocaleProvider;
+
+/**
+ * Locale provider for unit tests.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class MockedLocaleProvider implements LocaleProvider {
+    public Locale getLocale() {
+        return Locale.ENGLISH;
+    }
+}
diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedTranslationProvider.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/i18n/MockedTranslationProvider.java
new file mode 100644 (file)
index 0000000..7aa5501
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * 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 static java.util.Map.entry;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+
+/**
+ * Translation provider for unit tests.
+ *
+ * @author Jacob Laursen - Initial contribution
+ * @author Holger Friedrich - Imported from hdpowerview, adapted
+ */
+@NonNullByDefault
+public class MockedTranslationProvider implements TranslationProvider {
+
+    private static final Map<String, String> TEXTS = Map.ofEntries(entry("binding.knx.name", "KNX Binding"),
+            entry("error.knx-unknown-ip-connection-type", "Unknown IP connection type: {0}."),
+            entry("exception.KNXException", "Translated KNX Exception"));
+
+    public MockedTranslationProvider() {
+    }
+
+    @Nullable
+    public String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText,
+            @Nullable Locale locale) {
+        return "";
+    }
+
+    @Nullable
+    public String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText,
+            @Nullable Locale locale, @Nullable Object @Nullable... arguments) {
+        String text = TEXTS.get(key);
+        return MessageFormat.format(text != null ? text : key, arguments);
+    }
+}