Abstract

Locale is a technology that is often overlooked in day-to-day development. Especially when you’re developing something that’s just for the domestic market and only in Chinese, locales can be ignored. And when the project proposes multi-language support, because does not have the good understanding, may bury a lot of holes for oneself.

What is the Locale

The Java Doc for java.util.Locale is pretty detailed, but I won’t go into that.

A Locale object represents a specific geographical, political, or cultural region. An operation that requires a Locale to perform its task is called locale-sensitive and uses the Locale to tailor information for the user.

The main thing to remember about Locale instances is that they contain the following information. In years of development experience, Script and Variant are basically not used. No more introductions.

  • language
  • script
  • country (region)
  • variant

lanugage

ISO 639 alpha-2 or alpha-3 language code

In practical use, we almost never touch a three-letter language.

country

ISO 3166 alpha-2 country code or UN M.49 numeric-3 area code.

Also in practice, the basic use of 2 – letter country

Scirpt and variant

The IANA Language Subtag Registry defines the complete list. A sense script is an alternate name for a region, and a variant is a dialect. It’s good to know.

In practice, the first thing most students might think about is writing the following Locale.

  • zh_CN
  • en_US
  • ja_JP

If that’s all you’re thinking about, go to your browser -> Developer Tools -> Console and type the following JS

window.navigator.language
Copy the code

The browser is In Chinese and you are in China. The output is zh-cn and then if you reset your browser language, say to English. Once again, the output is en-cn What?? en-CN?? What the hell is this? So the first thing we know about Locale is that country depends on where you’re actually living. How do you deal with that? I’ll explain how to apply it later.

Application scenarios

A Locale is basically a way of displaying things differently, depending on the country and the language. Practical experience is mainly based on the following two points.

  1. Multilingual (more on that below)
  2. Amount displayed.
  3. Date format display.

Taking the amount display as an example, if you have no experience with this kind of development, you might think that there are different formats for the amount. It’s not always 1,000,000.00. You’d be wrong. Here are two examples.

  1. Japanese. Japanese money is not decimal point.
  2. French. In French, the thousands separator is a space and the decimal point is a comma. Like 1 000, 000,00

Understanding locales correctly, and using them correctly, can produce code that is both formal, concise, and of high quality. Rather than writing their own implementation for each language.

Creates the correct posture for Locale instances

It is important to use the correct pose for creation, which will be important later in the Spring application.

The following code is the most common creation I’ve seen.

Locale locale = new Locale("zh_CN");
Copy the code

Where zh_CN may be passed in directly from the front end as a Locale constructor argument for convenience. This is actually the wrong way to use it. Using this, the locale. language created is zh_cn.

Here are two correct postures. Then compare the results

  1. Locale API
// Use the Locale constructor
// If "zh_CN" is passed to the front end, it needs to be resolved and split by itself
Locale locale = new Locale("zh"."CN");
// Use Locale to preset constants. Check out the Locale source code for yourself.
Locale locale = Locale.SIMPLIFIED_CHINESE
Copy the code
  1. Commons-Lang LocaleUtils.toLocale()
Locale locale = LocaleUtils.toLocale("zh_CN");
Copy the code

The preceding three creation methods can create the same Locale object. Commons Lang3 localeutils.tolocale ()

So let’s compare the difference between the wrong Locale and the correct Locale.

public class LocaleShowCase {
    public static void main(String[] args) {
        logLocale(new Locale("zh_CN"));
        logLocale(Locale.SIMPLIFIED_CHINESE);
    }

    private static void logLocale(Locale locale) {
        System.out.println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =");
        System.out.println(String.format("Locale.toString: %s", locale.toString()));
        System.out.println(String.format("Language: %s", locale.getLanguage()));
        System.out.println(String.format("Country: %s", locale.getCountry()));
        System.out.println(String.format("LanguageTag: %s", locale.toLanguageTag()));
        System.out.println("= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = ="); }}Copy the code

The output

=================================
Locale.toString: zh_cn
Language: zh_cn
Country: 
LanguageTag: und
=================================
=================================
Locale.toString: zh_CN
Language: zh
Country: CN
LanguageTag: zh-CN
=================================
Copy the code

Let’s analyze the results

  1. Let’s start with the data. Incorrect creation of zh_CN as language. Country and LanguageTag are empty
  2. Language output is all lowercase
  3. Country is output in uppercase
  4. LanugageTag connects Language and Country with “-“
  5. Locale.toString connects Language and Country with “_”

zh_CN vs zh-CN

When to use “_” and when to use “-“, it’s really tricky. For example, when parsing Locale in Request.getLocale (), you can handle both formats. The Commons Lang3 localeutils.tolocale () input argument only supports the underscore format.

However, we can define a specification that uses only the “_” format in the back-end service and only the “-” format in the front-end.

The front end

There are too many front-end frameworks, just to mention umi+ Dva + React.

UMI

The Locale processing

Umi-plugin-react /locale is used in umi development projects to handle locale.

import { setLocale, getLocale } from 'umi-plugin-react/locale';

setLocale(language, true);
getLocale();
Copy the code

multilingual

Resource files are named using the “-” format.

.
|-- en-US
|   |-- common.ts
|   `-- form.ts
|-- ja-JP
|   |-- common.ts
|   `-- form.ts
|-- zh-CN
|   |-- common.ts
|   `-- form.ts
|-- en-US.ts
|-- ja-JP.ts
`-- zh-CN.ts
Copy the code

