Locales as part of the URL in Spring MVC

Viewed 11800

I'm now looking for a framework for multilingual web-applications. At the moment it seems to me that the best choice is Spring MVC. But I faced the fact that all the guidelines for developers suggests to switch languages using LocaleChangeInterceptor in such way:

http://www.somesite.com/action/?locale=en

Unfortunately, there are a number of reasons why I would like avoid this. How could I make language code to be an essential part of URL? For example:

http://www.somesite.com/en/action

Thanks.

UPD: I've found following solution. It's not complete yet, but works. Solution consists in two parts - servlet filter and locale resolver bean. It's looks little bit hackish, but I do not see other way to solve this problem.

public class LocaleFilter implements Filter
{

    ...

    private static final String DEFAULT_LOCALE = "en";
    private static final String[] AVAILABLE_LOCALES = new String[] {"en", "ru"};

    public LocaleFilter() {} 

    private List<String> getSevletRequestParts(ServletRequest request)
    {
        String[] splitedParts = ((HttpServletRequest) request).getServletPath().split("/");
        List<String> result = new ArrayList<String>();

        for (String sp : splitedParts)
        {
            if (sp.trim().length() > 0)
                result.add(sp);
        }

        return result;
    }

    private Locale getLocaleFromRequestParts(List<String> parts)
    {
        if (parts.size() > 0)
        {
            for (String lang : AVAILABLE_LOCALES)
            {
                if (lang.equals(parts.get(0)))
                {
                    return new Locale(lang);
                }
            }
        }

        return null;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        List<String> requestParts = this.getSevletRequestParts(request);
        Locale locale = this.getLocaleFromRequestParts(requestParts);

        if (locale != null)
        {
            request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);

            StringBuilder sb = new StringBuilder();
            for (int i = 1; i < requestParts.size(); i++)
            {
                sb.append('/');
                sb.append((String) requestParts.get(i));
            }

            RequestDispatcher dispatcher = request.getRequestDispatcher(sb.toString());
            dispatcher.forward(request, response);
        }
        else
        {
            request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", new Locale(DEFAULT_LOCALE));
            chain.doFilter(request, response);
        }
    }

    ...
}

public class FilterLocaleResolver implements LocaleResolver
{

    private Locale DEFAULT_LOCALE = new Locale("en");

    @Override
    public Locale resolveLocale(HttpServletRequest request)
    {
        Locale locale = (Locale) request.getAttribute(LocaleFilter.class.getName() + ".LOCALE");
        return (locale != null ? locale : DEFAULT_LOCALE);
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale)
    {
        request.setAttribute(LocaleFilter.class.getName() + ".LOCALE", locale);
    }

}

So there is no need to map locale in each action in controllers. The following example will work fine:

@Controller
@RequestMapping("/test")
public class TestController
{

    @RequestMapping("action")
    public ModelAndView action(HttpServletRequest request, HttpServletResponse response)
    {
        ModelAndView mav = new ModelAndView("test/action");
        ...
        return mav;
    }

}
5 Answers

In addition to the provided answers here's a way how to let Thymeleaf prepend the locale in path after context path automatically by implementing a ILinkBuilder:

@Bean
public ILinkBuilder pathVariableLocaleLinkBuilder() {
    PathVariableLocaleLinkBuilder pathVariableLocaleLinkBuilder = new PathVariableLocaleLinkBuilder();
    pathVariableLocaleLinkBuilder.setOrder(1);
    return pathVariableLocaleLinkBuilder;
}

@Bean
SpringTemplateEngine templateEngine(ThymeleafProperties properties, ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects, ObjectProvider<ILinkBuilder> linkBuilders) {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
    engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
    templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
    dialects.orderedStream().forEach(engine::addDialect);
    linkBuilders.orderedStream().forEach(engine::addLinkBuilder);
    return engine;
}

And here's the LinkBuilder itself:

public class PathVariableLocaleLinkBuilder extends AbstractLinkBuilder {
    @Autowired
    private LocaleResolver localeResolver;

    @Override
    public String buildLink(IExpressionContext context, String base, Map<String, Object> parameters) {
        Validate.notNull(context, "Expression context cannot be null");

        if (base == null) {
            return null;
        }

        if (!isLinkBaseContextRelative(base)) {
            return base;
        }

        if (!(context instanceof IWebContext)) {
            throw new TemplateProcessingException(
                    "Link base \"" + base + "\" cannot be context relative (/...) unless the context " +
                            "used for executing the engine implements the " + IWebContext.class.getName() + " interface");
        }

        final HttpServletRequest request = ((IWebContext) context).getRequest();
        return "/" + localeResolver.resolveLocale(request) + base;
    }

    private static boolean isLinkBaseContextRelative(final CharSequence linkBase) {
        if (linkBase.length() == 0 || linkBase.charAt(0) != '/') {
            return false;
        }

        return linkBase.length() == 1 || linkBase.charAt(1) != '/';
    }
}
Related