I18n 国际化概述

国际化也称作 i18n ,其来源是英文单词 internationalization 的首末字符 i 和 n ,18为中间的字符数。由于软件发行 可能面向多个国家,对于不同国家的用户,软件显示不同语言的过程就是国际化。通常来讲,软件中的国际化是通 过配置文件来实现的,假设要支撑两种语言,那么就需要两个版本的配置文件。

主要通过分离程序的核心逻辑语言、区域相关的资源(如文本、日期格式、货币符号等),使程序能适应不同国家 / 地区的语言和文化习惯。

Java国际化

Java自身是支持国际化的java.util.Locale用于指定当前用户所属的语言环境等信息,java.util.ResourceBundle用于查找绑定对应的资源文件。Locale包含了language信息和country信息,Locale创建默认locale对象时使用的静态方法:

1
2
3
4
5
6
7
8
9
/**
* This method must be called only for creating the Locale.*
* constants due to making shortcuts.
*/
private static Locale createConstant(String lang, String country) {
BaseLocale base = BaseLocale.createInstance(lang, country);
return getInstance(base, null);
}

Locale 类

  • 作用:代表特定的语言和区域(如 zh_CN 表示中文(中国),en_US 表示英文(美国))。

资源文件(Resource Bundle)

存储不同语言 / 区域的文本数据,文件名格式为 baseName_languageCode_countryCode.properties(如 message_zh_CN.properties)。

配置文件命名规则:

  • 基础名(baseName):自定义,如 message
  • 语言代码(languageCode):遵循 ISO 639-1 标准(如 zh 中文,en 英文)。
  • 国家 / 地区代码(countryCode):遵循 ISO 3166-1 标准(如 CN 中国,US 美国)。

basename_language_country.properties 必须遵循以上的命名规则,java 才会识别。其中,basename 是必须的,语言和国家是可选的。这里存在一个优先级概念,如果同时提供了 messages.propertiesmessages_zh_CN.propertes 两个配置文件,如果提供的 locale 符合en_CN,那么优先查找messages_en_CN.propertes配置文件,如果没查找到,再查找messages.properties配置文件。最后,提示下,所有的配置文件必须放在classpath中,一般放在resources目录下

ResourceBundle 类

  • 作用:加载资源文件并根据 Locale 获取对应语言的文本。
  • 核心方法
    • ResourceBundle.getBundle(String baseName, Locale locale):根据基础名和区域加载资源文件。
    • getString(String key):根据键获取对应的值。

演示 Java 国际化

创建资源文件

image-20250521171839434
1
2
3
4
5
6
7
8
9
10
11
# message.properties
greeting=Hello, Default!
welcome=Default welcome message

# message_zh_CN.properties
greeting=你好,世界!
welcome=欢迎使用国际化功能

# message_en_US.properties
greeting=Hello, World!
welcome=Welcome to i18n!

编写 Java 代码演示国际化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Locale;
import java.util.ResourceBundle;

public class I18nExample {
public static void main(String[] args) {
// 测试中文(中国)
Locale zhCN = Locale.CHINA;
printMessage(zhCN);

// 测试英文(美国)
Locale enUS = Locale.US;
printMessage(enUS);

// 测试未知区域(使用默认资源文件)
Locale frCA = new Locale("fr", "CA"); // 法语(加拿大),无对应资源文件
printMessage(frCA);
}

private static void printMessage(Locale locale) {
// 加载资源文件(基础名为 "message",区域为 locale)
ResourceBundle bundle = ResourceBundle.getBundle("message", locale);
String greeting = bundle.getString("greeting");
String welcome = bundle.getString("welcome");

System.out.println("===== Locale: " + locale + " =====");
System.out.println(greeting);
System.out.println(welcome);
System.out.println();
}
}

输出

image-20250521171821295

Spring 中支持国际化

MessageSource接口

加载资源文件并根据Locale提供国际化消息。

