To main content

Open Liberty: Content negotiation for language output

Published by Benjamin Marwell on

Some web applications will want to react to their user‘s browser language. Here is a short guide on how to do this using a standard JakartaEE restfulWS-based app on Open Liberty.

Step 1: Create a filter

The first thing you will want to do is to create a filter to save away your user‘s accepted language. Why a servlet filter? A filter allows an abstraction from all servlets and controllers. You would not want to react to the user‘s language individually.

Luckily, the Variant class is included in JakartaEE. Here is, how I store the user‘s preferred language in the servlet context:

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Variant;
import jakarta.ws.rs.ext.Provider;
import java.util.List;
import java.util.Locale;

@Provider
public class LanguageFilter implements ContainerRequestFilter, ContainerResponseFilter {

    private static final String LANG = "LanguageFilter.lang";

    public static final List<Variant> VARIANTS = Variant.VariantListBuilder.newInstance()
            .languages(Locale.ENGLISH, Locale.GERMAN)
            .build();

    @Override
    public void filter(ContainerRequestContext requestContext) {
        Variant variant = requestContext.getRequest().selectVariant(VARIANTS);

        if (variant == null) {
            // Error, respond with 406
            requestContext.abortWith(Response.notAcceptable(VARIANTS).build());
        } else {
            // keep the resolved lang around for the response
            requestContext.setProperty(LANG, variant.getLanguageString());
        }
    }

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
        String lang = (String) requestContext.getProperty(LANG);
        responseContext.getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, lang);
    }
}

This filter will extract the preferred language on incoming requests and re-set it (if available) on outgoing requests.

Create a message bundle

Yes, a good old plain Java message bundle! They are still around and will work for what we want to do here.

Here‘s an example:

File de/bmarwell/messages.properties
help.generic = Welcome to the dice parser!
File de/bmarwell/messages_de.properties
help.generic = Willkommen beim Würfel-Parser!

Create an injectable MessageProvider CDI bean

To be able to use a single message provider, you can use this class:

import jakarta.enterprise.context.ApplicationScoped;
import java.io.Serial;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;

@ApplicationScoped
public class ResourceBundleMessageProvider implements MessageProvider {

    @Override
    public String getString(String bundleName, Locale locale, String helpKey) {
        try {
            final ResourceBundle resourceBundle = ResourceBundle.getBundle(bundleName, locale);
            return resourceBundle.getString(helpKey);
        } catch (MissingResourceException missingResourceException) {
            return "";
        }
    }
}

Of course this needs some heave tweaking before such a class could go into production. Defaults, anyone? But for the sake of showing how to react to preferred languages, this will do for now.

Load messages

Now with resources and a CDI bean at hand, you can load the localized strings wherever you like:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.UriInfo;

@Path("/")
@ApplicationScoped
public class HelpResource {

    @Inject
    private MessageProvider msgProvider;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String help(@Context Request request, @Context UriInfo uriInfo) {
        return msgProvider.getString(
              "messages",
              request.selectVariant(LanguageFilter.VARIANTS).getLanguage(),
              "help.generic"
        );
    }

}

Conclusion

It is easy to get started with localized applications. Just be sure to extend on the idea, as this is not production ready.

More features you might want to see in your application are:

  • react to user agents differently.

  • the same language, but different countries.

  • format strings with placeholders like %s.

However, JAX-RS or RestfulWS will already have prepopulated the variant http header for your. Neat!