1

hand implementation of internationalization support example development

Internationalization support should be common for app development partners; as a java back-end partner, generally speaking, there are not many opportunities to contact internationalization. After all, there are not many companies that have business overseas.

SpringBoot provides internationalization support, and there are related tutorials on the Internet. However, when I actually experienced it, I found that it was not as smooth as expected; this article will introduce how SpringBoot supports nationalization, and some precautions in the support process

<!-- more -->

I. Project environment

1. Project dependencies

This project is developed with the help of SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA

Open a web service for testing

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

2. Configuration file

In the configuration file, specify the internationalization parameters and thmeleaf configuration information

application.yml

spring:
  messages:
    basename: i18n/messages/messages
    encoding: UTF-8
    fallbackToSystemLocale: false

  thymeleaf:
    mode: HTML
    encoding: UTF-8
    servlet:
      content-type: text/html
    cache: false

3. Internationalized information files

The above configuration spring.messages.basename specifies the directory and prefix of the international configuration file, the value is i18n/messages/messages

So in the resource directory, create a new file i18n/messages , the internationalized file name is messages-xxx.properties , the project result is as follows

Corresponding information such as simplified Chinese messages_zh_CN.properties

200=成功
500=內部异常
name=用户名
pwd=密码

English messages_en_US.properties

200=success
500=unexpected exception
name=user name
pwd=password

Traditional messages_zh_TW.properties

200=成功
500=內部異常
name=用戶名
pwd=密碼

Description

Note spring.messages.basename this configuration value of international documents directory + file name prefix, such as the above, if the last layer of less messages , you will be prompted to get any configuration

Secondly, in IDEA, after selecting the nationalized file, click on Resource Bundle below, you can enter a more friendly edit box as shown in the figure above, and support to modify information in multiple languages at once

II. Internationalization Support

The previous is the basic configuration of internationalization, so how to get the value of different languages according to the key in the previous configuration?

1. MessageSource

In SpringBoot, MessageSource is mainly used to obtain value information in different languages.

Like a most basic package

public class MsgUtil {
    private static MessageSource messageSource;

    public static void inti(MessageSource messageSource) {
        MsgUtil.messageSource = messageSource;
    }

    /**
     * 获取单个国际化翻译值
     */
    public static String get(String msgKey) {
        try {
            return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
        } catch (Exception e) {
            return msgKey;
        }
    }
}

2. Test the demo

Next, write a basic test demo, modify LocalContextHolder according to the passed parameters, so as to realize the switch of different languages

@Controller
@SpringBootApplication
public class Application {

    public Application(MessageSource messageSource) {
        MsgUtil.inti(messageSource);
    }

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

    @Data
    @Accessors(chain = true)
    public static class RspWrapper<T> {
        private int code;
        private String msg;
        private T data;
    }

    @GetMapping(path = "change")
    @ResponseBody
    public String changeLocal(String language) {
        String[] s = language.split("_");
        LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
        RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
        return JSON.toJSONString(res);
    }
}

The demo is as follows

3. Sub-thread support

Although the language can be switched according to the request parameters above, there is a problem. If internationalization support is performed in the child thread, it will not take effect.

@GetMapping(path = "change2")
@ResponseBody
public String changeLocal(String language) {
    String[] s = language.split("_");
    LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
    
    RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
    return JSON.toJSONString(res);
}

As shown in the figure below, even if the language is modified, the return is the default Chinese

The solution to this is to specify the second inheritable parameter as true when setting Locale

@GetMapping(path = "change3")
@ResponseBody
public String changeLocal(String language) {
    String[] s = language.split("_");
    LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
    RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
    return JSON.toJSONString(res);
}

4. Cookies to cache internationalized information

Although the above supports the setting of internationalization based on the parameters language=zh_CN , it is necessary to bring this parameter 060cd72d6027fe every time the parameters are passed, and we also need to parse the request parameters ourselves. We can consider using interceptors to achieve unified Local settings

This interceptor can be written in accordance with the above method, of course, it is more recommended to directly use the packaged one

