蒙国造博客

Spring Boot 多语言配置

建立一个多语言网站不仅有助于提高访问量,同时还能减少维护成本(相对于同时维护2个不同语言的网站)。

多语言网站也称为国际化(i18n),是对应于本地化(i10n)的叫法。

说明:i18n 来自英文单词(Internationalization),因为这个单词太长,所以取其首尾字母,加上中间的 18 个字母,组合成 i18n。类似的还有 k8s(kubernetes)等。

Spring Boot 默认就包含有多种语言的国际化工具,包括拦截器、语言处理器(Resolver) 和语言包。

本文详细说明如何使用 Spring Boot 创建一个多语言网站。

用户切换不同语言有2种方式,通过 query 参数以及通过路径方式

第一种情况:语言参数在 URL query 上

第二种情况:语言参数在 URL path 上

本文会讲解这两种方式的实现。

1 创建 Spring Boot 工程

打开 Idea 编辑器,选择菜单 FileNewProject…,创建一个 Spring Boot 项目,如下:

组件只选择 Spring Web 和 Thymeleaf 模板引擎。

再点击 Finish 完成创建。

2 创建语言包文件

这里我们创建 2 个语言包:英文语言包中文语言包,它们将由 messageResource Bean 加载和管理,更多语言包可自行增加。

需要注意的是,非英文语言包创建后不是 UTF-8 格式的,显示的时候会是乱码,所以需要将 .properties 文件默认的编码格式由 ISO-8859-1 改为 UTF-8,方法如下图。

创建英文语言包文件 src/main/resources/i18n/message_en.properties

title = Internationalization test
username = Username
password = Password
submit = Submit

简体中文语言包文件 src/main/resources/i18n/message_zh_CN.properties

title = 国际化测试
username = 用户名
password = 密码
submit = 提交

3 语言信息在 URL 参数上的情况

3.1 运行效果

最终效果如下,通过链接切换语言,相当于修改了请求参数,语言切换后即使刷新页面也同样有效。

3.2 拦截器和 LocaleResolver

创建一个 WebMvcConfig.java 配置类,配置 localeResolvermessageResource两个 bean,以及addInterceptors语言拦截器。

站点配置文件 src/main/java/com/awaimai/i18demo/config/WebMvcConfig.java

package com.awaimai.i18demo.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Bean(name = "localeResolver")
    public LocaleResolver getLocaleResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver();
        resolver.setCookieDomain("myAppLocaleCookie");
        resolver.setCookieMaxAge(60 * 60); // 60 minutes

        return resolver;
    }

    @Bean(name = "messageSource")
    public MessageSource getMessageResource() {
        ReloadableResourceBundleMessageSource messageResource = new ReloadableResourceBundleMessageSource();
        messageResource.setBasename("classpath:i18n/message");
        messageResource.setDefaultEncoding("UTF-8");

        return messageResource;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
        localeInterceptor.setParamName("lang");
        registry.addInterceptor(localeInterceptor).addPathPatterns("/*");
    }
}

3.3 控制器和视图

控制器文件 src/main/java/com/awaimai/i18demo/controller/MainController.java

package com.awaimai.i18demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController {
    @RequestMapping(value = { "login1" })
    public String login() {
        return "login1";
    }
}

Thymeleaf 视图文件 src/main/resources/templates/login1.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:utext="#{title}"></title>
</head>
<body>
<div style="text-align: right;padding:5px;margin:5px 0px;background:#ccc;">
    <a th:href="@{/login1?lang=en}">English</a>
     | 
    <a th:href="@{/login1?lang=cn}">简体中文</a>
</div>
<form method="post" action="">
    <table>
        <tr>
            <td>
                <strong th:utext="#{username}"></strong>
            </td>
            <td><input name="userName"/></td>
        </tr>
        <tr>
            <td>
                <strong th:utext="#{password}"></strong>
            </td>
            <td><input name="password"/></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" th:value="#{submit}"/>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

全部保存后运行 Spring Boot 应用,访问链接: http://localhost:8080/login1?lang=en ,即可看如开始的效果。

