Add i18n-maven-plugin to make internationalization easier (#2544)

* Add i18n-maven-plugin to make internationalization easier

This plugin simplifies generating the default translation .properties files from the add-on XML information files.

It reuses the same XStream parsing classes that are used by openhab-core for parsing the binding/config/thing XML files.
It will also keep any existing default translations already present in property files for translations using `@text/`.
Furthermore it will nicely group and sort the translations.

After building this Maven plugin you can use it on add-ons using:

`mvn org.openhab.core.tools:i18n-maven-plugin:3.2.0-SNAPSHOT:generate-default-translations`

Signed-off-by: Wouter Born <github@maindrain.net>
This commit is contained in:
Wouter Born 2021-12-01 18:52:03 +01:00 committed by GitHub
parent 8c1fe60abc
commit bb3224a434
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2323 additions and 0 deletions

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>i18n-maven-plugin</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

111
tools/i18n-plugin/pom.xml Normal file
View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.core.tools</groupId>
<artifactId>org.openhab.core.reactor.tools</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>
<artifactId>i18n-maven-plugin</artifactId>
<packaging>maven-plugin</packaging>
<name>Internationalization Maven Plugin</name>
<description>Generates translations files</description>
<properties>
<maven.core.version>3.6.0</maven.core.version>
<maven.plugin.api.version>3.6.0</maven.plugin.api.version>
<maven.plugin.annotations.version>3.6.0</maven.plugin.annotations.version>
<maven.plugin.plugin.version>3.6.0</maven.plugin.plugin.version>
<maven.plugin.compiler.version>3.8.1</maven.plugin.compiler.version>
</properties>
<dependencies>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.18</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.plugin.api.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${maven.plugin.annotations.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>${maven.plugin.plugin.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.annotation</artifactId>
<version>2.2.100</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.binding.xml</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.config.xml</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.thing.xml</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.test-index</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>${maven.plugin.plugin.version}</version>
<executions>
<execution>
<id>default-addPluginArtifactMetadata</id>
<goals>
<goal>addPluginArtifactMetadata</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>default-descriptor</id>
<goals>
<goal>descriptor</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import java.io.File;
import java.io.IOException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Base class for internationalization mojos using openHAB XML information.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractI18nMojo extends AbstractMojo {
/**
* The directory containing the bundle openHAB information
*/
@Parameter(property = "i18n.ohinf.dir", defaultValue = "${project.basedir}/src/main/resources/OH-INF")
protected @NonNullByDefault({}) File ohinfDirectory;
protected BundleInfo bundleInfo = new BundleInfo();
protected boolean ohinfExists() {
return ohinfDirectory.exists();
}
protected void readAddonInfo() throws IOException {
BundleInfoReader bundleInfoReader = new BundleInfoReader(getLog());
bundleInfo = bundleInfoReader.readBundleInfo(ohinfDirectory.toPath());
}
void setOhinfDirectory(File ohinfDirectory) {
this.ohinfDirectory = ohinfDirectory;
}
}

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.binding.xml.internal.BindingInfoXmlResult;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.thing.xml.internal.ChannelGroupTypeXmlResult;
import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;
/**
* The bundle information provided by the openHAB XML files in the <code>OH-INF</code> directory.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class BundleInfo {
private @Nullable BindingInfoXmlResult bindingInfoXml;
private List<ConfigDescription> configDescriptions = new ArrayList<>(5);
private List<ChannelGroupTypeXmlResult> channelGroupTypesXml = new ArrayList<>(5);
private List<ChannelTypeXmlResult> channelTypesXml = new ArrayList<>(5);
private List<ThingTypeXmlResult> thingTypesXml = new ArrayList<>(5);
public @Nullable BindingInfoXmlResult getBindingInfoXml() {
return bindingInfoXml;
}
public void setBindingInfoXml(BindingInfoXmlResult bindingInfo) {
this.bindingInfoXml = bindingInfo;
}
public List<ConfigDescription> getConfigDescriptions() {
return configDescriptions;
}
public void setConfigDescriptions(List<ConfigDescription> configDescriptions) {
this.configDescriptions = configDescriptions;
}
public List<ChannelGroupTypeXmlResult> getChannelGroupTypesXml() {
return channelGroupTypesXml;
}
public void setChannelGroupTypesXml(List<ChannelGroupTypeXmlResult> channelGroupTypesXml) {
this.channelGroupTypesXml = channelGroupTypesXml;
}
public List<ChannelTypeXmlResult> getChannelTypesXml() {
return channelTypesXml;
}
public void setChannelTypesXml(List<ChannelTypeXmlResult> channelTypesXml) {
this.channelTypesXml = channelTypesXml;
}
public List<ThingTypeXmlResult> getThingTypesXml() {
return thingTypesXml;
}
public void setThingTypesXml(List<ThingTypeXmlResult> thingTypesXml) {
this.thingTypesXml = thingTypesXml;
}
public String getBindingId() {
BindingInfoXmlResult localBindingInfoXml = bindingInfoXml;
return localBindingInfoXml == null ? "" : localBindingInfoXml.getBindingInfo().getUID();
}
public Optional<ConfigDescription> getConfigDescription(@Nullable URI uri) {
if (uri == null) {
return Optional.empty();
}
return configDescriptions.stream().filter(configDescription -> configDescription.getUID().equals(uri))
.findFirst();
}
}

View File

@ -0,0 +1,117 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import org.apache.maven.plugin.logging.Log;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.binding.xml.internal.BindingInfoReader;
import org.openhab.core.binding.xml.internal.BindingInfoXmlResult;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.xml.internal.ConfigDescriptionReader;
import org.openhab.core.thing.xml.internal.ChannelGroupTypeXmlResult;
import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
import org.openhab.core.thing.xml.internal.ThingDescriptionReader;
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;
import com.thoughtworks.xstream.converters.ConversionException;
/**
* Reads all the bundle information provided by XML files in the <code>OH-INF</code> directory to {@link BundleInfo}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class BundleInfoReader {
private final Log log;
public BundleInfoReader(Log log) {
this.log = log;
}
public BundleInfo readBundleInfo(Path ohinfPath) throws IOException {
BundleInfo bundleInfo = new BundleInfo();
readBindingInfo(ohinfPath, bundleInfo);
readConfigInfo(ohinfPath, bundleInfo);
readThingInfo(ohinfPath, bundleInfo);
return bundleInfo;
}
private Stream<Path> xmlPathStream(Path ohinfPath, String directory) throws IOException {
Path path = ohinfPath.resolve(directory);
return Files.exists(path)
? Files.find(path, Integer.MAX_VALUE, (filePath, fileAttr) -> fileAttr.isRegularFile())
: Stream.of();
}
private void readBindingInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
BindingInfoReader reader = new BindingInfoReader();
xmlPathStream(ohinfPath, "binding").forEach(path -> {
log.info("Reading: " + path);
try {
BindingInfoXmlResult bindingInfoXml = reader.readFromXML(path.toUri().toURL());
if (bindingInfoXml != null) {
bundleInfo.setBindingInfoXml(bindingInfoXml);
}
} catch (ConversionException | MalformedURLException e) {
log.warn("Exception while reading binding info from: " + path, e);
}
});
}
private void readConfigInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
ConfigDescriptionReader reader = new ConfigDescriptionReader();
xmlPathStream(ohinfPath, "config").forEach(path -> {
log.info("Reading: " + path);
try {
List<ConfigDescription> configDescriptions = reader.readFromXML(path.toUri().toURL());
if (configDescriptions != null) {
bundleInfo.getConfigDescriptions().addAll(configDescriptions);
}
} catch (ConversionException | MalformedURLException e) {
log.warn("Exception while reading config info from: " + path, e);
}
});
}
private void readThingInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
ThingDescriptionReader reader = new ThingDescriptionReader();
xmlPathStream(ohinfPath, "thing").forEach(path -> {
log.info("Reading: " + path);
try {
List<?> types = reader.readFromXML(path.toUri().toURL());
if (types == null) {
return;
}
for (Object type : types) {
if (type instanceof ThingTypeXmlResult) {
bundleInfo.getThingTypesXml().add((ThingTypeXmlResult) type);
} else if (type instanceof ChannelGroupTypeXmlResult) {
bundleInfo.getChannelGroupTypesXml().add((ChannelGroupTypeXmlResult) type);
} else if (type instanceof ChannelTypeXmlResult) {
bundleInfo.getChannelTypesXml().add((ChannelTypeXmlResult) type);
}
}
} catch (ConversionException | MalformedURLException e) {
log.warn("Exception while reading thing info from: " + path, e);
}
});
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enumerates all the different modes for generating default translations.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum DefaultTranslationsGenerationMode {
/**
* Creates XML based default translations files only when these do not yet exist.
*/
ADD_MISSING_FILES,
/**
* Same as {@link #ADD_MISSING_FILES} but also adds missing translations to existing default translations files.
*/
ADD_MISSING_TRANSLATIONS,
/**
* Removes existing default translation files and regenerates them based on the XML based texts only.
*/
REGENERATE_FILES
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static java.nio.file.StandardOpenOption.*;
import static org.openhab.core.tools.i18n.plugin.DefaultTranslationsGenerationMode.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.ConfigDescription;
/**
* Generates the default translations properties file for a bundle based on the XML files in the <code>OH-INF</code>
* directory.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
@Mojo(name = "generate-default-translations", threadSafe = true)
public class GenerateDefaultTranslationsMojo extends AbstractI18nMojo {
private static final Set<String> ADDON_TYPES = Set.of("automation", "binding", "io", "persistence", "transform",
"voice");
/**
* The directory where the properties files will be generated
*/
@Parameter(property = "i18n.target.dir", defaultValue = "${project.basedir}/src/main/resources/OH-INF/i18n")
private @NonNullByDefault({}) File targetDirectory;
@Parameter(property = "i18n.generation.mode", defaultValue = "ADD_MISSING_TRANSLATIONS")
private DefaultTranslationsGenerationMode generationMode = ADD_MISSING_TRANSLATIONS;
@Override
public void execute() throws MojoFailureException {
try {
if (ohinfExists()) {
readAddonInfo();
Path defaultTranslationsPath = ohinfDirectory.toPath()
.resolve(Path.of("i18n", propertiesFileName(bundleInfo)));
if (Files.exists(defaultTranslationsPath)) {
if (generationMode == ADD_MISSING_FILES) {
getLog().info("Skipped: " + defaultTranslationsPath);
return;
} else if (generationMode == REGENERATE_FILES) {
try {
Files.delete(defaultTranslationsPath);
getLog().info("Deleted: " + defaultTranslationsPath);
} catch (IOException e) {
throw new MojoFailureException(
"Failed to delete existing default translations: " + defaultTranslationsPath, e);
}
}
}
String translationsString = generateDefaultTranslations(defaultTranslationsPath);
if (!translationsString.isBlank()) {
writeDefaultTranslations(translationsString);
}
}
} catch (IOException e) {
throw new MojoFailureException("Failed to read OH-INF XML files", e);
}
}
private String propertiesFileName(BundleInfo bundleInfo) {
String name = bundleInfo.getBindingId();
if (name.isEmpty()) {
Optional<ConfigDescription> optional = bundleInfo.getConfigDescriptions().stream().findFirst();
if (optional.isPresent()) {
ConfigDescription configDescription = optional.get();
String[] uid = configDescription.getUID().toString().split(":");
if (uid.length > 2 && ADDON_TYPES.contains(uid[1])) {
name = uid[2].toLowerCase();
} else {
name = uid[1].toLowerCase();
}
}
}
if (name.isBlank()) {
name = "unknown";
}
return name + ".properties";
}
protected String generateDefaultTranslations(Path defaultTranslationsPath) {
XmlToTranslationsConverter xmlConverter = new XmlToTranslationsConverter();
Translations generatedTranslations = xmlConverter.convert(bundleInfo);
PropertiesToTranslationsConverter propertiesConverter = new PropertiesToTranslationsConverter(getLog());
Translations existingTranslations = propertiesConverter.convert(defaultTranslationsPath);
TranslationsMerger translationsMerger = new TranslationsMerger();
translationsMerger.merge(generatedTranslations, existingTranslations);
return generatedTranslations.linesStream().collect(Collectors.joining(System.lineSeparator()));
}
private void writeDefaultTranslations(String translationsString) throws MojoFailureException {
Path translationsPath = targetDirectory.toPath().resolve(propertiesFileName(bundleInfo));
try {
Files.createDirectories(translationsPath.getParent());
} catch (IOException e) {
throw new MojoFailureException(
"Failed to create translations target directory: " + translationsPath.getParent(), e);
}
try {
getLog().info("Writing: " + translationsPath);
Files.writeString(translationsPath, translationsString, CREATE, TRUNCATE_EXISTING, WRITE);
} catch (IOException e) {
throw new MojoFailureException("Failed to write generated translations to: " + translationsPath, e);
}
}
void setTargetDirectory(File targetDirectory) {
this.targetDirectory = targetDirectory;
}
void setGenerationMode(DefaultTranslationsGenerationMode generationMode) {
this.generationMode = generationMode;
}
}

View File

@ -0,0 +1,131 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry.entry;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup.group;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection.section;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;
import org.apache.maven.plugin.logging.Log;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection;
/**
* Converts the translation key/value pairs of properties files to {@link Translations}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PropertiesToTranslationsConverter {
private static final String HASH = "#";
private final Log log;
public PropertiesToTranslationsConverter(Log log) {
this.log = log;
}
public Translations convert(Path propertiesPath) {
if (!Files.exists(propertiesPath)) {
log.debug("Properties file '" + propertiesPath + "' does not exist");
return Translations.translations();
}
List<String> lines;
try {
lines = Files.readAllLines(propertiesPath);
} catch (IOException e) {
log.warn("Exception while converting properties to translations: " + e.getMessage());
return Translations.translations();
}
Builder<TranslationsSection> sectionsBuilder = Stream.builder();
Builder<TranslationsGroup> groupsBuilder = null;
Builder<TranslationsEntry> entriesBuilder = null;
boolean appendHeader = false;
String header = "";
for (String line : lines) {
line = line.trim();
if (HASH.equals(line)) {
line = "";
}
if (line.startsWith(HASH)) {
if (!appendHeader) {
if (groupsBuilder != null) {
sectionsBuilder.add(section(header, groupsBuilder.build()));
}
header = "";
groupsBuilder = Stream.builder();
}
if (line.length() > 1) {
if (!header.isEmpty()) {
header += System.lineSeparator();
}
header += line.substring(1).trim().toLowerCase();
}
appendHeader = true;
continue;
}
appendHeader = false;
if (!line.isBlank()) {
int index = line.indexOf("=");
if (index == -1) {
log.warn("Ignoring invalid translation key/value pair: " + line);
} else {
if (entriesBuilder == null) {
entriesBuilder = Stream.builder();
}
String key = line.substring(0, index).trim();
String value = line.substring(index + 1).trim();
entriesBuilder.add(entry(key, value));
}
} else if (entriesBuilder != null) {
if (groupsBuilder == null) {
groupsBuilder = Stream.builder();
}
groupsBuilder.add(group(entriesBuilder.build()));
entriesBuilder = null;
}
}
if (entriesBuilder != null) {
if (groupsBuilder == null) {
groupsBuilder = Stream.builder();
}
groupsBuilder.add(group(entriesBuilder.build()));
}
if (groupsBuilder != null) {
sectionsBuilder.add(section(header, groupsBuilder.build()));
}
return Translations.translations(sectionsBuilder.build());
}
}

View File

@ -0,0 +1,207 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Translations} of a bundle consisting of {@link TranslationsSection}s having {@link TranslationsGroup}s of
* {@link TranslationsEntry}s (key/value pairs).
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Translations {
public static class TranslationsEntry {
public final String key;
final String value;
public TranslationsEntry(String key, String value) {
this.key = key;
this.value = value;
}
public boolean hasTranslation() {
return !value.isBlank() && !value.startsWith("@text/");
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public static TranslationsEntry entry(String key, @Nullable String value) {
return new TranslationsEntry(key, value == null ? "" : value);
}
}
public static class TranslationsGroup implements Comparable<TranslationsGroup> {
final List<TranslationsEntry> entries;
public TranslationsGroup(List<TranslationsEntry> entries) {
this.entries = entries;
}
@Override
public int compareTo(TranslationsGroup other) {
if (entries.isEmpty()) {
return -1;
} else if (other.entries.isEmpty()) {
return 1;
}
return entries.get(0).getKey().compareTo(other.entries.get(0).getKey());
}
public boolean hasTranslations() {
return !entries.isEmpty() && entries.stream().anyMatch(TranslationsEntry::hasTranslation);
}
public Stream<String> keysStream() {
return entries.stream() //
.filter(TranslationsEntry::hasTranslation) //
.map(TranslationsEntry::getKey);
}
public Stream<String> linesStream() {
return entries.stream() //
.filter(TranslationsEntry::hasTranslation) //
.map(entry -> String.format("%s = %s", entry.key, entry.value));
}
public void removeEntries(Predicate<? super TranslationsEntry> filter) {
entries.removeIf(filter);
}
public static TranslationsGroup group(Stream<TranslationsEntry> entries) {
return new TranslationsGroup(entries.collect(Collectors.toList()));
}
public static TranslationsGroup group(TranslationsEntry... entries) {
return group(Arrays.stream(entries));
}
}
public static class TranslationsSection {
final String header;
final List<TranslationsGroup> groups;
public TranslationsSection(List<TranslationsGroup> groups) {
this("", groups);
}
public TranslationsSection(String header, List<TranslationsGroup> groups) {
this.header = header;
this.groups = groups;
}
public boolean hasTranslations() {
return groups.stream().anyMatch(TranslationsGroup::hasTranslations);
}
public Stream<String> keysStream() {
return groups.stream() //
.map(TranslationsGroup::keysStream) //
.flatMap(Function.identity());
}
public Stream<String> linesStream() {
Builder<String> builder = Stream.builder();
if (!header.isBlank()) {
Arrays.stream(header.split(System.lineSeparator())) //
.map(line -> "# " + line) //
.forEach(builder::add);
builder.add("");
}
groups.stream() //
.filter(TranslationsGroup::hasTranslations) //
.map(TranslationsGroup::linesStream) //
.flatMap(Function.identity()) //
.forEach(builder::add);
builder.add("");
return builder.build();
}
public void removeEntries(Predicate<? super TranslationsEntry> filter) {
groups.forEach(group -> group.removeEntries(filter));
}
public static TranslationsSection section(Stream<TranslationsGroup> groups) {
return section("", groups);
}
public static TranslationsSection section(String header, Stream<TranslationsGroup> groups) {
return new TranslationsSection(header, groups.sorted().collect(Collectors.toList()));
}
public static TranslationsSection section(TranslationsGroup... groups) {
return section("", groups);
}
public static TranslationsSection section(String header, TranslationsGroup... groups) {
return section(header, Arrays.stream(groups));
}
}
final List<TranslationsSection> sections;
public Translations(List<TranslationsSection> sections) {
this.sections = sections;
}
boolean hasTranslations() {
return sections.stream().anyMatch(TranslationsSection::hasTranslations);
}
public void addSection(TranslationsSection section) {
sections.add(section);
}
public Stream<String> keysStream() {
return sections.stream() //
.map(TranslationsSection::keysStream) //
.flatMap(Function.identity());
}
public Stream<String> linesStream() {
return sections.stream() //
.filter(TranslationsSection::hasTranslations) //
.map(TranslationsSection::linesStream) //
.flatMap(Function.identity());
}
public void removeEntries(Predicate<? super TranslationsEntry> filter) {
sections.forEach(section -> section.removeEntries(filter));
}
static Translations translations(Stream<TranslationsSection> sections) {
return new Translations(sections.collect(Collectors.toList()));
}
static Translations translations(TranslationsSection... sections) {
return translations(Arrays.stream(sections));
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection;
/**
* Merges multiple {@link Translations} into one.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class TranslationsMerger {
/**
* Adds any missing translations from <code>missingTranslations</code> to <code>mainTranslations</code>.
*/
public void merge(Translations mainTranslations, Translations missingTranslations) {
Set<String> mainEntryKeys = mainTranslations.keysStream().collect(Collectors.toSet());
missingTranslations.removeEntries(entry -> mainEntryKeys.contains(entry.key));
missingTranslations.sections.stream() //
.filter(TranslationsSection::hasTranslations) //
.forEach(mainTranslations::addSection);
}
}

View File

@ -0,0 +1,361 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry.entry;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup.group;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection.section;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.binding.BindingInfo;
import org.openhab.core.binding.xml.internal.BindingInfoXmlResult;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionParameterGroup;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.xml.internal.ChannelGroupTypeXmlResult;
import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup;
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection;
import org.openhab.core.types.CommandDescription;
import org.openhab.core.types.StateDescription;
/**
* Converts XML based {@link BundleInfo} to {@link Translations}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class XmlToTranslationsConverter {
public Translations convert(BundleInfo bundleInfo) {
BindingInfoXmlResult bindingInfoXml = bundleInfo.getBindingInfoXml();
return bindingInfoXml == null ? configTranslations(bundleInfo) : bindingTranslations(bundleInfo);
}
private Translations bindingTranslations(BundleInfo bundleInfo) {
return Translations.translations( //
bindingSection(bundleInfo), //
bindingConfigSection(bundleInfo), //
thingTypesSection(bundleInfo), //
thingTypesConfigSection(bundleInfo), //
channelGroupTypesSection(bundleInfo), //
channelTypesSection(bundleInfo), //
channelTypesConfigSection(bundleInfo));
}
private Translations configTranslations(BundleInfo bundleInfo) {
Builder<TranslationsGroup> groupsBuilder = Stream.builder();
bundleInfo.getConfigDescriptions().stream().map(configDescription -> {
Object[] uid = configDescription.getUID().toString().split(":");
String configKeyPrefix = String.format("%s.config" + ".%s".repeat(uid.length - 1), uid);
Builder<TranslationsGroup> streamBuilder = Stream.builder();
configDescriptionGroupParameters(configKeyPrefix, configDescription.getParameterGroups())
.forEach(streamBuilder::add);
configDescriptionParameters(configKeyPrefix, configDescription.getParameters()).forEach(streamBuilder::add);
return streamBuilder.build();
}).reduce(Stream::concat).orElseGet(Stream::empty).forEach(groupsBuilder::add);
return Translations.translations(section(groupsBuilder.build()));
}
private TranslationsSection bindingSection(BundleInfo bundleInfo) {
String header = "binding";
BindingInfoXmlResult bindingInfoXml = bundleInfo.getBindingInfoXml();
if (bindingInfoXml == null) {
return section(header);
}
BindingInfo bindingInfo = bindingInfoXml.getBindingInfo();
String bindingId = bundleInfo.getBindingId();
String keyPrefix = String.format("binding.%s", bindingId);
return section(header, group( //
entry(String.format("%s.name", keyPrefix), bindingInfo.getName()),
entry(String.format("%s.description", keyPrefix), bindingInfo.getDescription()) //
));
}
private TranslationsSection bindingConfigSection(BundleInfo bundleInfo) {
String header = "binding config";
BindingInfoXmlResult bindingInfoXml = bundleInfo.getBindingInfoXml();
if (bindingInfoXml == null) {
return section(header);
}
ConfigDescription bindingConfig = bindingInfoXml.getConfigDescription();
if (bindingConfig == null) {
return section(header);
}
String keyPrefix = String.format("binding.config.%s", bundleInfo.getBindingId());
return section(header,
Stream.concat(configDescriptionGroupParameters(keyPrefix, bindingConfig.getParameterGroups()),
configDescriptionParameters(keyPrefix, bindingConfig.getParameters())));
}
private TranslationsSection thingTypesSection(BundleInfo bundleInfo) {
String header = "thing types";
List<ThingTypeXmlResult> thingTypesXml = bundleInfo.getThingTypesXml();
if (thingTypesXml.isEmpty()) {
return section(header);
}
String bindingId = bundleInfo.getBindingId();
String keyPrefix = String.format("thing-type.%s", bindingId);
return section(header, thingTypesXml.stream().map(thingTypeXml -> {
ThingType thingType = thingTypeXml.toThingType();
String thingId = thingType.getUID().getId();
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
entriesBuilder.add(entry(String.format("%s.%s.label", keyPrefix, thingId), thingType.getLabel()));
entriesBuilder
.add(entry(String.format("%s.%s.description", keyPrefix, thingId), thingType.getDescription()));
thingType.getChannelDefinitions().stream() //
.sorted(Comparator.comparing(ChannelDefinition::getId)) //
.forEach(channelDefinition -> {
String label = channelDefinition.getLabel();
if (label != null) {
entriesBuilder.add(entry(String.format("%s.%s.channel.%s.label", keyPrefix, thingId,
channelDefinition.getId()), label));
}
String description = channelDefinition.getDescription();
if (description != null) {
entriesBuilder.add(entry(String.format("%s.%s.channel.%s.description", keyPrefix, thingId,
channelDefinition.getId()), description));
}
});
thingType.getChannelGroupDefinitions().stream() //
.sorted(Comparator.comparing(ChannelGroupDefinition::getId)) //
.forEach(channelGroupDefinition -> {
String label = channelGroupDefinition.getLabel();
if (label != null) {
entriesBuilder.add(entry(String.format("%s.%s.group.%s.label", keyPrefix, thingId,
channelGroupDefinition.getId()), label));
}
String description = channelGroupDefinition.getDescription();
if (description != null) {
entriesBuilder.add(entry(String.format("%s.%s.group.%s.description", keyPrefix, thingId,
channelGroupDefinition.getId()), description));
}
});
return group(entriesBuilder.build());
}));
}
private TranslationsSection thingTypesConfigSection(BundleInfo bundleInfo) {
String header = "thing types config";
List<ThingTypeXmlResult> thingTypesXml = bundleInfo.getThingTypesXml();
if (thingTypesXml.isEmpty()) {
return section(header);
}
Stream<ConfigDescription> configDescriptionStream = Stream.concat( //
thingTypesXml.stream() //
.map(ThingTypeXmlResult::getConfigDescription) //
.filter(Objects::nonNull),
thingTypesXml.stream() //
.map(ThingTypeXmlResult::toThingType) //
.map(ThingType::getConfigDescriptionURI) //
.distinct() //
.map(bundleInfo::getConfigDescription) //
.filter(Optional::isPresent) //
.map(Optional::get));
Builder<TranslationsGroup> groupsBuilder = Stream.builder();
configDescriptionStream.map(configDescription -> {
String configKeyPrefix = String.format("%s.config.%s.%s",
(Object[]) configDescription.getUID().toString().split(":"));
Builder<TranslationsGroup> streamBuilder = Stream.builder();
configDescriptionGroupParameters(configKeyPrefix, configDescription.getParameterGroups())
.forEach(streamBuilder::add);
configDescriptionParameters(configKeyPrefix, configDescription.getParameters()).forEach(streamBuilder::add);
return streamBuilder.build();
}).reduce(Stream::concat).orElseGet(Stream::empty).forEach(groupsBuilder::add);
return section(header, groupsBuilder.build());
}
private TranslationsSection channelGroupTypesSection(BundleInfo bundleInfo) {
String header = "channel group types";
List<ChannelGroupTypeXmlResult> channelGroupTypesXml = bundleInfo.getChannelGroupTypesXml();
if (channelGroupTypesXml.isEmpty()) {
return section(header);
}
String keyPrefix = String.format("channel-group-type.%s", bundleInfo.getBindingId());
return section(header, channelGroupTypesXml.stream().map(ChannelGroupTypeXmlResult::toChannelGroupType)
.map(channelGroupType -> {
String groupTypeKeyPrefix = String.format("%s.%s", keyPrefix,
channelGroupType.getUID().toString().split(":")[1]);
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
entriesBuilder
.add(entry(String.format("%s.label", groupTypeKeyPrefix), channelGroupType.getLabel()));
entriesBuilder.add(entry(String.format("%s.description", groupTypeKeyPrefix),
channelGroupType.getDescription()));
channelGroupType.getChannelDefinitions().stream() //
.sorted(Comparator.comparing(ChannelDefinition::getId)) //
.forEach(channelDefinition -> {
String label = channelDefinition.getLabel();
if (label != null) {
entriesBuilder.add(entry(String.format("%s.channel.%s.label", groupTypeKeyPrefix,
channelDefinition.getId()), label));
}
String description = channelDefinition.getDescription();
if (description != null) {
entriesBuilder.add(entry(String.format("%s.channel.%s.description",
groupTypeKeyPrefix, channelDefinition.getId()), description));
}
});
return group(entriesBuilder.build());
}));
}
private TranslationsSection channelTypesSection(BundleInfo bundleInfo) {
String header = "channel types";
List<ChannelTypeXmlResult> channelTypesXml = bundleInfo.getChannelTypesXml();
if (channelTypesXml.isEmpty()) {
return section(header);
}
String keyPrefix = String.format("channel-type.%s", bundleInfo.getBindingId());
return section(header, channelTypesXml.stream().map(channelTypeXml -> {
ChannelType channelType = channelTypeXml.toChannelType();
String channelId = channelType.getUID().getId();
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
entriesBuilder.add(entry(String.format("%s.%s.label", keyPrefix, channelId), channelType.getLabel()));
entriesBuilder
.add(entry(String.format("%s.%s.description", keyPrefix, channelId), channelType.getDescription()));
StateDescription stateDescription = channelType.getState();
if (stateDescription != null) {
stateDescription.getOptions().stream()
.map(option -> entry(
String.format("%s.%s.state.option.%s", keyPrefix, channelId, option.getValue()),
option.getLabel()))
.forEach(entriesBuilder::add);
if (stateDescription.getPattern() != null) {
String pattern = stateDescription.getPattern();
if (pattern != null && pattern.contains("%1$t")) {
entriesBuilder.add(entry(String.format("%s.%s.state.pattern", keyPrefix, channelId),
stateDescription.getPattern()));
}
}
}
CommandDescription commandDescription = channelType.getCommandDescription();
if (commandDescription != null) {
commandDescription.getCommandOptions().stream()
.map(option -> entry(
String.format("%s.%s.command.option.%s", keyPrefix, channelId, option.getCommand()),
option.getLabel()))
.forEach(entriesBuilder::add);
}
return group(entriesBuilder.build());
}));
}
private TranslationsSection channelTypesConfigSection(BundleInfo bundleInfo) {
String header = "channel types config";
List<ChannelTypeXmlResult> channelTypesXml = bundleInfo.getChannelTypesXml();
if (channelTypesXml.isEmpty()) {
return section(header);
}
Stream<ConfigDescription> configDescriptionStream = Stream.concat(
channelTypesXml.stream().map(ChannelTypeXmlResult::getConfigDescription) //
.filter(Objects::nonNull),
channelTypesXml.stream().map(ChannelTypeXmlResult::toChannelType)
.map(ChannelType::getConfigDescriptionURI) //
.distinct() //
.map(bundleInfo::getConfigDescription) //
.filter(Optional::isPresent) //
.map(Optional::get));
Builder<TranslationsGroup> groupsBuilder = Stream.builder();
configDescriptionStream.map(configDescription -> {
String configKeyPrefix = String.format("%s.config.%s.%s",
(Object[]) configDescription.getUID().toString().split(":"));
Builder<TranslationsGroup> streamBuilder = Stream.builder();
configDescriptionGroupParameters(configKeyPrefix, configDescription.getParameterGroups())
.forEach(streamBuilder::add);
configDescriptionParameters(configKeyPrefix, configDescription.getParameters()).forEach(streamBuilder::add);
return streamBuilder.build();
}).reduce(Stream::concat).orElseGet(Stream::empty).forEach(groupsBuilder::add);
return section(header, groupsBuilder.build());
}
private Stream<TranslationsGroup> configDescriptionGroupParameters(String keyPrefix,
List<ConfigDescriptionParameterGroup> parameterGroups) {
String groupKeyPrefix = String.format("%s.group", keyPrefix);
return parameterGroups.stream()
.map(parameterGroup -> group(
entry(String.format("%s.%s.label", groupKeyPrefix, parameterGroup.getName()),
parameterGroup.getLabel()),
entry(String.format("%s.%s.description", groupKeyPrefix, parameterGroup.getName()),
parameterGroup.getDescription())));
}
private Stream<TranslationsGroup> configDescriptionParameters(String keyPrefix,
List<ConfigDescriptionParameter> parameters) {
return parameters.stream().map(parameter -> {
String parameterKeyPrefix = String.format("%s.%s", keyPrefix, parameter.getName());
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
entriesBuilder.add(entry(String.format("%s.label", parameterKeyPrefix), parameter.getLabel()));
entriesBuilder.add(entry(String.format("%s.description", parameterKeyPrefix), parameter.getDescription()));
parameter.getOptions().stream()
.map(option -> entry(String.format("%s.option.%s", parameterKeyPrefix, option.getValue()),
option.getLabel()))
.forEach(entriesBuilder::add);
return group(entriesBuilder.build());
});
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.binding.xml.internal.BindingInfoXmlResult;
/**
* Tests {@link BundleInfoReader}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class BundleInfoReaderTest {
@Test
public void readBindingInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/acmeweather.bundle/OH-INF"));
BindingInfoXmlResult bindingInfoXml = bundleInfo.getBindingInfoXml();
assertThat(bindingInfoXml, is(notNullValue()));
if (bindingInfoXml != null) {
assertThat(bindingInfoXml.getBindingInfo().getName(), is("ACME Weather Binding"));
assertThat(bindingInfoXml.getBindingInfo().getDescription(),
is("ACME Weather - Current weather and forecasts in your city."));
}
assertThat(bundleInfo.getBindingId(), is("acmeweather"));
assertThat(bundleInfo.getChannelGroupTypesXml().size(), is(1));
assertThat(bundleInfo.getChannelTypesXml().size(), is(2));
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
assertThat(bundleInfo.getThingTypesXml().size(), is(2));
}
@Test
public void readGenericBundleInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/acmetts.bundle/OH-INF"));
assertThat(bundleInfo.getBindingInfoXml(), is(nullValue()));
assertThat(bundleInfo.getBindingId(), is(""));
assertThat(bundleInfo.getChannelGroupTypesXml().size(), is(0));
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
}
@Test
public void readPathWithoutAnyInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/infoless.bundle/OH-INF"));
assertThat(bundleInfo.getBindingInfoXml(), is(nullValue()));
assertThat(bundleInfo.getBindingId(), is(""));
assertThat(bundleInfo.getChannelGroupTypesXml().size(), is(0));
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
assertThat(bundleInfo.getConfigDescriptions().size(), is(0));
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
}
}

View File

@ -0,0 +1,237 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.core.tools.i18n.plugin.DefaultTranslationsGenerationMode.*;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* Tests {@link GenerateDefaultTranslationsMojo}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class GenerateDefaultTranslationsMojoTest {
@TempDir
public @NonNullByDefault({}) Path tempPath;
public @NonNullByDefault({}) Path tempI18nPath;
public static final Path TTS_RESOURCES_PATH = Path.of("src/test/resources/acmetts.bundle");
public static final Path TTS_I18N_RESOURCES_PATH = TTS_RESOURCES_PATH.resolve("OH-INF/i18n");
public static final Path TTS_ALL_PROPERTIES_PATH = TTS_I18N_RESOURCES_PATH.resolve("acmetts.properties");
public static final Path TTS_ALL_DE_PROPERTIES_PATH = TTS_I18N_RESOURCES_PATH.resolve("acmetts_de.properties");
public static final Path TTS_GENERATED_PROPERTIES_PATH = TTS_I18N_RESOURCES_PATH
.resolve("acmetts.generated.properties");
public static final Path TTS_PARTIAL_PROPERTIES_PATH = TTS_I18N_RESOURCES_PATH
.resolve("acmetts.partial.properties");
public static final Path WEATHER_RESOURCES_PATH = Path.of("src/test/resources/acmeweather.bundle");
public static final Path WEATHER_I18N_RESOURCES_PATH = WEATHER_RESOURCES_PATH.resolve("OH-INF/i18n");
public static final Path WEATHER_ALL_PROPERTIES_PATH = WEATHER_I18N_RESOURCES_PATH
.resolve("acmeweather.properties");
public static final Path WEATHER_ALL_DE_PROPERTIES_PATH = WEATHER_I18N_RESOURCES_PATH
.resolve("acmeweather_de.properties");
public static final Path WEATHER_GENERATED_PROPERTIES_PATH = WEATHER_I18N_RESOURCES_PATH
.resolve("acmeweather.generated.properties");
public static final Path WEATHER_PARTIAL_PROPERTIES_PATH = WEATHER_I18N_RESOURCES_PATH
.resolve("acmeweather.partial.properties");
public static final Path INFOLESS_RESOURCES_PATH = Path.of("src/test/resources/infoless.bundle");
private @NonNullByDefault({}) GenerateDefaultTranslationsMojo mojo;
private void copyPath(Path src, Path dest) throws IOException {
try (Stream<Path> stream = Files.walk(src)) {
stream.forEach(source -> copy(source, dest.resolve(src.relativize(source))));
}
}
private void copy(Path source, Path dest) {
try {
Files.copy(source, dest, REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException(e.getMessage(), e);
}
}
private void deleteTempI18nPath() throws IOException {
try (DirectoryStream<Path> entries = Files.newDirectoryStream(tempI18nPath)) {
for (Path entry : entries) {
Files.delete(entry);
}
}
Files.delete(tempI18nPath);
}
@BeforeEach
public void before() {
tempI18nPath = tempPath.resolve("OH-INF/i18n");
mojo = new GenerateDefaultTranslationsMojo();
mojo.setLog(new SystemStreamLog());
mojo.setOhinfDirectory(tempPath.resolve("OH-INF").toFile());
mojo.setTargetDirectory(tempI18nPath.toFile());
}
private void assertSameProperties(Path expectedPath, Path actualPath) throws IOException {
String expected = Files.readString(expectedPath);
String actual = Files.readString(actualPath);
assertThat(expected, equalTo(actual));
}
@Test
public void addMissingBindingTranslationsWithoutI18nPath() throws IOException, MojoFailureException {
copyPath(WEATHER_RESOURCES_PATH, tempPath);
deleteTempI18nPath();
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(WEATHER_GENERATED_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather.properties"));
}
@Test
public void addMissingBindingTranslationsNoChanges() throws IOException, MojoFailureException {
copyPath(WEATHER_RESOURCES_PATH, tempPath);
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(WEATHER_ALL_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather.properties"));
assertSameProperties(WEATHER_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather_de.properties"));
}
@Test
public void addMissingBindingTranslationsToPartialTranslation() throws IOException, MojoFailureException {
copyPath(WEATHER_RESOURCES_PATH, tempPath);
Files.move(tempI18nPath.resolve("acmeweather.partial.properties"),
tempI18nPath.resolve("acmeweather.properties"), REPLACE_EXISTING);
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(WEATHER_ALL_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather.properties"));
assertSameProperties(WEATHER_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather_de.properties"));
}
@Test
public void skipTranslationsBindingTranslationsGeneratingWithExistingTranslations()
throws IOException, MojoFailureException {
copyPath(WEATHER_RESOURCES_PATH, tempPath);
Files.move(tempI18nPath.resolve("acmeweather.partial.properties"),
tempI18nPath.resolve("acmeweather.properties"), REPLACE_EXISTING);
mojo.setGenerationMode(ADD_MISSING_FILES);
mojo.execute();
assertSameProperties(WEATHER_PARTIAL_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather.properties"));
assertSameProperties(WEATHER_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather_de.properties"));
}
@Test
public void regenerateBindingTranslations() throws IOException, MojoFailureException {
copyPath(WEATHER_RESOURCES_PATH, tempPath);
mojo.setGenerationMode(REGENERATE_FILES);
mojo.execute();
assertSameProperties(WEATHER_GENERATED_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather.properties"));
assertSameProperties(WEATHER_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmeweather_de.properties"));
}
@Test
public void addMissingGenericBundleTranslationsWithoutI18nPath() throws IOException, MojoFailureException {
copyPath(TTS_RESOURCES_PATH, tempPath);
deleteTempI18nPath();
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(TTS_GENERATED_PROPERTIES_PATH, tempI18nPath.resolve("acmetts.properties"));
}
@Test
public void addMissingGenericBundleTranslationsNoChanges() throws IOException, MojoFailureException {
copyPath(TTS_RESOURCES_PATH, tempPath);
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(TTS_ALL_PROPERTIES_PATH, tempI18nPath.resolve("acmetts.properties"));
assertSameProperties(TTS_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmetts_de.properties"));
}
@Test
public void addMissingGenericBundleTranslationsToPartialTranslation() throws IOException, MojoFailureException {
copyPath(TTS_RESOURCES_PATH, tempPath);
Files.move(tempI18nPath.resolve("acmetts.partial.properties"), tempI18nPath.resolve("acmetts.properties"),
REPLACE_EXISTING);
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
assertSameProperties(TTS_ALL_PROPERTIES_PATH, tempI18nPath.resolve("acmetts.properties"));
assertSameProperties(TTS_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmetts_de.properties"));
}
@Test
public void skipTranslationsGenericBundleTranslationsGeneratingWithExistingTranslations()
throws IOException, MojoFailureException {
copyPath(TTS_RESOURCES_PATH, tempPath);
Files.move(tempI18nPath.resolve("acmetts.partial.properties"), tempI18nPath.resolve("acmetts.properties"),
REPLACE_EXISTING);
mojo.setGenerationMode(ADD_MISSING_FILES);
mojo.execute();
assertSameProperties(TTS_PARTIAL_PROPERTIES_PATH, tempI18nPath.resolve("acmetts.properties"));
assertSameProperties(TTS_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmetts_de.properties"));
}
@Test
public void regenerateGenericBundleTranslations() throws IOException, MojoFailureException {
copyPath(TTS_RESOURCES_PATH, tempPath);
mojo.setGenerationMode(REGENERATE_FILES);
mojo.execute();
assertSameProperties(TTS_GENERATED_PROPERTIES_PATH, tempI18nPath.resolve("acmetts.properties"));
assertSameProperties(TTS_ALL_DE_PROPERTIES_PATH, tempI18nPath.resolve("acmetts_de.properties"));
}
@Test
public void addMissingTranslationsWithoutOhInfPath() throws IOException, MojoFailureException {
copyPath(INFOLESS_RESOURCES_PATH, tempPath);
mojo.setGenerationMode(ADD_MISSING_TRANSLATIONS);
mojo.execute();
}
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyString;
import java.nio.file.Path;
import java.util.stream.Collectors;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests {@link PropertiesToTranslationsConverter}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PropertiesToTranslationsConverterTest {
@Test
public void readBindingInfo() {
PropertiesToTranslationsConverter converter = new PropertiesToTranslationsConverter(new SystemStreamLog());
Translations translations = converter
.convert(Path.of("src/test/resources/acmeweather.bundle/OH-INF/i18n/acmeweather.properties"));
assertThat(translations.hasTranslations(), is(true));
assertThat(translations.sections.size(), is(6));
assertThat(translations.keysStream().count(), is(31L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, containsString("# binding"));
assertThat(lines, containsString("binding.acmeweather.name = ACME Weather Binding"));
assertThat(lines, containsString(
"binding.acmeweather.description = ACME Weather - Current weather and forecasts in your city."));
assertThat(lines, containsString(
"channel-group-type.acmeweather.forecast.channel.minTemperature.description = Minimum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial)."));
assertThat(lines, containsString(
"channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"));
assertThat(lines, containsString("CUSTOM_KEY = Provides various weather data from the ACME weather service"));
}
@Test
public void readGenericBundleInfo() {
PropertiesToTranslationsConverter converter = new PropertiesToTranslationsConverter(new SystemStreamLog());
Translations translations = converter
.convert(Path.of("src/test/resources/acmetts.bundle/OH-INF/i18n/acmetts.properties"));
assertThat(translations.hasTranslations(), is(true));
assertThat(translations.sections.size(), is(2));
assertThat(translations.keysStream().count(), is(19L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, containsString("voice.config.acmetts.clientId.label = Client Id"));
assertThat(lines,
containsString("voice.config.acmetts.clientId.description = ACME Platform OAuth 2.0-Client Id."));
assertThat(lines, containsString("voice.config.acmetts.pitch.label = Pitch"));
assertThat(lines, containsString(
"voice.config.acmetts.pitch.description = Customize the pitch of your selected voice, up to 20 semitones more or less than the default output."));
}
@Test
public void readPathWithoutAnyInfo() {
PropertiesToTranslationsConverter converter = new PropertiesToTranslationsConverter(new SystemStreamLog());
Translations translations = converter
.convert(Path.of("src/test/resources/infoless.bundle/OH-INF/i18n/nonexisting.properties"));
assertThat(translations.hasTranslations(), is(false));
assertThat(translations.keysStream().count(), is(0L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, is(emptyString()));
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry.entry;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup.group;
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection.section;
import static org.openhab.core.tools.i18n.plugin.Translations.translations;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests {@link TranslationsMerger}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class TranslationsMergerTest {
@Test
public void mergeEmptyTranslations() {
Translations mainTranslations = translations();
Translations missingTranslations = translations();
TranslationsMerger merger = new TranslationsMerger();
merger.merge(mainTranslations, missingTranslations);
assertThat(mainTranslations.hasTranslations(), is(false));
assertThat(mainTranslations.keysStream().count(), is(0L));
assertThat(missingTranslations.hasTranslations(), is(false));
assertThat(missingTranslations.keysStream().count(), is(0L));
}
@Test
public void mergeDifferentTranslations() {
Translations mainTranslations = Translations.translations( //
section("main section 1", group( //
entry("key1", "mainValue1"), //
entry("key2", "mainValue2"))),
section("main section 2", group( //
entry("key3", "mainValue3"), //
entry("key4", "mainValue4"))));
Translations missingTranslations = Translations.translations( //
section("missing section 1", group( //
entry("key1", "missingValue1"), //
entry("key2", "missingValue2"))),
section("missing section 3", group( //
entry("key5", "missingValue5"), //
entry("key6", "missingValue6"))));
TranslationsMerger merger = new TranslationsMerger();
merger.merge(mainTranslations, missingTranslations);
assertThat(mainTranslations.hasTranslations(), is(true));
assertThat(mainTranslations.keysStream().count(), is(6L));
assertThat(mainTranslations.sections.size(), is(3));
String lines = mainTranslations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, containsString("# main section 1"));
assertThat(lines, containsString("key1 = mainValue1"));
assertThat(lines, containsString("key2 = mainValue2"));
assertThat(lines, containsString("# main section 2"));
assertThat(lines, containsString("key3 = mainValue3"));
assertThat(lines, containsString("key4 = mainValue4"));
assertThat(lines, containsString("# missing section 3"));
assertThat(lines, containsString("key5 = missingValue5"));
assertThat(lines, containsString("key6 = missingValue6"));
}
}

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2021 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.core.tools.i18n.plugin;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyString;
import java.io.IOException;
import java.nio.file.Path;
import java.util.stream.Collectors;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Tests {@link XmlToTranslationsConverter}.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class XmlToTranslationsConverterTest {
@Test
public void convertBindingInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/acmeweather.bundle/OH-INF"));
XmlToTranslationsConverter converter = new XmlToTranslationsConverter();
Translations translations = converter.convert(bundleInfo);
assertThat(translations.hasTranslations(), is(true));
assertThat(translations.sections.size(), is(7));
assertThat(translations.keysStream().count(), is(30L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, containsString("# binding"));
assertThat(lines, containsString("binding.acmeweather.name = ACME Weather Binding"));
assertThat(lines, containsString(
"binding.acmeweather.description = ACME Weather - Current weather and forecasts in your city."));
assertThat(lines, containsString(
"channel-group-type.acmeweather.forecast.channel.minTemperature.description = Minimum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial)."));
assertThat(lines, containsString(
"channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"));
}
@Test
public void convertGenericBundleInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/acmetts.bundle/OH-INF"));
XmlToTranslationsConverter converter = new XmlToTranslationsConverter();
Translations translations = converter.convert(bundleInfo);
assertThat(translations.hasTranslations(), is(true));
assertThat(translations.sections.size(), is(1));
assertThat(translations.keysStream().count(), is(18L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, containsString("voice.config.acmetts.clientId.label = Client Id"));
assertThat(lines,
containsString("voice.config.acmetts.clientId.description = ACME Platform OAuth 2.0-Client Id."));
assertThat(lines, containsString("voice.config.acmetts.pitch.label = Pitch"));
assertThat(lines, containsString(
"voice.config.acmetts.pitch.description = Customize the pitch of your selected voice, up to 20 semitones more or less than the default output."));
}
@Test
public void convertPathWithoutAnyInfo() throws IOException {
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/infoless.bundle/OH-INF"));
XmlToTranslationsConverter converter = new XmlToTranslationsConverter();
Translations translations = converter.convert(bundleInfo);
assertThat(translations.hasTranslations(), is(false));
assertThat(translations.keysStream().count(), is(0L));
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
assertThat(lines, is(emptyString()));
}
}

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="voice:acmetts">
<parameter-group name="authentication">
<label>Authentication</label>
<description>Authentication for connecting to ACME Platform.</description>
</parameter-group>
<parameter-group name="tts">
<label>TTS Configuration</label>
<description>Parameters for ACME TTS API.</description>
</parameter-group>
<parameter name="clientId" type="text" required="true" groupName="authentication">
<label>Client Id</label>
<description>ACME Platform OAuth 2.0-Client Id.</description>
</parameter>
<parameter name="clientSecret" type="text" required="true" groupName="authentication">
<context>password</context>
<label>Client Secret</label>
<description>ACME Platform OAuth 2.0-Client Secret.</description>
</parameter>
<parameter name="authcode" type="text" groupName="authentication">
<label>Authorization Code</label>
<description><![CDATA[The auth-code is a one-time code needed to retrieve the necessary access-codes from ACME Platform. <b>Please go to your browser ...</b> https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code <b>... to generate an auth-code and paste it here</b>.]]></description>
</parameter>
<parameter name="pitch" type="decimal" min="-20" max="20" step="0.1" groupName="tts">
<label>Pitch</label>
<description>Customize the pitch of your selected voice, up to 20 semitones more or less than the default output.</description>
<default>0</default>
</parameter>
<parameter name="volumeGain" type="decimal" min="-96" max="16" groupName="tts">
<label>Volume Gain</label>
<description>Increase the volume of the output by up to 16db or decrease the volume up to -96db.</description>
<default>0</default>
</parameter>
<parameter name="speakingRate" type="decimal" min="0.25" max="4" groupName="tts">
<label>Speaking Rate</label>
<description>Speaking rate can be 4x faster or slower than the normal rate.</description>
<default>1</default>
</parameter>
<parameter name="purgeCache" type="boolean">
<advanced>true</advanced>
<label>Purge Cache</label>
<description>Purges the cache e.g. after testing different voice configuration parameters. When enabled the cache is
purged once. Make sure to disable this setting again so the cache is maintained after restarts.</description>
<default>false</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,18 @@
voice.config.acmetts.authcode.label = Authorization Code
voice.config.acmetts.authcode.description = The auth-code is a one-time code needed to retrieve the necessary access-codes from ACME Platform. <b>Please go to your browser ...</b> https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code <b>... to generate an auth-code and paste it here</b>.
voice.config.acmetts.clientId.label = Client Id
voice.config.acmetts.clientId.description = ACME Platform OAuth 2.0-Client Id.
voice.config.acmetts.clientSecret.label = Client Secret
voice.config.acmetts.clientSecret.description = ACME Platform OAuth 2.0-Client Secret.
voice.config.acmetts.group.authentication.label = Authentication
voice.config.acmetts.group.authentication.description = Authentication for connecting to ACME Platform.
voice.config.acmetts.group.tts.label = TTS Configuration
voice.config.acmetts.group.tts.description = Parameters for ACME TTS API.
voice.config.acmetts.pitch.label = Pitch
voice.config.acmetts.pitch.description = Customize the pitch of your selected voice, up to 20 semitones more or less than the default output.
voice.config.acmetts.purgeCache.label = Purge Cache
voice.config.acmetts.purgeCache.description = Purges the cache e.g. after testing different voice configuration parameters. When enabled the cache is purged once. Make sure to disable this setting again so the cache is maintained after restarts.
voice.config.acmetts.speakingRate.label = Speaking Rate
voice.config.acmetts.speakingRate.description = Speaking rate can be 4x faster or slower than the normal rate.
voice.config.acmetts.volumeGain.label = Volume Gain
voice.config.acmetts.volumeGain.description = Increase the volume of the output by up to 16db or decrease the volume up to -96db.

View File

@ -0,0 +1,3 @@
# custom
CUSTOM_KEY = Could not connect to the ACME TTS API

View File

@ -0,0 +1,22 @@
voice.config.acmetts.authcode.label = Authorization Code
voice.config.acmetts.authcode.description = The auth-code is a one-time code needed to retrieve the necessary access-codes from ACME Platform. <b>Please go to your browser ...</b> https://accounts.google.com/o/oauth2/auth?client_id={{clientId}}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/cloud-platform&response_type=code <b>... to generate an auth-code and paste it here</b>.
voice.config.acmetts.clientId.label = Client Id
voice.config.acmetts.clientId.description = ACME Platform OAuth 2.0-Client Id.
voice.config.acmetts.clientSecret.label = Client Secret
voice.config.acmetts.clientSecret.description = ACME Platform OAuth 2.0-Client Secret.
voice.config.acmetts.group.authentication.label = Authentication
voice.config.acmetts.group.authentication.description = Authentication for connecting to ACME Platform.
voice.config.acmetts.group.tts.label = TTS Configuration
voice.config.acmetts.group.tts.description = Parameters for ACME TTS API.
voice.config.acmetts.pitch.label = Pitch
voice.config.acmetts.pitch.description = Customize the pitch of your selected voice, up to 20 semitones more or less than the default output.
voice.config.acmetts.purgeCache.label = Purge Cache
voice.config.acmetts.purgeCache.description = Purges the cache e.g. after testing different voice configuration parameters. When enabled the cache is purged once. Make sure to disable this setting again so the cache is maintained after restarts.
voice.config.acmetts.speakingRate.label = Speaking Rate
voice.config.acmetts.speakingRate.description = Speaking rate can be 4x faster or slower than the normal rate.
voice.config.acmetts.volumeGain.label = Volume Gain
voice.config.acmetts.volumeGain.description = Increase the volume of the output by up to 16db or decrease the volume up to -96db.
# custom
CUSTOM_KEY = Could not connect to the ACME TTS API

View File

@ -0,0 +1,22 @@
voice.config.acmetts.authcode.label = Autorisierungscode
voice.config.acmetts.authcode.description = Der Autorisierungscode ist ein einmaliger Code, der benötigt wird, um den notwendigen Authentifizierungscode von der ACME Plattform abzurufen. <b>Aufruf der URL ...</b> https\://accounts.google.com/o/oauth2/auth?client_id\={{clientId}}&redirect_uri\=urn\:ietf\:wg\:oauth\:2.0\:oob&scope\=https\://www.googleapis.com/auth/cloud-platform&response_type\=code <b>im Browser, um einen Autorisierungscode zu generieren und ihn hier einzufügen<b>.
voice.config.acmetts.clientId.label = Client Id
voice.config.acmetts.clientId.description = ACME Plattform OAuth 2.0-Client Id.
voice.config.acmetts.clientSecret.label = Client Secret
voice.config.acmetts.clientSecret.description = ACME Plattform OAuth 2.0-Client Secret.
voice.config.acmetts.group.authentication.label = Authentifizierungscode
voice.config.acmetts.group.authentication.description = Authentifizierungscode für die Verbindung zur ACME Plattform.
voice.config.acmetts.group.tts.label = Sprachkonfiguration
voice.config.acmetts.group.tts.description = Sprachkonfiguration für die ACME TTS API.
voice.config.acmetts.pitch.label = Tonhöhe
voice.config.acmetts.pitch.description = Die Tonhöhe der gewählten Stimme kann bis zu 20 Halbtöne höher oder niedriger sein als der Standardwert.
voice.config.acmetts.purgeCache.label = Cache Leeren
voice.config.acmetts.purgeCache.description = Leert den Cache z.B. nach dem Testen verschiedener Sprachkonfigurationen. Wenn aktiviert, wird der Cache einmalig gelöscht. Stellen Sie sicher, dass Sie diese Einstellung im Anschluss deaktivieren wird, so dass der Cache nach einem Neustart genutzt wird.
voice.config.acmetts.speakingRate.label = Sprachgeschwindigkeit
voice.config.acmetts.speakingRate.description = Die Sprachgeschwindigkeit kann bis zu 4x schneller oder langsamer sein als die normale Geschwindigkeit.
voice.config.acmetts.volumeGain.label = Lautstärke Verstärkung
voice.config.acmetts.volumeGain.description = Die Lautstärke der Sprachausgabe kann um bis zu 16db erhöht oder um bis zu -96db verringert werden.
# custom
CUSTOM_KEY = Keine Verbindung mit ACME TTS API möglich

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="acmeweather" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>ACME Weather Binding</name>
<description>ACME Weather - Current weather and forecasts in your city.</description>
</binding:binding>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:acmeweather:weather">
<parameter name="refreshInterval" type="integer" min="1" unit="min">
<label>Refresh Interval</label>
<description>Specifies the refresh interval (in minutes).</description>
<default>60</default>
</parameter>
<parameter name="language" type="text">
<label>Language</label>
<description>Language to be used by the ACME API.</description>
<options>
<option value="nl">Dutch</option>
<option value="de">German</option>
<option value="en">English</option>
<option value="fr">French</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,44 @@
# binding
binding.acmeweather.name = ACME Weather Binding
binding.acmeweather.description = ACME Weather - Current weather and forecasts in your city.
# thing types
thing-type.acmeweather.weather-with-group.label = Weather Information with Group
thing-type.acmeweather.weather-with-group.group.forecastToday.label = Today
thing-type.acmeweather.weather-with-group.group.forecastToday.description = This is the weather forecast for today.
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.label = Weather Forecast Tomorrow
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.description = This is the weather forecast for tomorrow.
thing-type.acmeweather.weather.label = Weather Information *
thing-type.acmeweather.weather.channel.minTemperature.label = Min. Temperature
thing-type.acmeweather.weather.channel.minTemperature.description = Minimum temperature in degrees Celsius (metric) or Fahrenheit (imperial).
# thing types config
thing-type.config.acmeweather.weather.language.label = Language
thing-type.config.acmeweather.weather.language.description = Language to be used by the ACME API.
thing-type.config.acmeweather.weather.language.option.nl = Dutch
thing-type.config.acmeweather.weather.language.option.de = German
thing-type.config.acmeweather.weather.language.option.en = English
thing-type.config.acmeweather.weather.language.option.fr = French
thing-type.config.acmeweather.weather.refreshInterval.label = Refresh Interval
thing-type.config.acmeweather.weather.refreshInterval.description = Specifies the refresh interval (in minutes).
# channel group types
channel-group-type.acmeweather.forecast.label = Weather information group
channel-group-type.acmeweather.forecast.description = Weather information group description.
channel-group-type.acmeweather.forecast.channel.maxTemperature.label = Max. Temperature
channel-group-type.acmeweather.forecast.channel.maxTemperature.description = Maximum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).
channel-group-type.acmeweather.forecast.channel.minTemperature.label = Min. Temperature
channel-group-type.acmeweather.forecast.channel.minTemperature.description = Minimum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).
# channel types
channel-type.acmeweather.temperature.label = Temperature
channel-type.acmeweather.temperature.description = Temperature in degrees Celsius (metric) or Fahrenheit (imperial).
channel-type.acmeweather.temperature.state.option.VALUE = My label
channel-type.acmeweather.time-stamp.label = Observation Time
channel-type.acmeweather.time-stamp.description = Time of data observation.
channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS

View File

@ -0,0 +1,3 @@
# custom
CUSTOM_KEY = Provides various weather data from the ACME weather service

View File

@ -0,0 +1,48 @@
# binding
binding.acmeweather.name = ACME Weather Binding
binding.acmeweather.description = ACME Weather - Current weather and forecasts in your city.
# thing types
thing-type.acmeweather.weather-with-group.label = Weather Information with Group
thing-type.acmeweather.weather-with-group.group.forecastToday.label = Today
thing-type.acmeweather.weather-with-group.group.forecastToday.description = This is the weather forecast for today.
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.label = Weather Forecast Tomorrow
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.description = This is the weather forecast for tomorrow.
thing-type.acmeweather.weather.label = Weather Information *
thing-type.acmeweather.weather.channel.minTemperature.label = Min. Temperature
thing-type.acmeweather.weather.channel.minTemperature.description = Minimum temperature in degrees Celsius (metric) or Fahrenheit (imperial).
# thing types config
thing-type.config.acmeweather.weather.language.label = Language
thing-type.config.acmeweather.weather.language.description = Language to be used by the ACME API.
thing-type.config.acmeweather.weather.language.option.nl = Dutch
thing-type.config.acmeweather.weather.language.option.de = German
thing-type.config.acmeweather.weather.language.option.en = English
thing-type.config.acmeweather.weather.language.option.fr = French
thing-type.config.acmeweather.weather.refreshInterval.label = Refresh Interval
thing-type.config.acmeweather.weather.refreshInterval.description = Specifies the refresh interval (in minutes).
# channel group types
channel-group-type.acmeweather.forecast.label = Weather information group
channel-group-type.acmeweather.forecast.description = Weather information group description.
channel-group-type.acmeweather.forecast.channel.maxTemperature.label = Max. Temperature
channel-group-type.acmeweather.forecast.channel.maxTemperature.description = Maximum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).
channel-group-type.acmeweather.forecast.channel.minTemperature.label = Min. Temperature
channel-group-type.acmeweather.forecast.channel.minTemperature.description = Minimum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).
# channel types
channel-type.acmeweather.temperature.label = Temperature
channel-type.acmeweather.temperature.description = Temperature in degrees Celsius (metric) or Fahrenheit (imperial).
channel-type.acmeweather.temperature.state.option.VALUE = My label
channel-type.acmeweather.time-stamp.label = Observation Time
channel-type.acmeweather.time-stamp.description = Time of data observation.
channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
# custom
CUSTOM_KEY = Provides various weather data from the ACME weather service

View File

@ -0,0 +1,48 @@
# binding
binding.acmeweather.name = ACME Wetter Binding
binding.acmeweather.description = ACME Wetter - Aktuelles Wetter und Prognosen in Ihrer Stadt.
# thing types
thing-type.acmeweather.weather-with-group.label = Wetterinformationen mit Gruppe
thing-type.acmeweather.weather-with-group.group.forecastToday.label = Wettervorhersage heute
thing-type.acmeweather.weather-with-group.group.forecastToday.description = Wettervorhersage für den heutigen Tag.
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.label = Wettervorhersage morgen
thing-type.acmeweather.weather-with-group.group.forecastTomorrow.description = Wettervorhersage für den morgigen Tag.
thing-type.acmeweather.weather.label = Wetterinformation
thing-type.acmeweather.weather.channel.minTemperature.label = Min. Temperatur
thing-type.acmeweather.weather.channel.minTemperature.description = Minimale Temperatur in Grad Celsius (Metrisch) oder Fahrenheit (Imperial).
# thing types config
thing-type.config.acmeweather.weather.language.label = Sprache
thing-type.config.acmeweather.weather.language.description = Sprache zur Anzeige der Daten.
thing-type.config.acmeweather.weather.language.option.nl = Holländisch
thing-type.config.acmeweather.weather.language.option.de = Deutsch
thing-type.config.acmeweather.weather.language.option.en = Englisch
thing-type.config.acmeweather.weather.language.option.fr = Französisch
thing-type.config.acmeweather.weather.refreshInterval.label = Abfrageintervall
thing-type.config.acmeweather.weather.refreshInterval.description = Intervall zur Abfrage der OpenWeatherMap API (in min).
# channel group types
channel-group-type.acmeweather.forecast.label = Wetterinformation mit Gruppe
channel-group-type.acmeweather.forecast.description = Wetterinformation mit Gruppe Beschreibung.
channel-group-type.acmeweather.forecast.channel.maxTemperature.label = Max. Temperatur
channel-group-type.acmeweather.forecast.channel.maxTemperature.description = Maximale vorhergesagte Temperatur in Grad Celsius (Metrisch) oder Fahrenheit (Imperial).
channel-group-type.acmeweather.forecast.channel.minTemperature.label = Min. Temperatur
channel-group-type.acmeweather.forecast.channel.minTemperature.description = Minimale vorhergesagte Temperatur in Grad Celsius (Metrisch) oder Fahrenheit (Imperial).
# channel types
channel-type.acmeweather.temperature.label = Temperatur
channel-type.acmeweather.temperature.description = Temperatur in Grad Celsius (Metrisch) oder Fahrenheit (Imperial).
channel-type.acmeweather.temperature.state.option.VALUE = Mein String
channel-type.acmeweather.time-stamp.label = Letzte Messung
channel-type.acmeweather.time-stamp.description = Zeigt den Zeitpunkt der letzten Messung an.
channel-type.acmeweather.time-stamp.state.pattern = %1$td.%1$tm.%1$tY %1$tH:%1$tM:%1$tS
# custom
CUSTOM_KEY = Stellt verschiedene Wetterdaten vom ACME Wetterdienst bereit

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="acmeweather"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Channel Group -->
<channel-group-type id="forecast">
<label>Weather information group</label>
<description>Weather information group description.</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="minTemperature" typeId="temperature">
<label>Min. Temperature</label>
<description>Minimum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).</description>
</channel>
<channel id="maxTemperature" typeId="temperature">
<label>Max. Temperature</label>
<description>Maximum forecasted temperature in degrees Celsius (metric) or Fahrenheit (imperial).</description>
</channel>
</channels>
</channel-group-type>
<!-- Channel -->
<channel-type id="temperature">
<item-type>Number</item-type>
<label>Temperature</label>
<description>Temperature in degrees Celsius (metric) or Fahrenheit (imperial).</description>
<state pattern="%d degree Celsius">
<options>
<option value="VALUE">My label</option>
</options>
</state>
</channel-type>
<channel-type id="time-stamp">
<item-type>DateTime</item-type>
<label>Observation Time</label>
<description>Time of data observation.</description>
<category>Time</category>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="acmeweather"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- ACME Weather Binding -->
<thing-type id="weather">
<label>Weather Information *</label>
<description>@text/CUSTOM_KEY</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="minTemperature" typeId="temperature">
<label>Min. Temperature</label>
<description>Minimum temperature in degrees Celsius (metric) or Fahrenheit (imperial).</description>
</channel>
<channel id="time-stamp" typeId="time-stamp"/>
</channels>
<config-description-ref uri="thing-type:acmeweather:weather"/>
</thing-type>
<!-- ACME Weather Binding with group -->
<thing-type id="weather-with-group">
<label>Weather Information with Group</label>
<channel-groups>
<channel-group id="forecastToday" typeId="forecast">
<label>Today</label>
<description>This is the weather forecast for today.</description>
</channel-group>
<channel-group id="forecastTomorrow" typeId="forecast">
<label>Weather Forecast Tomorrow</label>
<description>This is the weather forecast for tomorrow.</description>
</channel-group>
</channel-groups>
</thing-type>
</thing:thing-descriptions>

View File

@ -18,6 +18,7 @@
<modules>
<module>archetype</module>
<module>i18n-plugin</module>
</modules>
</project>