diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 2e6380efb31b3..3b750aeaecb2c 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1657,6 +1657,47 @@ hello_name=Hallo {name}! <1> <2> <1> Each line in a localized file represents a message template. <2> Keys and values are separated by the equals sign. +Once we have the localized bundles defined we need a way to _select_ a correct bundle. +If you use a message bundle expression in a template you'll have to specify the `locale` attribute of a template instance. + +.`locale` Attribute Example +[source,java] +---- +@Singleton +public class MyBean { + + @Inject + Template hello; + + String render() { + return hello.instance().setAttribute("locale", Locale.forLanguageTag("cs")).render(); <1> + } +} +---- +<1> You can set a `Locale` instance or a locale tag string (IETF). + + +NOTE: When using <> the `locale` attribute is derived from the the `Accept-Language` header if not set by a user. + +The `@Localized` qualifier can be used to inject a localized message bundle interface. + +.Injected Localized Message Bundle Example +[source,java] +---- +@Singleton +public class MyBean { + + @Localized("cs") <1> + AppMessages msg; + + String render() { + return msg.hello_name("Jachym"); + } +} +---- +<1> The annotation value is a locale tag string (IETF). + + === Configuration Reference include::{generated-dir}/config/quarkus-qute.adoc[leveloffset=+1, opts=optional] diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java index f5852dc693a18..b68248a51e67d 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/i18n/MessageBundles.java @@ -21,6 +21,8 @@ import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.Resolver; import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.Variant; import io.quarkus.qute.runtime.MessageBundleRecorder.BundleContext; public final class MessageBundles { @@ -81,10 +83,21 @@ static void setupNamespaceResolvers(@Observes EngineBuilder builder, BundleConte public CompletionStage resolve(EvalContext context) { Object locale = context.getAttribute(ATTRIBUTE_LOCALE); if (locale == null) { - return defaultResolver.resolve(context); + Object selectedVariant = context.getAttribute(TemplateInstance.SELECTED_VARIANT); + if (selectedVariant != null) { + locale = ((Variant) selectedVariant).getLocale(); + } + if (locale == null) { + return defaultResolver.resolve(context); + } } + // First try the exact match Resolver localeResolver = interfaces .get(locale instanceof Locale ? ((Locale) locale).toLanguageTag() : locale.toString()); + if (localeResolver == null && locale instanceof Locale) { + // Next try the language + localeResolver = interfaces.get(((Locale) locale).getLanguage()); + } return localeResolver != null ? localeResolver.resolve(context) : defaultResolver.resolve(context); } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index 0345334ed4bee..7c3bc40d3154b 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -184,7 +185,8 @@ private Template template() { Variant selected = (Variant) getAttribute(TemplateInstance.SELECTED_VARIANT); String id; if (selected != null) { - id = variants.variantToTemplate.get(selected); + // Currently, we only use the content type to match the template + id = variants.getId(selected.getContentType()); if (id == null) { id = variants.defaultTemplate; } @@ -206,6 +208,15 @@ public TemplateVariants(Map variants, String defaultTemplate) { this.defaultTemplate = defaultTemplate; } + String getId(String contentType) { + for (Entry entry : variantToTemplate.entrySet()) { + if (entry.getKey().getContentType().equals(contentType)) { + return entry.getValue(); + } + } + return null; + } + @Override public String toString() { return "TemplateVariants{default=" + defaultTemplate + ", variants=" + variantToTemplate + "}"; @@ -213,7 +224,7 @@ public String toString() { } private static Map initVariants(String base, List availableVariants, ContentTypes contentTypes) { - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); for (String path : availableVariants) { if (!base.equals(path)) { map.put(new Variant(null, contentTypes.getContentType(path), null), path); diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MessageBundleLocaleFromVariantTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MessageBundleLocaleFromVariantTest.java new file mode 100644 index 0000000000000..997b205f6a83d --- /dev/null +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/MessageBundleLocaleFromVariantTest.java @@ -0,0 +1,32 @@ +package io.quarkus.qute.resteasy.deployment; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class MessageBundleLocaleFromVariantTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(AppMessages.class, AppMessageHelloResource.class) + .addAsResource(new StringAsset( + "{msg:hello_name('Georg')}"), + "templates/hello.html") + .addAsResource(new StringAsset( + "hello=Hallo Welt!\nhello_name=Hallo {name}!"), + "messages/msg_de.properties")); + + @Test + public void testMessageBundles() { + given().header("Accept-Language", "de-DE").when().get("/hello").then().body(is("Hallo Georg!")); + + } +} diff --git a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/VariantTemplateTest.java b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/VariantTemplateTest.java index a74f1d63adf25..fe1beab155dfb 100644 --- a/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/VariantTemplateTest.java +++ b/extensions/resteasy-qute/deployment/src/test/java/io/quarkus/qute/resteasy/deployment/VariantTemplateTest.java @@ -25,7 +25,7 @@ public class VariantTemplateTest { @Test public void testVariant() { given().when().accept("text/plain").get("/item/10").then().body(Matchers.is("Item foo: 10")); - given().when().get("/item/20").then().body(Matchers.is("Item foo: 20")); + given().when().accept("text/html").get("/item/20").then().body(Matchers.is("Item foo: 20")); } } diff --git a/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/runtime/TemplateResponseFilter.java b/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/runtime/TemplateResponseFilter.java index d9b3c8b02e051..170019b73d8d0 100644 --- a/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/runtime/TemplateResponseFilter.java +++ b/extensions/resteasy-qute/runtime/src/main/java/io/quarkus/resteasy/qute/runtime/TemplateResponseFilter.java @@ -1,8 +1,9 @@ package io.quarkus.resteasy.qute.runtime; import java.io.IOException; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Locale; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; @@ -18,6 +19,7 @@ @Provider public class TemplateResponseFilter implements ContainerResponseFilter { + @SuppressWarnings("unchecked") @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { @@ -30,24 +32,30 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont TemplateInstance instance = (TemplateInstance) entity; Object variantsAttr = instance.getAttribute(TemplateInstance.VARIANTS); if (variantsAttr != null) { - @SuppressWarnings("unchecked") - List variants = (List) variantsAttr; + List variants = new ArrayList<>(); + for (Variant variant : (List) variantsAttr) { + variants.add(new javax.ws.rs.core.Variant(MediaType.valueOf(variant.getMediaType()), variant.getLocale(), + variant.getEncoding())); + } javax.ws.rs.core.Variant selected = requestContext.getRequest() - .selectVariant(variants.stream() - .map(v -> new javax.ws.rs.core.Variant(MediaType.valueOf(v.getMediaType()), v.getLocale(), - v.getEncoding())) - .collect(Collectors.toList())); + .selectVariant(variants); + if (selected != null) { + Locale selectedLocale = selected.getLanguage(); + if (selectedLocale == null) { + List acceptableLocales = requestContext.getAcceptableLanguages(); + if (!acceptableLocales.isEmpty()) { + selectedLocale = acceptableLocales.get(0); + } + } instance.setAttribute(TemplateInstance.SELECTED_VARIANT, - new Variant(selected.getLanguage(), selected.getMediaType().toString(), selected.getEncoding())); + new Variant(selectedLocale, selected.getMediaType().toString(), selected.getEncoding())); mediaType = selected.getMediaType(); } else { - // TODO we should use the default - mediaType = null; + mediaType = responseContext.getMediaType(); } } else { - // TODO how to get media type from non-variant templates? - mediaType = null; + mediaType = responseContext.getMediaType(); } try { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java index 43bde3d4cac9d..d455a7becb0ab 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java @@ -25,7 +25,7 @@ public class VariantTemplateTest { @Test public void testVariant() { given().when().accept("text/plain").get("/item/10").then().body(Matchers.is("Item foo: 10")); - given().when().get("/item/20").then().body(Matchers.is("Item foo: 20")); + given().when().accept("text/html").get("/item/20").then().body(Matchers.is("Item foo: 20")); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java index e2c3c0da7c59c..458c8a6f7bea2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java @@ -1,7 +1,8 @@ package io.quarkus.resteasy.reactive.qute.runtime; +import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Locale; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.core.MediaType; @@ -15,6 +16,7 @@ public class TemplateResponseFilter { + @SuppressWarnings("unchecked") @ServerResponseFilter public Uni filter(ResteasyReactiveContainerRequestContext requestContext, ContainerResponseContext responseContext) { Object entity = responseContext.getEntity(); @@ -26,16 +28,24 @@ public Uni filter(ResteasyReactiveContainerRequestContext requestContext, TemplateInstance instance = (TemplateInstance) entity; Object variantsAttr = instance.getAttribute(TemplateInstance.VARIANTS); if (variantsAttr != null) { - @SuppressWarnings("unchecked") - List variants = (List) variantsAttr; + List variants = new ArrayList<>(); + for (Variant variant : (List) variantsAttr) { + variants.add(new javax.ws.rs.core.Variant(MediaType.valueOf(variant.getMediaType()), variant.getLocale(), + variant.getEncoding())); + } javax.ws.rs.core.Variant selected = requestContext.getRequest() - .selectVariant(variants.stream() - .map(v -> new javax.ws.rs.core.Variant(MediaType.valueOf(v.getMediaType()), v.getLocale(), - v.getEncoding())) - .collect(Collectors.toList())); + .selectVariant(variants); + if (selected != null) { + Locale selectedLocale = selected.getLanguage(); + if (selectedLocale == null) { + List acceptableLocales = requestContext.getAcceptableLanguages(); + if (!acceptableLocales.isEmpty()) { + selectedLocale = acceptableLocales.get(0); + } + } instance.setAttribute(TemplateInstance.SELECTED_VARIANT, - new Variant(selected.getLanguage(), selected.getMediaType().toString(), selected.getEncoding())); + new Variant(selectedLocale, selected.getMediaType().toString(), selected.getEncoding())); mediaType = selected.getMediaType(); } else { mediaType = responseContext.getMediaType();