SpringBoot之国际化信息

494 阅读7分钟

SpringBoot之国际化信息

定义

国际化信息也称为本地化信息,根据本地化对象(语言 + 国家/地区)来返回对应格式的信息。

使用

POM

spring-boot-starter-web依赖的spring-context中包含国际化信息功能的核心内容。

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

创建国际化信息来源文件

在resources根目录下创建messages.properties、messages_zh_CN.properties、messages_en_US.properties。
messages是国际化信息来源文件默认的文件名称,如果想要设置成其他名称可以在配置文件application.yml中将spring.messages.basename设置成其他名称,spring.messages.basename支持设置多个文件名称,以逗号相隔,并且文件名称可以包含目录路径如lang/messages,而且还可以是包路径如com.sword.messages
messages_zh_CN.properties和messages_en_US.properties则代表了这两个文件分别是中文中国和英文美国的国际化信息来源文件。详细对照信息见本地化对象Locale

国际化信息配置文件1

国际化信息配置文件2

messages.properties是默认的国际化信息来源文件,国际化信息查找规则如下:

  1. 如果有多个国际化信息来源文件名称,则按照名称先后顺序循环执行接下来的规则。
  2. 根据指定的本地化对象(没有指定则是系统所在地区的默认本地化对象)查找对应的国际化信息来源文件,如果找到了则读取内容作为本级来源并且再去读取默认的国际化信息来源文件作为父级来源,本级来源优先级高于父级来源。
  3. 如果根据指定的本地化对象没有找到对应的来源文件则会根据系统所在地区的默认本地化对象查找对应的国际化信息来源文件,如果找到了则读取内容作为本级来源并且再去读取默认的国际化信息来源文件作为父级来源。
  4. 如果找不到本地化对象对应的国际化信息来源文件,那么就会直接用默认的国际化信息来源文件作为本级来源。
  5. 从本级来源开始查找想要的国际化信息,如果没有找到再从父级来源查找。
  6. 如果找到了国际化信息则返回;如果没有找到则按照步骤2到5继续下一个循环;如果没有找到且没有可以找的来源文件了,则报错。

如当spring.messages.basename=lang/messages,messages且本地化对象为Locale.CHINA则国际化信息查找顺序如下:

国际化信息来源文件读取顺序

注入MessageSource并使用

org.springframework.context.MessageSource是国际化信息接口, 在org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration自动配置类中对该接口进行了注册,所以可以在需要使用国际化信息功能的地方直接注入该接口即可。然后可以使用org.springframework.context.MessageSource#getMessage(java.lang.String, java.lang.Object[], java.util.Locale)方法来获取想要的国际化信息。

/**
 *
 * 国际化信息接口
 * @author sword
 * @date 2021/12/13 15:11
 */
@RestController
@Data
@RequiredArgsConstructor
@Api(tags = "国际化信息接口")
@RequestMapping("/messages")
@Slf4j
public class MessagesApi {
    /**
     * 国际化信息来源
     */
    private final MessageSource messageSource;

    /**
     * 获取默认语言地区的国际化信息(中文)
     * @return java.lang.String 默认语言地区的国际化信息(中文)
     * @author sword
     * @date 2021/12/13 15:14
     */
    @GetMapping("/getDefault")
    @ApiOperation("获取默认语言地区的国际化信息(中文)")
    public String getDefault() {
        return messageSource.getMessage("welcome", new Object[]{}, Locale.getDefault());
    }

