Skip to content

Commit

Permalink
Merge pull request #14801 from mkouba/issue-14794
Browse files Browse the repository at this point in the history
Qute message bundles - use locale from the selected variant if needed
  • Loading branch information
mkouba authored Feb 5, 2021
2 parents 9480b5f + ded1d7d commit 3056287
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 25 deletions.
41 changes: 41 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<quarkus-resteasy-qute,resteasy_integration>> 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]
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -81,10 +83,21 @@ static void setupNamespaceResolvers(@Observes EngineBuilder builder, BundleConte
public CompletionStage<Object> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -206,14 +208,23 @@ public TemplateVariants(Map<Variant, String> variants, String defaultTemplate) {
this.defaultTemplate = defaultTemplate;
}

String getId(String contentType) {
for (Entry<Variant, String> entry : variantToTemplate.entrySet()) {
if (entry.getKey().getContentType().equals(contentType)) {
return entry.getValue();
}
}
return null;
}

@Override
public String toString() {
return "TemplateVariants{default=" + defaultTemplate + ", variants=" + variantToTemplate + "}";
}
}

private static Map<Variant, String> initVariants(String base, List<String> availableVariants, ContentTypes contentTypes) {
Map<Variant, String> map = new HashMap<>();
Map<Variant, String> map = new LinkedHashMap<>();
for (String path : availableVariants) {
if (!base.equals(path)) {
map.put(new Variant(null, contentTypes.getContentType(path), null), path);
Expand Down
Original file line number Diff line number Diff line change
@@ -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!"));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html><body>Item foo: 20</body></html>"));
given().when().accept("text/html").get("/item/20").then().body(Matchers.is("<html><body>Item foo: 20</body></html>"));
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +19,7 @@
@Provider
public class TemplateResponseFilter implements ContainerResponseFilter {

@SuppressWarnings("unchecked")
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
Expand All @@ -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<Variant> variants = (List<Variant>) variantsAttr;
List<javax.ws.rs.core.Variant> variants = new ArrayList<>();
for (Variant variant : (List<Variant>) 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<Locale> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html><body>Item foo: 20</body></html>"));
given().when().accept("text/html").get("/item/20").then().body(Matchers.is("<html><body>Item foo: 20</body></html>"));
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +16,7 @@

public class TemplateResponseFilter {

@SuppressWarnings("unchecked")
@ServerResponseFilter
public Uni<Void> filter(ResteasyReactiveContainerRequestContext requestContext, ContainerResponseContext responseContext) {
Object entity = responseContext.getEntity();
Expand All @@ -26,16 +28,24 @@ public Uni<Void> filter(ResteasyReactiveContainerRequestContext requestContext,
TemplateInstance instance = (TemplateInstance) entity;
Object variantsAttr = instance.getAttribute(TemplateInstance.VARIANTS);
if (variantsAttr != null) {
@SuppressWarnings("unchecked")
List<Variant> variants = (List<Variant>) variantsAttr;
List<javax.ws.rs.core.Variant> variants = new ArrayList<>();
for (Variant variant : (List<Variant>) 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<Locale> 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();
Expand Down

0 comments on commit 3056287

Please sign in to comment.