Display multilanguage

import { formatMessage } from 'umi-plugin-react/locale';
formatMessage({id: 'xxx'})
Copy the code

The date shown

import { formatDate } from 'umi-plugin-react/locale';
formatDate(new Date());Copy the code

Digital display

formatNumber(10000000.00);
Copy the code

The back-end service

This section describes only the Stateless Rest API developed based on Spring Boot. SpringMVC is out of date, so I won’t cover it.

The Locale processing

How do I determine the Locale used for the current API call?

Spring Boot uses LocaleResolver to determine which Locale is used for the current API call. After the LocaleResolver gets the Locale, it stores the Locale into LocaleContextHolder.

Spring Boot provides several standard implementations. The main difference is that it provides a method for obtaining locales based on where they are stored

  • AcceptHeaderLocaleResovler from the Request Header Accept – Language
  • CookieLocaleResolver Obtained from a Cookie
  • FixedLocaleResolver FixedLocale, using only the system-configured Locale
  • SessionLocaleResolver Obtained from the Session

While Spring already provides multiple implementations of fetching LocaleResolver, there are more complex scenarios in specific business scenarios. For example, it needs to be set based on the language of the current login user. This is when we need to implement our own set of LocaleResovler.

multilingual

Resource file

In Spring, we can add properties files to do multilingual support.

.
|-- java
....
`-- resources
    `-- i18n
        |-- messages.properties
        |-- messages_ja.properties
        |-- messages_ja_JP.properties
        |-- messages_xx.properties
        |-- messages_zh.properties
        |-- messages_zh_CN.properties
        |-- another.properties
        |-- another_zh_CN.properties
        |-- another_zh_TW.properties
        |-- another_en.properties
        `-- another_ja.properties
Copy the code

You can see that in the example

  • There are two sets of resource files. One group is message and the other is anthor. Modularized management can be done in this way in a project.
  • Each set of resource files can have its own list of supported locales
  • Each file defines the translation for the locale
  • No locale resource file is the default language. Such as messages. The properties and another. The properties. The default resource file content is used when there is no locale match.
  • messages_xx.properties? It’s legal. But the proposal uses, also won’t run into basically. Just to clarify, the framework is supported. Use new Locale(“xx”) to create locales whose language is XX.

Locale Matches the priority

language+country+variant > language+country > lanaguage

Take message*.properties as an example:

  • zh_CN -> messages_zh_CN.properties
  • En or zh_JP -> messages_zh.properties
  • en_US -> messages.properties.

configurationMessageSource

@Configuration
public class MessageConfiguration {
    @Bean
    public MessageSource messageSource(a) {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setBasenames("classpath:i18n/messages"."classpath:i18n/another");
        returnmessageSource; }}Copy the code

Note here messageSource. SetBasenames (” classpath: i18n/messages “, “the classpath: i18n/another”) the BaseName is the group name resource file.

use

@Service
public class XXXService {
    private final MessageSource messageSource;
    public XXXService(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    public String getI18N(String key, Object[] params) {
        return messageSource.getMessage(key, params, LocaleContextHolder.getLocale())
    }
}

Copy the code

The date shown

DateFormat fullDF = DateFormat.getDateInstance(DateFormat.FULL, locale);
System.out.println(fullDF.format(new Date()));
Copy the code

Digital display

System.out.println(NumberFormat.getInstance(locale).format(10000000));
Copy the code

Application in actual scenarios

Most products require users to log in, as mentioned in LocaleResovler. We can use Locale based on the language of the current user. This makes it easier to control the Locale the service receives. And we can define the language supported by the system during development, such as support zh_CN, en_US, ja_JP. This way, API calls after the user logs in don’t have to worry about receiving unsupported locales. Since we need to use the user Settings language, we need to implement a LocaleResovler ourselves.

@Data
public class Principal {
    private String username;
    privateString language; . }public class CustomLocaleResolver implements LocaleResolver {
    private Locale defaultLocale;

    public CustomLocaleResolver(Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public Locale resolveLocale(HttpServletRequest request) {
        Principal principal = (Principal) SecurityContextHolder.getContext().getAuthentication();
        if(principal ! =null && !StringUtils.isEmpty(principal.getLanguage())) {
            return LocaleUtils.toLocale(principal.getLanguage());
        } else {
            return request.getHeader("Accept-Language") != null ? request.getLocale() : this.defaultLocale; }}public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
        throw new UnsupportedOperationException("Cannot change Principal data - use a different locale resolution strategy"); }}@Configuration
public class LocaleConfiguration {
    @Bean
    public CustomLocaleResolver localeResolver(@Value("${default-language:zh_CN}") String defaultLanguage) {
        return newCustomLocaleResolver(LocaleUtils.toLocale(defaultLanguage)); }}Copy the code

Before the user logs in and the front end does nothing, the back end receives a Locale like en_CN and fails to match the resource file. If you follow the resource file design below and set a default translation for each language, you can resolve the problem of receiving irregular locales.

.
|-- java
....
`-- resources
    `-- i18n
        |-- messages.properties
        |-- messages_ja.properties
        |-- messages_ja_JP.properties
        |-- messages_en.properties
        |-- messages_en_US.properties
        |-- messages_en_GB.properties
        |-- messages_zh.properties
        |-- messages_zh_CN.properties
        `-- messages_zh_TW.properties
Copy the code

It doesn’t matter what country you’re in. There is a default match for Chinese, English, and Japanese.

Cheers~