    /**
     * 获取默认语言地区的格式化国际化信息(中文)
     * @return java.lang.String 默认语言地区的格式化国际化信息(中文)
     * @author sword
     * @date 2021/12/13 15:14
     */
    @GetMapping("/getFormatDefault")
    @ApiOperation("获取默认语言地区的格式化国际化信息(中文)")
    public String getFormatDefault() {
        Object[] numberArgs = new Object[]{new Double("0.99")};
        Object[] dateArgs = new Object[]{new Date()};
        Object[] stringArgs = new Object[]{"0.99"};
        Object[] otherArgs = new Object[]{new HashMap<String, String>(3){
            private static final long serialVersionUID = 1L;
            {
                put("key1", "value1");
                put("key2", "value2");
            }
        }};

        Object[] numberArg1 = new Object[]{new Double("-1")};
        Object[] numberArg2 = new Object[]{new Double("-0.5")};
        Object[] numberArg3 = new Object[]{new Double("0")};
        Object[] numberArg4 = new Object[]{new Double("1.1")};
        Object[] numberArg5 = new Object[]{new Double("2")};
        Object[] numberArg6 = new Object[]{new Double("3")};

        return "({0}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format", numberArgs, Locale.getDefault()) + ";  "
                + "({0}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format", dateArgs, Locale.getDefault()) + ";  "
                + "({0}, [String(" + stringArgs[0].toString() + ")]) = " + geFormat("format", stringArgs, Locale.getDefault()) + ";  "
                + "({0}, [Map(" + otherArgs[0].toString() + ")]) = " + geFormat("format", otherArgs, Locale.getDefault()) + ";  "
                + "({0, number}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format.number", numberArgs, Locale.getDefault()) + ";  "
                + "({0, integer}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format.number.integer", numberArgs, Locale.getDefault()) + ";  "
                + "({0, currency}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format.number.currency", numberArgs, Locale.getDefault()) + ";  "
                + "({0, percent}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format.number.percent", numberArgs, Locale.getDefault()) + ";  "
                + "({0, number, 0.0000}, [Double(" + numberArgs[0].toString() + ")]) = " + geFormat("format.number.SubformatPattern", numberArgs, Locale.getDefault()) + ";  "
                + "({0, date}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date", dateArgs, Locale.getDefault()) + ";  "
                + "({0, date, short}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date.short", dateArgs, Locale.getDefault()) + ";  "
                + "({0, date, medium}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date.medium", dateArgs, Locale.getDefault()) + ";  "
                + "({0, date, long}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date.long", dateArgs, Locale.getDefault()) + ";  "
                + "({0, date, full}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date.full", dateArgs, Locale.getDefault()) + ";  "
                + "({0, date, yyyy-MM-dd}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.date.SubformatPattern", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time, short}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time.short", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time, medium}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time.medium", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time, long}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time.long", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time, full}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time.full", dateArgs, Locale.getDefault()) + ";  "
                + "({0, time, HH:mm:ss}, [Date(" + dateArgs[0].toString() + ")]) = " + geFormat("format.time.SubformatPattern", dateArgs, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg1[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg1, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg2[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg2, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg3[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg3, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg4[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg4, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg5[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg5, Locale.getDefault()) + ";  "
                + "({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(" + numberArg6[0].toString() + ")]) = " + geFormat("format.choice.SubformatPattern", numberArg6, Locale.getDefault()) + ";  "
                ;
    }

    /**
     * 获取格式化国际化信息
     * @param code 编码
     * @param args 格式化参数
     * @param locale 本地化对象
     * @return java.lang.String
     * @author sword
     * @date 2021/12/17 11:38
     */
    private String geFormat(String code, Object[] args, Locale locale) {
        return messageSource.getMessage(code, args, locale);
    }
}
    /**
    * 查找参数对应的国际化信息,如果找不到则报错
    * @param code 国际化信息的code,建议code取值限于类名或包名,避免冲突
    * @param args 国际化信息需要格式化时用于填充的参数数组
    * @param locale 本地化对象,用于定位国际化信息配置文件
    * @return 国际化信息
    * @throws NoSuchMessageException 找不到则报错
    * @see #getMessage(MessageSourceResolvable, Locale)
    * @see java.text.MessageFormat
    */
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

国际化信息格式化填充表达式

由国际化信息的code和本地化对象找到的国际化信息可以是简单文本内容,如“你好!”或者“Hello!”;也可以是需要使用指定的参数数组进行进一步格式化填充的表达式,如“你好,{0},今天是{1, date, short} {1, time, short}。”

国际化信息格式化填充表达式的格式

{ArgumentIndex[, FormatType][, FormatStyle]}
  • ArgumentIndex:参数数组的下标,即当前表达式要格式化填充参数数组中哪一个元素

  • FormatType:参数格式化类型

    • number:数字
    • date:日期
    • time:时间
    • choice:范围选择
  • FormatStyle:参数格式化样式

参数格式化类型与样式的组合

FormatTypeFormatStyle对应的格式化说明备注
Number.class:NumberFormat.getInstance(getLocale())
Date.class:DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale())
String.class:(String) obj
其他:obj.toString()
如果没有指定参数格式化类型与样式则以入参的类型做自动判断,如果是Number类型则是指定语言环境的通用数值格式,如果是Date类型则是简短日期时间模式和指定语言环境对应的格式,如果是字符串类型,则不做处理,如果是其他类型,则返回toString()--
numberNumberFormat.getInstance(getLocale())指定语言环境的通用数值格式--
numberintegerNumberFormat.getIntegerInstance(getLocale())指定语言环境的整数格式--
numbercurrencyNumberFormat.getCurrencyInstance(getLocale())指定语言环境的货币格式--
numberpercentNumberFormat.getPercentInstance(getLocale())指定语言环境的百分比格式--
numberSubformatPatternnew DecimalFormat(subformatPattern, DecimalFormatSymbols.getInstance(getLocale()))指定格式化表达式和指定语言环境对应的格式,subformatPattern用法详见DecimalFormat类--
dateDateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())默认日期模式(同medium)和指定语言环境对应的格式Locale.CHINA:yyyy-M-d
Locale.US:MMM d, yyyy
dateshortDateFormat.getDateInstance(DateFormat.SHORT, getLocale())简短日期模式和指定语言环境对应的格式Locale.CHINA:yy-M-d
Locale.US:M/d/yy
datemediumDateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())适中日期模式(常用)和指定语言环境对应的格式Locale.CHINA:yyyy-M-d
Locale.US:MMM d, yyyy
datelongDateFormat.getDateInstance(DateFormat.LONG, getLocale())长的日期模式和指定语言环境对应的格式Locale.CHINA:yyyy年M月d日
Locale.US:MMMM d, yyyy
datefullDateFormat.getDateInstance(DateFormat.FULL, getLocale())完整的日期模式和指定语言环境对应的格式Locale.CHINA:yyyy年M月d日 EEEE
Locale.US:EEEE, MMMM d, yyyy
dateSubformatPatternnew SimpleDateFormat(subformatPattern, getLocale())指定表达式的日期模式和指定语言环境对应的格式yyyy-MM-dd
timeDateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())默认时间模式(同medium)和指定语言环境对应的格式Locale.CHINA:H:mm:ss
Locale.US:h:mm:ss a
timeshortDateFormat.getTimeInstance(DateFormat.SHORT, getLocale())简短时间模式和指定语言环境对应的格式Locale.CHINA:ah:mm
Locale.US:h:mm a
timemediumDateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())适中时间模式(常用)和指定语言环境对应的格式Locale.CHINA:H:mm:ss
Locale.US:h:mm:ss a
timelongDateFormat.getTimeInstance(DateFormat.LONG, getLocale())长的时间模式和指定语言环境对应的格式Locale.CHINA:ahh时mm分ss秒
Locale.US:h:mm:ss a z
timefullDateFormat.getTimeInstance(DateFormat.FULL, getLocale())完整的时间模式和指定语言环境对应的格式Locale.CHINA:ahh时mm分ss秒 z
Locale.US:h:mm:ss a z
timeSubformatPatternnew SimpleDateFormat(subformatPattern, getLocale())指定表达式的时间模式和指定语言环境对应的格式HH:mm:ss
choiceSubformatPatternnew ChoiceFormat(subformatPattern)指定表达式的范围选择格式--