spring中国际化是通过MessageSource这个接口来支持的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface MessageSource {

/**
* 获取国际化信息
* @param code 表示国际化资源中的属性名;
* @param args用于传递格式化串占位符所用的运行参数;
* @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;
* @param locale 表示本地化对象
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

/**
* 与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

/**
* @param MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个方法相同
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}

常见实现类

  • ResourceBundleMessageSource

    这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源,需遵循上述的特定文件命名规则。

  • ReloadableResourceBundleMessageSource

    这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息,支持资源文件热加载(开发环境实用)。

  • StaticMessageSource

    它允许通过编程的方式提供国际化信息,可以通过这个来实现db中存储国际化信息的功能。

LocaleResolver 接口

解析用户请求中的Locale信息(如浏览器语言、URL 参数、Cookie 等)。

  • 常用实现类
    • AcceptHeaderLocaleResolver:基于浏览器请求头中的Accept-Language
    • SessionLocaleResolver:基于用户会话(Session)存储Locale
    • CookieLocaleResolver:基于 Cookie 存储Locale
    • FixedLocaleResolver:固定Locale,不动态变化。

使用Spring6国际化

创建资源文件

国际化文件命名格式:基本名称 _ 语言 _ 国家.properties

  • 默认文件:messages.properties

    1
    2
    3
    greeting=Hello!
    welcome=Welcome to Spring i18n
    www.ergoutree.com=welcome {0},时间:{1}
  • 中文文件:messages_zh_CN.properties

    1
    2
    3
    greeting=你好!
    welcome=欢迎使用Spring国际化
    www.ergoutree.com=欢迎{0},时间:{1}
  • 英文文件:messages_en_US.properties

    1
    2
    3
    greeting=Hello!
    welcome=Welcome to Spring i18n
    www.ergoutree.com=welcome {0},time:{1}

在 Spring 配置文件中定义MessageSource Bean,指定资源文件位置

bean名称必须为:messageSource

1
2
3
4
5
6
7
8
9
10
@Configuration
public class AppConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages"); // 资源文件基础名(无需扩展名)
messageSource.setDefaultEncoding("UTF-8"); // 防止中文乱码
return messageSource;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class I18nTest {
public static void main(String[] args) {
// 加载Spring配置
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// 获取MessageSource Bean
org.springframework.context.MessageSource messageSource =
context.getBean("messageSource", org.springframework.context.MessageSource.class);

// 测试不同Locale
testLocale(messageSource, Locale.US); // 英文(美国)
testLocale(messageSource, Locale.CHINA); // 中文(中国)
testLocale(messageSource, new Locale("fr")); // 法语(无对应文件,使用默认)
}

private static void testLocale(org.springframework.context.MessageSource messageSource, Locale locale) {
System.out.println("=== Locale: " + locale + " ===");

// 普通消息
String greeting = messageSource.getMessage("greeting", null, locale);
String welcome = messageSource.getMessage("welcome", null, locale);

// 带参数的消息
String paramMessage = messageSource.getMessage(
"www.ergoutree.com",
new Object[]{"Alice", new SimpleDateFormat("HH:mm:ss").format(new Date())},
locale
);

System.out.println(greeting);
System.out.println(welcome);
System.out.println(paramMessage);
System.out.println();
}
}

配置 LocaleResolver

SessionLocaleResolver为例(存储Locale到 Session):

1
2
3
4
5
6
7
8
9
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US); // 默认Locale
return slr;
}
}

配置 LocaleChangeInterceptor

允许通过 URL 参数(如?lang=en)切换Locale

1
2
3
4
5
6
7
8
9
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // URL参数名
registry.addInterceptor(interceptor);
}
}

监控国际化文件的变化

在 Spring 框架中,ReloadableResourceBundleMessageSource类是ResourceBundleMessageSource的增强版本,支持动态监控资源文件的变化,无需重启应用即可生效。这在开发和测试环境中特别有用。

ReloadableResourceBundleMessageSource这个类,功能和上面案例中的ResourceBundleMessageSource类似,不过多了个可以监控国际化资源文件变化的功能,有个方法用来设置缓存时间:

1
public void setCacheMillis(long cacheMillis)

-1:表示永远缓存

0:每次获取国际化信息的时候,都会重新读取国际化文件

大于0:上次读取配置文件的时间距离当前时间超过了这个时间,重新读取国际化文件

还有个按秒设置缓存时间的方法setCacheSeconds,和setCacheMillis类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class AppConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();

// 设置资源文件基础名(无需扩展名)
messageSource.setBasenames("classpath:messages");

// 设置编码,防止中文乱码
messageSource.setDefaultEncoding("UTF-8");

// 设置缓存时间(开发环境建议设为0,生产环境可设为较长时间如60秒)
messageSource.setCacheSeconds(0); // 每次请求都重新加载

return messageSource;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

public class ReloadableI18nTest {
public static void main(String[] args) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
ReloadableResourceBundleMessageSource messageSource =
context.getBean("messageSource", ReloadableResourceBundleMessageSource.class);

// 初始加载
printMessage(messageSource);

// 修改messages_zh_CN.properties文件内容(手动操作)
System.out.println("\n>>> 请在5秒内修改messages_zh_CN.properties文件内容... <<<\n");
TimeUnit.SECONDS.sleep(5);

// 再次加载(如果缓存时间为0,将显示最新内容)
printMessage(messageSource);
}

private static void printMessage(ReloadableResourceBundleMessageSource messageSource) {
Locale zhCN = Locale.CHINA;
System.out.println("当前缓存时间: " + messageSource.getCacheMillis() + "ms");
System.out.println("greeting: " + messageSource.getMessage("greeting", null, zhCN));
}
}