How to configure i18n in Spring boot 2 + Webflux + Thymeleaf?

Viewed 3831

I just start a new project based on Spring boot 2 + Webflux. On upgrading version of spring boot and replace spring-boot-starter-web with spring-boot-starter-webflux classes like

  • WebMvcConfigurerAdapter
  • LocaleResolver
  • LocaleChangeInterceptor

are missing. How now can I configure defaultLocale, and interceptor to change the language?

4 Answers

Just add a WebFilter that sets the Accept-Language header from the value of a query parameter. The following example gets the language from the language query parameter on URIs like http://localhost:8080/examples?language=es:

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import reactor.core.publisher.Mono;

import static org.springframework.util.StringUtils.isEmpty;

@Component
public class LanguageQueryParameterWebFilter implements WebFilter {

    private final ApplicationContext applicationContext;

    private HttpWebHandlerAdapter httpWebHandlerAdapter;

    public LanguageQueryParameterWebFilter(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void loadHttpHandler() {
        this.httpWebHandlerAdapter = applicationContext.getBean(HttpWebHandlerAdapter.class);
    }

    @Override
    public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
        final ServerHttpRequest request = exchange.getRequest();
        final MultiValueMap<String, String> queryParams = request.getQueryParams();
        final String languageValue = queryParams.getFirst("language");

        final ServerWebExchange localizedExchange = getServerWebExchange(languageValue, exchange);
        return chain.filter(localizedExchange);
    }

    private ServerWebExchange getServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
        return isEmpty(languageValue)
                ? exchange
                : getLocalizedServerWebExchange(languageValue, exchange);
    }

    private ServerWebExchange getLocalizedServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
        final ServerHttpRequest httpRequest = exchange.getRequest()
                .mutate()
                .headers(httpHeaders -> httpHeaders.set("Accept-Language", languageValue))
                .build();

        return new DefaultServerWebExchange(httpRequest, exchange.getResponse(),
                httpWebHandlerAdapter.getSessionManager(), httpWebHandlerAdapter.getCodecConfigurer(),
                httpWebHandlerAdapter.getLocaleContextResolver());
    }
}

It uses @EventListener(ApplicationReadyEvent.class) in order to avoid cyclic dependencies.

Feel free to test it and provide feedback on this POC.

Another solution with spring boot starter web flux, which is much more cleaner, is to define your own HttpHandler using WebHttpHandlerBuilder in which you can set your LocaleContextResolver.

Documentation (see 1.2.2. WebHandler API) : https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-customize

MyLocaleContextResolver.java

public class MyLocaleContextResolver implements LocaleContextResolver {


    @Override
    public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {      
        return new SimpleLocaleContext(Locale.FRENCH);        
    }

    @Override
    public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
        throw new UnsupportedOperationException();
    }
}

Then in a config file (annotated with @Configuration) or in your spring boot application file, defined your own HttpHandler bean.

Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public HttpHandler httpHandler(ApplicationContext context) {

        MyLocaleContextResolver localeContextResolver = new MyLocaleContextResolver();

        return WebHttpHandlerBuilder.applicationContext(context)
                .localeContextResolver(localeContextResolver) // set your own locale resolver
                .build();

    }

}

That's it!

Starting with Spring Boot 2.4.0, the WebFluxAutoConfiguration contains a bean definition for the LocaleContextResolver, which allows us to inject custom LocaleContextResolver. For reference, the following is the default bean definition in Spring Boot 2.5.4 (the implementation may be different in earlier versions):

@Bean
@Override
@ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public LocaleContextResolver localeContextResolver() {
    if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
        return new FixedLocaleContextResolver(this.webProperties.getLocale());
    }
    AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
    localeContextResolver.setDefaultLocale(this.webProperties.getLocale());
    return localeContextResolver;
}

You can provide your own LocaleContextResolver implementation to get the locale from the query parameter by providing a custom bean definition:

//@Component("localeContextResolver")
@Component(WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public class RequestParamLocaleContextResolver implements LocaleContextResolver {
    @Override
    public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
        List<String> lang = exchange.getRequest().getQueryParams().get("lang");
        Locale targetLocale = null;
        if (lang != null && !lang.isEmpty()) {
            targetLocale = Locale.forLanguageTag(lang.get(0));
        }
        if (targetLocale == null) {
            targetLocale = Locale.getDefault();
        }
        return new SimpleLocaleContext(targetLocale);
    }

    @Override
    public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
        throw new UnsupportedOperationException(
            "Cannot change lang query parameter - use a different locale context resolution strategy");
    }
}

Note that the framework consumes the LocaleContextResolver with a specific name localeContextResolver (WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME). You need to provide the bean with the given name. See #24209.

Related