Demo

  • Locale.CHINA

    ({0}, [Double(0.99)]) = 0.99
    ({0}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 22-1-2 下午8:35
    ({0}, [String(0.99)]) = 0.99
    ({0}, [Map({key1=value1, key2=value2})]) = {key1=value1, key2=value2}
    ({0, number}, [Double(0.99)]) = 0.99
    ({0, integer}, [Double(0.99)]) = 1
    ({0, currency}, [Double(0.99)]) = ¥0.99
    ({0, percent}, [Double(0.99)]) = 99%
    ({0, number, 0.0000}, [Double(0.99)]) =  0.9900
    ({0, date}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 2022-1-2
    ({0, date, short}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 22-1-2
    ({0, date, medium}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 2022-1-2
    ({0, date, long}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 2022年1月2日
    ({0, date, full}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 2022年1月2日 星期日
    ({0, date, yyyy-MM-dd}, [Date(Sun Jan 02 20:35:49 CST 2022)]) =  2022-01-02
    ({0, time}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 20:35:49
    ({0, time, short}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 下午8:35
    ({0, time, medium}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 20:35:49
    ({0, time, long}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 下午08时35分49秒
    ({0, time, full}, [Date(Sun Jan 02 20:35:49 CST 2022)]) = 下午08时35分49秒 CST
    ({0, time, HH:mm:ss}, [Date(Sun Jan 02 20:35:49 CST 2022)]) =  20:35:49
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(-1.0)]) = 等于负一
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(-0.5)]) = 等于负一
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(0.0)]) = 等于零
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(1.1)]) = 大于一
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(2.0)]) = 等于二
    ({0, choice, -1.0#等于负一|0#等于零|1.0<大于一|2#等于二|2<大于二}, [Number(3.0)]) = 大于二
    
  • Locale.US

    ({0}, [Double(0.99)]) = 0.99
    ({0}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 1/2/22 8:38 PM
    ({0}, [String(0.99)]) = 0.99
    ({0}, [Map({key1=value1, key2=value2})]) = {key1=value1, key2=value2}
    ({0, number}, [Double(0.99)]) = 0.99
    ({0, integer}, [Double(0.99)]) = 1
    ({0, currency}, [Double(0.99)]) = $0.99
    ({0, percent}, [Double(0.99)]) = 99%
    ({0, number, 0.0000}, [Double(0.99)]) =  0.9900
    ({0, date}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = Jan 2, 2022
    ({0, date, short}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 1/2/22
    ({0, date, medium}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = Jan 2, 2022
    ({0, date, long}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = January 2, 2022
    ({0, date, full}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = Sunday, January 2, 2022
    ({0, date, yyyy-MM-dd}, [Date(Sun Jan 02 20:38:30 CST 2022)]) =  2022-01-02
    ({0, time}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 8:38:30 PM
    ({0, time, short}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 8:38 PM
    ({0, time, medium}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 8:38:30 PM
    ({0, time, long}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 8:38:30 PM CST
    ({0, time, full}, [Date(Sun Jan 02 20:38:30 CST 2022)]) = 8:38:30 PM CST
    ({0, time, HH:mm:ss}, [Date(Sun Jan 02 20:38:30 CST 2022)]) =  20:38:30
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(-1.0)]) = equal to minus one
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(-0.5)]) = equal to minus one
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(0.0)]) = equal to zero
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(1.1)]) = greater than one
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(2.0)]) = equal to two
    ({0, choice, -1.0#equal to minus one|0#equal to zero|1.0<greater than one|2#equal to two|2<greater than two}, [Number(3.0)]) = greater than two
    

本地化对象Locale

java.util.Locale表示特定的语言、国家、地区,所以获取国际化信息的时候会根据本地化对象来确定国际化信息来源文件,并且国际化信息需要格式化填充时也会根据本地化对象填充不同格式的数据。

常用对照表

Locale常量语言国家/地区国际化信息来源文件后缀备注
Locale.CHINA
Locale.PRC
Locale.SIMPLIFIED_CHINESE
zhCNzh_CN中文中国
Locale.TAIWAN
Locale.TRADITIONAL_CHINESE
zhTWzh_TW中文中国台湾
Locale.USenUSen_US英文美国
Locale.UKenGBen_GB英文英国
Locale.CANADAenCAen_CA英文加拿大
Locale.CANADA_FRENCHfrCAfr_CA法语加拿大
Locale.FRANCEfrFRfr_FR法语法国
Locale.GERMANYdeDEde_DE德语德国
Locale.ITALYitITit_IT意大利语意大利
Locale.JAPANjaJPja_JP日语日本
Locale.KOREAkoKRko_KR韩语韩国

相关源码详见gitee