Locale深度解析

摘要

Locale是平常開發中比較容易忽視的技術點。特別是開發一些只作國內市場,只有中文的項目時,Locale可能就直接被忽視了。並且在項目提出多語言支持的時候,由於沒有很好的理解,可能給本身埋了不少坑。javascript

什麼是Locale

其實java.util.Locale的Java Doc有很詳細的解釋,我就不過多解釋。html

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.

主要記住Locale實例包括如下信息就能夠了。在多年的開發經驗中,script和variant基本沒有用到。就不過多介紹。前端

  • language
  • script
  • country (region)
  • variant

lanugage

ISO 639 alpha-2 or alpha-3 language code

在實際使用中,基本咱們碰不到3位字母表示的語言。java

country

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

一樣實際使用中,基本使用2位字母表示的國家react

scirpt 和 variant

IANA Language Subtag Registry 定義了完整列表。
感受script是地區的別稱,variant是方言。瞭解一下就好。後端

在實際使用中,大部分同窗第一反應多是會寫出如下Locale。api

  • zh_CN
  • en_US
  • ja_JP

若是你也是隻想到這些,請打開你的瀏覽器->開發者工具->控制檯中輸入如下js瀏覽器

window.navigator.language

瀏覽器爲中文,你又在國內。輸出結果就是zh-CN
而後若是從新你設置瀏覽器語言,好比設置成英文。再執行一下,輸出結果是en-CN
What?? en-CN?? 這是什麼鬼? 首先經過這個Locale,咱們能夠知道country,是和你實際在哪一個地區有關。
那該怎麼處理? 後面詳細說怎麼應用。前端框架

應用場景

Locale的使用場景基本就是根據不一樣國家和語言,進行不一樣的顯示。實際經驗如下2點爲主。oracle

  1. 多語言 (下文會詳細說明)
  2. 金額顯示。
  3. 日期格式顯示。

以金額顯示爲例,若是沒有相似開發經驗的話,你能夠爲會想,金額還有不一樣的格式。通常不都是 1,000,000.00。那你就錯了。舉兩個例子。

  1. 日語。日本人金額是不帶小數點的。
  2. 法語。法語中千位分隔符爲空格,小數點爲逗號。好比 1 000 000,00

正確理解Locale,並正確使用能夠寫出既規範,又簡練,質量又高的代碼。而不是見招拆招,每一個語言寫一個本身的實現。

建立Locale 實例的正確姿式

使用正確的姿式建立很是重要,這在後面Spring裏應用部分很是重要。

如下這段代碼是我見過最多的建立方式。

Locale locale = new Locale("zh_CN");

其中zh_CN多是前端直接傳入,爲了方便直接做爲Locale構造方法參數。其實這是一個錯誤的用法。這樣的使用,建立出來的Locale.language就是zh_cn。

如下先列舉一下兩種正確姿式。而後比較一下結果

  1. Locale API
// 使用Locale構造方法
// 若是前端傳入"zh_CN",此處須要自行解析並拆分
Locale locale = new Locale("zh", "CN");
// 使用Locale預置常量。請自行查看Locale源代碼。
Locale locale = Locale.SIMPLIFIED_CHINESE
  1. Commons-Lang LocaleUtils.toLocale()
Locale locale = LocaleUtils.toLocale("zh_CN");

以上三種建立方式,能夠建立出同樣的Locale object。
本人推薦使用Commons Lang3 LocaleUtils.toLocale()

下面咱們的來對比一下錯誤和正解方式建立的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("=================================");
    }
}

輸出結果

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

讓咱們來分析一下結果

  1. 首先看一下數據。錯誤的建立方式,實際上是把zh_CN做爲language。Country和LanguageTag爲空
  2. Language輸出時均爲小寫
  3. Country輸出時均爲大寫
  4. LanugageTag爲Language和Country以"-"鏈接
  5. Locale.toString則是Language和Country以"_"鏈接

