CCM NG: Lokalisierung Themes über ResourceBundle (#2795)

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5262 8810af33-2d31-482b-a856-94f89814c4df

Former-commit-id: ee857ec4c8
pull/2/head
jensp 2018-02-08 18:33:05 +00:00
parent db40133722
commit e15e952b21
5 changed files with 245 additions and 2 deletions

View File

@ -53,6 +53,7 @@ import net.sf.saxon.value.SequenceType;
import net.sf.saxon.value.StringValue; import net.sf.saxon.value.StringValue;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.theming.ProcessesThemes; import org.libreccm.theming.ProcessesThemes;
import org.libreccm.theming.Themes; import org.libreccm.theming.Themes;
import org.libreccm.theming.manifest.ThemeTemplate; import org.libreccm.theming.manifest.ThemeTemplate;
@ -67,7 +68,13 @@ import java.io.Reader;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import javax.inject.Inject; import javax.inject.Inject;
import javax.xml.transform.ErrorListener; import javax.xml.transform.ErrorListener;
@ -97,6 +104,9 @@ public class XsltThemeProcessor implements ThemeProcessor {
private static final Logger LOGGER = LogManager private static final Logger LOGGER = LogManager
.getLogger(XsltThemeProcessor.class); .getLogger(XsltThemeProcessor.class);
@Inject
private GlobalizationHelper globalizationHelper;
@Inject @Inject
private SettingsUtils settingsUtils; private SettingsUtils settingsUtils;
@ -137,7 +147,7 @@ public class XsltThemeProcessor implements ThemeProcessor {
} catch (ParserConfigurationException ex) { } catch (ParserConfigurationException ex) {
throw new UnexpectedErrorException(ex); throw new UnexpectedErrorException(ex);
} }
final Document document; final Document document;
try { try {
final InputStream xmlBytesStream = new ByteArrayInputStream( final InputStream xmlBytesStream = new ByteArrayInputStream(
@ -213,6 +223,9 @@ public class XsltThemeProcessor implements ThemeProcessor {
configuration configuration
.registerExtensionFunction( .registerExtensionFunction(
new GetSettingFunctionDefinition(theme, themeProvider)); new GetSettingFunctionDefinition(theme, themeProvider));
configuration
.registerExtensionFunction(
new LocalizeFunctionDefinition(theme, themeProvider));
configuration configuration
.registerExtensionFunction(new TruncateTextFunctionDefinition()); .registerExtensionFunction(new TruncateTextFunctionDefinition());
final Transformer transformer; final Transformer transformer;
@ -389,6 +402,173 @@ public class XsltThemeProcessor implements ThemeProcessor {
} }
/**
* Definition for XSL function for localising texts in the theme. Allows use
* of {@link ResourceBundle}s instead of a custom XML format which was used
* in legacy versions.
*
* The XSL function {@code ccm:localize} expects one mandatory parameter,
* the {@code key} of the text to localise. The optional second parameter
* {@code bundle} identifies the {@link ResourceBundle} to use. The
* {@code bundle} parameter is passed to
* {@link ResourceBundle#getBundle(java.lang.String)}. All bundle paths are
* resolved relative to the root of the theme using {@link ThemeProvider}.
* If the {@code bundle} parameter is omitted the function will look for
* {@link PropertyResourceBundle} named {@code theme-bundle.properties} in
* the root of the theme. Examples:
*
* {@code <xsl:value-of select="ccm:localize('footer.privacy')" />}
*
* In this case this function will load the file
* {@code theme-bundle.properties} from the root of the theme and use it to
* create an instance of {@link PropertyResourceBundle}. If this is
* successful the key {@code footer.privacy} is lookup in the resource
* bundle.
*
* {@code <xsl:value-of select="ccm:localize('footer.privacy', '/texts/footer')" />}
*
* In this case the function tries find a property file called
* {@code footer.properties} in the texts directory in the theme.
*
* Of course the function, or better {@link ResourceBundle} will also take
* into account the current locale, therefore in both examples the first
* file name will be {@code footer_$locale.properties} where {@code $locale}
* is the the locale returned by
* {@link GlobalizationHelper#getNegotiatedLocale()}.
*/
private class LocalizeFunctionDefinition
extends ExtensionFunctionDefinition {
private final ThemeInfo theme;
private final ThemeProvider themeProvider;
public LocalizeFunctionDefinition(final ThemeInfo theme,
final ThemeProvider themeProvider) {
super();
this.theme = theme;
this.themeProvider = themeProvider;
}
@Override
public StructuredQName getFunctionQName() {
return new StructuredQName(FUNCTION_XMLNS_PREFIX,
FUNCTION_XMLNS,
"localize");
}
@Override
public SequenceType[] getArgumentTypes() {
return new SequenceType[]{SequenceType.SINGLE_STRING};
}
@Override
public int getMaximumNumberOfArguments() {
return 2;
}
@Override
public SequenceType getResultType(final SequenceType[] arguments) {
return SequenceType.SINGLE_STRING;
}
@Override
public ExtensionFunctionCall makeCallExpression() {
return new ExtensionFunctionCall() {
@Override
public Sequence call(final XPathContext xPathContext,
final Sequence[] arguments)
throws XPathException {
final String bundle;
if (arguments.length > 1) {
bundle = ((Item) arguments[1]).getStringValue();
} else {
bundle = "theme-bundle";
}
final String key = ((Item) arguments[0]).getStringValue();
LOGGER.debug("Localizing key \"{}\" from bundle \"{}\"...",
key,
bundle);
final ResourceBundle resourceBundle = ResourceBundle
.getBundle(
bundle,
globalizationHelper.getNegotiatedLocale(),
new LocalizedResourceBundleControl(theme,
themeProvider));
return StringValue
.makeStringValue(resourceBundle.getString(key));
}
};
}
}
private class LocalizedResourceBundleControl
extends ResourceBundle.Control {
private final ThemeInfo theme;
private final ThemeProvider themeProvider;
public LocalizedResourceBundleControl(
final ThemeInfo theme,
final ThemeProvider themeProvider) {
this.theme = theme;
this.themeProvider = themeProvider;
}
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Arrays.asList("java.properties");
}
@Override
public ResourceBundle newBundle(final String baseName,
final Locale locale,
final String format,
final ClassLoader classLoader,
final boolean reload)
throws IllegalAccessException,
InstantiationException,
IOException {
if ("java.properties".equals(format)) {
final String bundleName = toBundleName(baseName, locale);
final Optional<InputStream> inputStream = themeProvider
.getThemeFileAsStream(theme.getName(),
theme.getVersion(),
String.format("%s.properties",
bundleName));
if (inputStream.isPresent()) {
return new PropertyResourceBundle(inputStream.get());
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
}
}
private class TruncateTextFunctionDefinition private class TruncateTextFunctionDefinition
extends ExtensionFunctionDefinition { extends ExtensionFunctionDefinition {

View File

@ -52,6 +52,8 @@
</xsl:for-each> </xsl:for-each>
</div> </div>
<!--<xsl:apply-templates select="greetingItem" />--> <!--<xsl:apply-templates select="greetingItem" />-->
<xsl:call-template name="themeFunctionsExamples" />
</main> </main>
<xsl:call-template name="footer" /> <xsl:call-template name="footer" />
<!--<footer> <!--<footer>
@ -81,6 +83,9 @@
</p> </p>
<xsl:value-of disable-output-escaping="true" select="./text" /> <xsl:value-of disable-output-escaping="true" select="./text" />
</xsl:template>
<xsl:template name="themeFunctionsExamples">
<h2>Example of Theme Utils</h2> <h2>Example of Theme Utils</h2>
<dl> <dl>
<dt> <dt>
@ -107,8 +112,31 @@
<xsl:value-of select="ccm:truncateText('0123456789 123456789 123456789', 20)" /> <xsl:value-of select="ccm:truncateText('0123456789 123456789 123456789', 20)" />
</code> </code>
</dd> </dd>
<dt>
<code>localized('label.critical')</code>
</dt>
<dd>
<xsl:value-of select="ccm:localize('label.critical', 'texts/labels')" />
</dd>
<dt>
<code>localized('label.error')</code>
</dt>
<dd>
<xsl:value-of select="ccm:localize('label.error', 'texts/labels')" />
</dd>
<dt>
<code>localized('label.ok')</code>
</dt>
<dd>
<xsl:value-of select="ccm:localize('label.ok', 'texts/labels')" />
</dd>
<dt>
<code>localized('label.warning')</code>
</dt>
<dd>
<xsl:value-of select="ccm:localize('label.warning', 'texts/labels')" />
</dd>
</dl> </dl>
</xsl:template> </xsl:template>
</xsl:stylesheet> </xsl:stylesheet>

View File

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ccm="http://xmlns.libreccm.org"
exclude-result-prefixes="ccm xsl">
<xsl:template name="footer">
<footer>
<ul>
<li>
<a href="/impressum">
<!--Impressum-->
<xsl:value-of select="ccm:localize('footer.impressum')" />
</a>
</li>
<li>
<a href="/privacy">
<!--Privacy-->
<xsl:value-of select="ccm:localize('footer.privacy')" />
</a>
</li>
<li>
<code>imported</code>
</li>
</ul>
</footer>
</xsl:template>
</xsl:stylesheet>

View File

@ -0,0 +1,4 @@
label.critical=Critical
label.error=Error
label.ok=OK
label.warning=Warning

View File

@ -0,0 +1,2 @@
footer.impressum=Impressum
footer.privacy=Privacy