资讯专栏INFORMATION COLUMN

Locale深度解析

spacewander / 2431人阅读

摘要:特别是开发一些只做国内市场,只有中文的项目时,可能就直接被忽视了。应用场景的使用场景基本就是根据不同国家和语言,进行不同的显示。比如中解析时,可以同时处理两种格式。一组为,一组为。对于中文,英文,日文都有一个默认的匹配。

摘要

Locale是日常开发中比较容易忽视的技术点。特别是开发一些只做国内市场,只有中文的项目时,Locale可能就直接被忽视了。而且在项目提出多语言支持的时候,因为没有很好的理解,可能给自己埋了很多坑。

什么是Locale

其实java.util.Locale的Java Doc有很详细的解释,我就不过多解释。

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位字母表示的语言。

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

同样实际使用中,基本使用2位字母表示的国家

scirpt 和 variant

IANA Language Subtag Registry 定义了完整列表。
感觉script是地区的别称,variant是方言。了解一下就好。

在实际使用中,大部分同学第一反应可能是会写出以下Locale。

zh_CN

en_US

ja_JP

如果你也是只想到这些,请打开你的浏览器->开发者工具->控制台中输入以下js

window.navigator.language

浏览器为中文,你又在国内。输出结果就是zh-CN
然后如果重新你设置浏览器语言,比如设置成英文。再执行一下,输出结果是en-CN
What?? en-CN?? 这是什么鬼? 首先通过这个Locale,我们可以知道country,是和你实际在哪个地区有关。
那该怎么处理? 后面详细说怎么应用。

应用场景

Locale的使用场景基本就是根据不同国家和语言,进行不同的显示。实际经验以下2点为主。

多语言 (下文会详细说明)

金额显示。

日期格式显示。

以金额显示为例,如果没有类似开发经验的话,你可以为会想,金额还有不同的格式。一般不都是 1,000,000.00。那你就错了。举两个例子。

日语。日本人金额是不带小数点的。

法语。法语中千位分隔符为空格,小数点为逗号。比如 1 000 000,00

正确理解Locale,并正确使用可以写出既规范,又简练,质量又高的代码。而不是见招拆招,每个语言写一个自己的实现。

创建Locale 实例的正确姿势

使用正确的姿势创建非常重要,这在后面Spring里应用部分非常重要。

以下这段代码是我见过最多的创建方式。

Locale locale = new Locale("zh_CN");

其中zh_CN可能是前端直接传入,为了方便直接作为Locale构造方法参数。其实这是一个错误的用法。这样的使用,创建出来的Locale.language就是zh_cn。

以下先列举一下两种正确姿势。然后比较一下结果

Locale API

// 使用Locale构造方法
// 如果前端传入"zh_CN",此处需要自行解析并拆分
Locale locale = new Locale("zh", "CN");
// 使用Locale预置常量。请自行查看Locale源代码。
Locale locale = Locale.SIMPLIFIED_CHINESE

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

让我们来分析一下结果

首先看一下数据。错误的创建方式,其实是把zh_CN作为language。Country和LanguageTag为空

Language输出时均为小写

Country输出时均为大写

LanugageTag为Language和Country以"-"连接

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

这样的编排方式,不管你在什么国家。对于中文,英文,日文都有一个默认的匹配。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105963.html

相关文章

  • Locale深度解析

    摘要:特别是开发一些只做国内市场,只有中文的项目时,可能就直接被忽视了。应用场景的使用场景基本就是根据不同国家和语言,进行不同的显示。比如中解析时,可以同时处理两种格式。一组为,一组为。对于中文,英文,日文都有一个默认的匹配。 摘要 Locale是日常开发中比较容易忽视的技术点。特别是开发一些只做国内市场,只有中文的项目时,Locale可能就直接被忽视了。而且在项目提出多语言支持的时候,因为...

    taowen 评论0 收藏0
  • SpringMVC之源码分析--LocaleResolver(一)

    摘要:概述为我们提供国际化支持,通过设置系统的环境,根据运行环境使用不同的语言显示。提供接口的作用是解析客户端使用的地区,目的是为了根据这些信息实现视图多语言即国际化。接口继承接口,增加时区支持。 概述 Spring MVC为我们提供国际化支持,通过设置系统的环境,根据运行环境使用不同的语言显示。Spring提供LocaleResolver接口的作用是解析客户端使用的地区(Locale),目...

    HtmlCssJs 评论0 收藏0
  • SpringMVC之源码分析--ViewResolver(六)

    摘要:与一样,该类继承抽象类,并且通过外部的属性文件定义逻辑视图名称与真正的视图对象的关系,属性文件默认是下的,可以通过或属性来指定,该属性指的是文件的基名称,也就是说以属性值开头的属性文件。 概述 本章再学习另外两个ViewResolver,分别是XmlViewResolver和ResourceBundleViewResolver,从功能上说,这两个视图解析器都是从外部资源文件中查找视图V...

    alighters 评论0 收藏0
  • SpringMVC之源码分析--LocaleResolver(二)

    摘要:概述上一篇就默认的进行了分析,详细请参考,本节我们继续分析学习,主要分析解析器类继承关系如下图由上面类图可知,继承并实现接口,主要是操作的工具类,继承接口,增加了信息操作。即通过实现的选择。 概述 上一篇就Spring MVC默认的LocaleResovler(AcceptHeaderLocaleResolver)进行了分析,详细请参考https://segmentfault.com/...

    RichardXG 评论0 收藏0
  • SpringMVC之源码分析--ViewResolver(五)

    摘要:此解析器与差不多,更改下配置文件中的类全路径即可。总结本章介绍了以及三个视图解析器。这部分内容有点儿多,我会尽快结束。 概述 通过上几篇的学习,我们分析了并试验了ViewResolverComposite、BeanNameViewResolver和ContentNegotiatingViewResolver,这三个类都直接实现ViewResolver接口。Spring MVC提供了很多...

    klinson 评论0 收藏0

发表评论

0条评论

spacewander

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<