zh_CN vs zh-CN

何時使用"_",何時使用"-",確實比較搞。
好比request.getLocale()中解析Locale時,能夠同時處理兩種格式。而Commons Lang3 LocaleUtils.toLocale()的入參只支持下劃線格式。

不過咱們能夠定義這樣的規範,在後端服務中只使用"_"格式,而前端只使用"-"格式。

前端

前端框架太多,就只說一下最近在玩的umi+dva+react。

UMI

Locale處理

umi開發的項目中使用umi-plugin-react/locale來處理Locale。

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

setLocale(language, true);
getLocale();

多語言

資源文件使用"-"格式命名。

.
|-- 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

顯示多語言

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

日期顯示

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

數字顯示

formatNumber(10000000.00);

後端服務

這裏只介紹基於Spring Boot開發的Stateless Rest API。SpringMVC已通過時,就不作介紹。

Locale處理

如何確認當前API調用使用的Locale?

Spring Boot使用LocaleResolver來肯定當前API調用使用什麼Locale。在LocaleResolver獲取Locale以後,將Locale存入LocaleContextHolder中。

Spring Boot提供了幾個標準實現,主要區別是針對Locale存放的地方不同提供對應獲取方式

  • AcceptHeaderLocaleResovler 從Request Header的Accept-Language中獲取
  • CookieLocaleResolver 從Cookie中獲取
  • FixedLocaleResolver 固定Locale,只使用系統配置的Locale
  • SessionLocaleResolver 從Session中獲取

雖然Spring已經提供了多種獲取LocaleResolver實現,可是在具體業務場景中會有更復雜的場景。好比須要根據當前登陸用戶的語言設置。這個時間就須要咱們本身實現一套LocaleResovler。

多語言

資源文件

在Spring中,咱們能夠添加properties文件來作多語言支持。

.
|-- 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

能夠看到在例子

  • 有兩組資源文件。一組爲message,一組爲anthor。在項目中能夠這種方式進行模塊化管理。
  • 每組資源文件能夠有本身支持的locale列表
  • 每一個文件定義對應locale的翻譯
  • 沒有locale資源文件爲默認語言。如messages.properties, another.properties。當沒有locale匹配時,使用默認資源文件內容。
  • messages_xx.properties? 這是合法的。但建議使用,也基本不會碰到。這裏是說明一下,框架是支持的。用new Locale("xx")能夠建立language爲xx的Locale。

Locale匹配優先級

language+country+variant > language+country > lanaguage

以message*.properties爲例:

  • zh_CN -> messages_zh_CN.properties
  • zh或zh_JP -> messages_zh.properties
  • en_US -> messages.properties.

配置MessageSource

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

這裏注意messageSource.setBasenames("classpath:i18n/messages", "classpath:i18n/another")BaseName就是資源文件的組名。

使用方式

@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())
    }
}

日期顯示

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

數字顯示

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

實際場景中的應用

通常產品基本須要用戶登陸,在LocaleResovler中也提到。咱們能夠根據當前用戶的語言設置做爲使用Locale。這樣比較好控制服務接收到的Locale。並且咱們在開發時能夠定義好系統支持的語言,好比支持zh_CN, en_US, ja_JP。這樣在用戶登陸後的API調用就不用擔憂接收到不支持的Locale。而由於須要使用用戶設置語言,咱們須要本身實現一個LocaleResovler。

@Data
public class Principal {
    private String username;
    private String 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 new CustomLocaleResolver(LocaleUtils.toLocale(defaultLanguage));
    }
}

而用戶沒有登陸以前,而前端不作任何處理時,後端會接收到相似en_CN的Locale,而沒法匹配資源文件。若是按如下資源文件設計,給每一個語言設置一個默認翻譯,則能夠解決接收到不規則Locale問題。

.
|-- 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

這樣的編排方式,無論你在什麼國家。對於中文,英文,日文都有一個默認的匹配。

相關文章
相關標籤/搜索