@Configuration
public class AutoConfig implements WebMvcConfigurer {
    /**
     * 这个如果不存在,则会抛异常: nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy
     *
     * @return
     */
    @Bean
    public LocaleResolver localeResolver() {
        // 也可以换成 SessionLocalResolver, 区别在于国际化的应用范围
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }

    /**
     * 根据请求参数,来设置本地化
     *
     * @return
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        // Defaults to "locale" if not set
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        interceptorRegistry.addInterceptor(localeChangeInterceptor());
    }
}

Please pay attention to the above localResolver , when we do not register this bean, the operation will throw an exception nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution

In the above example, CookieLocaleResolver is used, so the language information will be cached in the cookie. Once the modification is made, the subsequent changes will take effect.

The test is as follows

@GetMapping(path = "say")
@ResponseBody
public String say(String name) {
    RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
    return JSON.toJSONString(res);
}

@GetMapping(path = "say2")
@ResponseBody
public String say2(String name) {
    RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
    return JSON.toJSONString(res);
}

The language is mainly set in one place, and subsequent visits without language parameters will reuse the previously set language, which makes it more concise to use

5. Internationalization of page elements

The above is that the returned json string supports internationalization. Another scenario is the page we return. We hope that the rendered data can also achieve internationalization support.

There is no difficulty in achieving this on the basis of the above

In the resource directory, create a new directory templates , create a new template file index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="author" content="YiHui"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>一灰灰blog 国际化测试页面</title>
</head>
<body>

<div>
    <div class="title">hello world!</div>
    <br/>
    <div class="content" th:text="'name: ' + ${name}">默认用户名</div>
    <br/>
    <div class="sign" th:text="'pwd: ' + ${pwd}">默认密码</div>
    <br/>
</div>
</body>
</html>

Corresponding controller

@GetMapping(path = {"", "/", "/index"})
public String index(Model model) {
    model.addAttribute("name", MsgUtil.get("name"));
    model.addAttribute("pwd", MsgUtil.get("pwd"));
    return "index";
}

Although the above implementation of nationalization support, it does not look elegant. Is it necessary to escape the back-end interface? Isn't there an easier way?

Themeleaf provides a simpler way to support, just change the above $ to #

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="author" content="YiHui"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>一灰灰blog 国际化测试页面</title>
</head>
<body>

<div>
    <div class="title">hello world!</div>
    <br/>
    <div class="content" th:text="'name: ' + #{name}">默认用户名</div>
    <br/>
    <div class="sign" th:text="'pwd: ' + #{pwd}">默认密码</div>
    <br/>
    <div class="content" th:text="'200: ' + #{200}">200</div>
    <br/>
    <div class="content" th:text="'500: ' + #{500}">500</div>
</div>
</body>
</html>

Corresponding rest

@GetMapping(path = "show")
public String show() {
    return "show";
}

6. Matters needing attention

In the process of realizing internationalization, I encountered the following problems, hereby record

6.1 Unable to obtain configuration information

Using messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale()) query the configuration information, the result shows org.springframework.context.NoSuchMessageException: No message found under code '200' for locale 'en_US'.

When the above problem occurs, of course, first determine whether this parameter is really configured, and secondly, confirm spring.messages.basename is accurate, and the corresponding value is the directory + language prefix

  • If my configuration file is i18n/messages/messages_en_US.properties , then the value should be i18n/messages/messages

6.2 Chinese garbled problem

  • Set code spring.messages.encoding=utf-8

If you find that the above setting still does not take effect, then consider whether the configuration file is utf-8 encoding

6.3 Support internationalization upon request

LocaleChangeInterceptor needs to be added to realize the parsing of the locale according to the request parameters

Secondly, you need to register LocaleResolver . For example, use CookieLocaleResolver demo to save the internationalization information (if you don’t set it, it will throw an exception)

II. Other

0. Project

1. A Grey Blog

It is not as good as the letter. The above content is purely a family statement. Due to limited personal ability, it is inevitable that there will be omissions and errors. If you find a bug or have better suggestions, criticisms and corrections are welcome, and I am grateful.

The following is a gray personal blog, which records all the blog posts in study and work. Welcome everyone to visit

一灰灰blog


小灰灰Blog
251 声望46 粉丝