diff --git a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java index cf5d407ee..6b4a75306 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java +++ b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java @@ -53,6 +53,7 @@ import net.sf.saxon.value.SequenceType; import net.sf.saxon.value.StringValue; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.libreccm.l10n.GlobalizationHelper; import org.libreccm.theming.ProcessesThemes; import org.libreccm.theming.Themes; import org.libreccm.theming.manifest.ThemeTemplate; @@ -67,7 +68,13 @@ import java.io.Reader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; 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.PropertyResourceBundle; +import java.util.ResourceBundle; import javax.inject.Inject; import javax.xml.transform.ErrorListener; @@ -97,6 +104,9 @@ public class XsltThemeProcessor implements ThemeProcessor { private static final Logger LOGGER = LogManager .getLogger(XsltThemeProcessor.class); + @Inject + private GlobalizationHelper globalizationHelper; + @Inject private SettingsUtils settingsUtils; @@ -137,7 +147,7 @@ public class XsltThemeProcessor implements ThemeProcessor { } catch (ParserConfigurationException ex) { throw new UnexpectedErrorException(ex); } - + final Document document; try { final InputStream xmlBytesStream = new ByteArrayInputStream( @@ -213,6 +223,9 @@ public class XsltThemeProcessor implements ThemeProcessor { configuration .registerExtensionFunction( new GetSettingFunctionDefinition(theme, themeProvider)); + configuration + .registerExtensionFunction( + new LocalizeFunctionDefinition(theme, themeProvider)); configuration .registerExtensionFunction(new TruncateTextFunctionDefinition()); 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 } + * + * 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 } + * + * 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 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 = 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 extends ExtensionFunctionDefinition { diff --git a/ccm-core/src/main/resources/themes/ccm/category-page.xsl b/ccm-core/src/main/resources/themes/ccm/category-page.xsl index fbcdc1a5b..f9ab9cdef 100644 --- a/ccm-core/src/main/resources/themes/ccm/category-page.xsl +++ b/ccm-core/src/main/resources/themes/ccm/category-page.xsl @@ -52,6 +52,8 @@ + + + + + +
  • + + + + +
  • +
  • + imported +
  • + + + + + \ No newline at end of file diff --git a/ccm-core/src/main/resources/themes/ccm/texts/labels.properties b/ccm-core/src/main/resources/themes/ccm/texts/labels.properties new file mode 100644 index 000000000..2866a71d5 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm/texts/labels.properties @@ -0,0 +1,4 @@ +label.critical=Critical +label.error=Error +label.ok=OK +label.warning=Warning diff --git a/ccm-core/src/main/resources/themes/ccm/theme-bundle.properties b/ccm-core/src/main/resources/themes/ccm/theme-bundle.properties new file mode 100644 index 000000000..af5d27da0 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm/theme-bundle.properties @@ -0,0 +1,2 @@ +footer.impressum=Impressum +footer.privacy=Privacy