mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
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:
parent
8c1fe60abc
commit
bb3224a434
38
tools/i18n-plugin/.classpath
Normal file
38
tools/i18n-plugin/.classpath
Normal 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>
|
23
tools/i18n-plugin/.project
Normal file
23
tools/i18n-plugin/.project
Normal 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
111
tools/i18n-plugin/pom.xml
Normal 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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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>
|
@ -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.
|
@ -0,0 +1,3 @@
|
||||
# custom
|
||||
|
||||
CUSTOM_KEY = Could not connect to the ACME TTS API
|
@ -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
|
@ -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
|
@ -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>
|
@ -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>
|
@ -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
|
@ -0,0 +1,3 @@
|
||||
# custom
|
||||
|
||||
CUSTOM_KEY = Provides various weather data from the ACME weather service
|
@ -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
|
@ -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
|
@ -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>
|
@ -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>
|
@ -18,6 +18,7 @@
|
||||
|
||||
<modules>
|
||||
<module>archetype</module>
|
||||
<module>i18n-plugin</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
Loading…
Reference in New Issue
Block a user