4 语言信息在 URL 路径上的情况

这种情况主要在需要做 SEO 的网站,语言选项在 URL 路径上,如下:

这种情况我们需要多一些配置,包括 UrlLocaleInterceptor URL 拦截器和UrlLocaleResolver语言处理器。

URL 拦截器文件 src/main/java/com/awaimai/i18demo/config/UrlLocaleInterceptor.java

package com.awaimai.i18demo.config;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class UrlLocaleInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
        if (localeResolver == null) {
            throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
        }

        // Get Locale from LocaleResolver
        Locale locale = localeResolver.resolveLocale(request);
        localeResolver.setLocale(request, response, locale);

        return true;
    }
}

UrlLocaleResolver语言处理器 src/main/java/com/awaimai/i18demo/config/UrlLocaleResolver.java

package com.awaimai.i18demo.config;

import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class UrlLocaleResolver implements LocaleResolver {
    private static final String URL_LOCALE_ATTRIBUTE_NAME = "URL_LOCALE_ATTRIBUTE_NAME";

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String uri = request.getRequestURI();
        System.out.println("URI=" + uri);

        String prefixEn = request.getServletContext().getContextPath() + "/en/";
        String prefixCn = request.getServletContext().getContextPath() + "/cn/";
        Locale locale = null;

        if (uri.startsWith(prefixEn)) {
            locale = Locale.ENGLISH;
        } else if (uri.startsWith(prefixCn)) {
            locale = Locale.SIMPLIFIED_CHINESE;
        }

        if (locale != null) {
            request.getSession().setAttribute(URL_LOCALE_ATTRIBUTE_NAME, locale);
        }

        if (locale == null) {
            locale = (Locale) request.getSession().getAttribute(URL_LOCALE_ATTRIBUTE_NAME);
            if (locale == null) {
                locale = Locale.ENGLISH;
            }
        }

        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
        // Nothing
    }
}

WebMvcConfig 配置文件需要做一些修改,如下:

package com.awaimai.i18demo.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Bean(name = "localeResolver")
    public LocaleResolver getLocaleResolver() {
        return new UrlLocaleResolver();
    }

    @Bean(name = "messageSource")
    public MessageSource getMessageResource() {
        ReloadableResourceBundleMessageSource messageResource = new ReloadableResourceBundleMessageSource();
        messageResource.setBasename("classpath:i18n/message");
        messageResource.setDefaultEncoding("UTF-8");

        return messageResource;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UrlLocaleInterceptor()).addPathPatterns("/en/*", "/cn/*");
    }
}

创建一个新的控制器 src/main/java/com/awaimai/i18demo/controller/MainController2.java

package com.awaimai.i18demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController2 {
    @RequestMapping(value = { "/{locale:en|cn}/login2" })
    public String login() {
        return "login2";
    }
}

以及新的 Thymeleaf 视图文件 src/main/resources/templates/login2.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:utext="#{title}"></title>
</head>
<body>
<div style="text-align: right;padding:5px;margin:5px 0px;background:#ccc;">
    <a th:href="@{/en/login2}">English</a>
     | 
    <a th:href="@{/cn/login2}">简体中文</a>
</div>
<form method="post" action="">
    <table>
        <tr>
            <td>
                <strong th:utext="#{username}"></strong>
            </td>
            <td><input name="userName"/></td>
        </tr>
        <tr>
            <td>
                <strong th:utext="#{password}"></strong>
            </td>
            <td><input name="password"/></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" th:value="#{submit}"/>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

视图文件仅仅修改了切换语言按钮的链接格式,由@{/login1?lang=en}格式改成了@{/en/login2}

运行项目,访问:http://localhost:8080/en/login2 即可看到开始的效果。

5 内容存储在数据库的多语言网站

如果你需要根据语言信息从不同的数据库、数据表或数据字段中读取数据,那么以上两个例子就无法满足了。

解决方法:为 Spring Boot 配置不同的数据源(Datasources),每个数据源包含不同语言的内容。

退出移动版