国际化(i18n)

目前文档内容对标 ballcat v0.7.0 以上版本

快速安装

BallCat 基于 Spring 的 MessageSource 体系实现了国际化处理

依赖引入

springboot 项目,直接在项目中引入 starter 组件:

<dependency>
  <groupId>com.hccake</groupId>
  <artifactId>ballcat-spring-boot-starter-i18n</artifactId>
  <version>${lastedVersion}</version>
</dependency>

非 spring-boot 项目,则在引入以上依赖后自己手动对依赖包中的配置类进行注册处理。

业务组件

业务组件是非必须组件,用户可以选择不引入

<dependency>
  <groupId>com.hccake</groupId>
  <artifactId>ballcat-admin-i18n</artifactId>
  <version>${lastedVersion}</version>
</dependency>

此依赖引入了 ballcat-i18n 业务模块,并自定义了一个 I18nMessageProvider

使用业务组件的前提是,必须要使用 ballat 提供的 i18n 相关的表

CREATE TABLE `i18n_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `language_tag` varchar(10) DEFAULT NULL COMMENT '语言标签',
  `code` varchar(60) DEFAULT NULL COMMENT '唯一标识 = 业务:关键词',
  `message` varchar(255) DEFAULT NULL COMMENT '文本值,可以使用 {} 加角标,作为占位符',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `udx_laguage_tag_code` (`language_tag`,`code`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8mb4 COMMENT='国际化信息';

同时注意添加如下配置:

  spring:
    messages:
      basename: "ballcat-*"     

功能使用

Resource Bundle

一组各种语言的国际化配置文件组成了一个 Resource Bundle,国际化中从对应语言的国际化配置文件中提取最终的文案。

配置文件名称格式为 {resourceBundleName}_{language}.properties, 语言可以为空,作为表示默认的配置。

配置文件的内容为 key=value 的结构,key 为 国际化的 Code,value 是该 Code 在当前语言下的文案,支持使用占位符。

我们来定义一个名为 messageResource Bundle,在 resource 目录下, 新建以下三个文件:

  • messages.properties

    # 默认的配置
    test.hello=默认的你好
    test.placeholder={0},默认你好,{1}
    
  • **messages_zh_CN.properties **

    # 中文
    test.hello=你好中文
    test.placeholder={0},你好,{1}
    
  • messages_en_US.properties

    # 英文
    test.hello=Hello
    test.placeholder={0},hello,{1}
    

这时,ide 会自动识别这三个文件,并折叠为名为 messageResource Bundle

i18n-resource-bundle-message

属性配置

只新建 Resource Bundle 还不行,我们需要将其注册到 Spring 容器中,在 spring.messages.basename 属性中配置对应的 Resource Bundle 名称即可,Spring Boot 会自动进行收集处理

下面是 Spring 提供的完整的关于国际化的配置,可以按需修改:

spring:
  messages:
    basename: "messages"               # ResourceBundle 名称,多个用,号隔开,默认是 messages
    fallback-to-system-locale: true     # 当找不到对应区域的语言时,是否回退到系统区域的语言,默认 true
    encoding: 'utf-8'                    # ResourceBundle 的 charset
    cacheDuration: 						# ResourceBundle 加载后的过期时间(s),不设置则不过期,默认不过期
    alwaysUseMessageFormat: false       # 是否始终应用MessageFormat规则,甚至不带参数地解析消息,默认false
    useCodeAsDefaultMessage: false  #找不到对应的区域的语言(包括回退)时,直接返回 code 值,否则抛出异常, 默认flase

Ballcat 对 spring.messages.basename 做了增强处理,支持通配符 *

例如以下配置,可以读取所有的依赖下以 ”ballcat-“ 开头的 ResourceBundle:

spring:
  messages:
    basename: "ballcat-*"               # ResourceBundle 名称,多个用,号隔开,默认是 messages

代码使用

首先注入 MessageSource 对象

@Autowired
private MessageSource messageSource;

然后使用 MessageSource#getMessage 方法,获取对应语言的文案,方法的三个参数分别对应:国际化的code占位符的参数国际化语言

最基本的使用

messageSource.getMessage("test.hello", null, Locale.US);  // Hello
messageSource.getMessage("test.hello", null, Locale.CHINA);  // 你好中文

带占位符的文案

// 可以看出来,占位符中的{0},{1},对应参数数组的角标
messageSource.getMessage("test.placeholder", new Object[]{"小方", "小红"}, Locale.CHINA);  // 小方,你好,小红
// 当参数不存在时,会直接输出占位符
messageSource.getMessage("test.placeholder", new Object[]{"小方"}, Locale.CHINA); // 小方,你好,{1}

默认值回退

当我们使用不存在的语言时,默认会进行回退到系统的语言

// 没有 UK 的语言,回退到系统语言(测试系统为中文环境)
messageSource.getMessage("test.hello", null, Locale.UK);  // 你好中文

可以发现输出的并不是默认配置 messages.properties 中的文案,这是因为当开启了语言回退到系统时,就不再使用默认配置了,关闭此配置再进行测试:

spring:
  messages:
    fallback-to-system-locale: false #取消回退到系统语言

结果则会是:

// 没有 UK 的语言,回退到系统语言(测试系统为中文环境)
messageSource.getMessage("test.hello", null, Locale.UK);  // 默认的你好

MessageSource#getMessage 的重载方法中还可以指定默认语言,这个优先级最高:

// 没有 UK 的语言,回退到指定的默认语言
messageSource.getMessage("test.hello", null, "指定的你好", Locale.UK);  // 指定的你好

切换语言

在 Web 环境下,我们需要根据使用者指定的语言进行文案的返回,这时这个语言 Locale 就是动态获取的:

SpringBoot 会使用 LocaleResolver 解析客户端需要使用的 Locale, 解析完成后会将 Locale 存储于 LocaleContextHolder 中,我们可以利用静态方法 LocaleContextHolder#getLocale 获取到。

我们在请求服务端时,必须携带请求头 Accept-Language,其值为期望的语言类型,因为 SpringBoot 默认注册的 LocaleResolver 实现类是 AcceptHeaderLocaleResolver

Spring 中一共提供了以下几种语言解析器:

  • AcceptHeaderLocaleResolver:从 请求头 Accept-Language 中解析对应的 Locale
  • CoockieLocaleResolver:从 Cookie 的属性 CoockieLocaleResolver.LOCAL 中解析
  • SessionLocaleResolver:从 Session 的属性 SessionLocaleResolver.LOCAL 中解析
  • FixedLocaleResolver:固定的语言解析器,总是返回同一个 Locale,默认是 jvm 的语言环境

用户可以自己按需替换,也可以自定义自己的解析处理器,只需要注册到容器中,就会覆盖默认行为。

代码中,我们不再写死使用的 Locale,而是改为动态获取,这样在客户端切换语言时,就会动态返回不同的语言文案:

messageSource.getMessage("test.hello", null, LocaleContextHolder.getLocale())

校验注解的国际化

Hibernate Validator 是自带国际化的,它默认使用名为 ValidationMessagesResource Bundle,对应的配置文件在其 jar 包。

如果我们需要扩展错误消息,默认情况下,是需要在 classpath 下新建名为 ValidationMessages.properties 以及 ValidationMessages_xx_xx.properties 类似的语言文件,添加自己的国际化配置。

Ballcat 对其做了一些扩展,让它可以使用通过 Spring 加载的 Resource Bundle ,同时还可以使用自带的 ValidationMessages,另外还提供了一种使用 {} 占位符替代默认的国际化信息的能力。

使用示例

我们新建名为 validation 的 Resource Bundle:

  • validation.properties

      validation.username=用户名
      validation.age=年龄
      # 可以使用注解中的属性,作为占位符
      validation.range=需要在{min}和{max}之间
    
  • validation_zh_CN.properties

      validation.username=用户名
      validation.age=年龄
      validation.range=需要在{min}和{max}之间
    
  • validation_en_US.properties

    validation.username=Username
    validation.age=Age
    validation.range=must be between {min} and {max}
    

添加配置:

spring:
  messages:
    basename: "messages, validation"   # ResourceBundle 名称,多个用,号隔开,默认是 messages

新建测试实体:

@Data
public class DemoData {
    // 属性不能为空
    @NotNull
    private String username;

    // 值需要在 0-150 之间
    @Range(min = 0, max = 150)
    private Integer age;
}

测试代码:

@SpringBootTest
class I18nTestValidateTests {
    @Autowired
    private Validator validator;

    @Test
    void testValidate() {
        // 构造一个 username 为null,age 不满足条件的 实例
        DemoData demoData = new DemoData();
        demoData.setAge(200); 
        // 校验并输出结果
        Set<ConstraintViolation<DemoData>> set = validator.validate(demoData);
        for (ConstraintViolation<DemoData> violation : set) {
            System.out.println(violation.getMessage());
        }
    }
}

输出结果:

需要在0150之间
不能为null

这个错误信息是 Hibernate Validator 自带的 Resuource Bundle 中提供的,虽然错误信息有了,但是没有说明是哪个属性出现的问题,我们不能直接拿来使用。

自定义错误提示信息

其实校验注解中有个 message 属性,可以用来指定错误时的提示信息的。

@NotNull 为例,它的 message 属性的默认值为 {javax.validation.constraints.NotNull.message}

public @interface NotNull {
	String message() default "{javax.validation.constraints.NotNull.message}";
}

{} 标识一个国际化Code的占位,Hibernate Validator 会自动对其做国际化转换,如果需要原文输出,不加 {} 就好了,此外,message 中可以传入多个占位符。

我们修改下注解的 message 属性,拼接上我们的属性名试试:

@Data
public class DemoData {
    // 可以多个占位符传参
    @NotNull(message = "{validation.username}:{javax.validation.constraints.NotNull.message}")
    private String username;

    // 可以使用注解上的值
    @Range(min = 0, max = 150, message = "{validation.age}:{org.hibernate.validator.constraints.Range.message}")
    private Integer age;
}

输出结果

用户名:不能为null
年龄:需要在0和150之间

简化 message 配置

以上结果国际化的确是有效的,但是非常繁琐,比如 username 的 notnull 判断,我们写了 username 的国际化配置后,还必须写下其 defaultMessage,这个 defaultMessage 这么长,谁记得住啊?

所以 Ballcat 对其提供了扩展,支持使用空的花括号表示 defaultMessage 的占位

DemoData 校验注解的 message 可以简化为这样:

@Data
public class DemoData {
    // 可以多个占位符传参
    @NotNull(message = "{validation.username}:{}")
    private String username;

    // 可以使用注解上的值
    @Range(min = 0, max = 150,message = "{validation.age}:{}")
    private Integer age;
}

输出结果不变:

用户名:不能为null
年龄:需要在0和150之间

基于注解的国际化

在上面的章节中,我们举例了使用 MessageSource 以编码的方式来进行国际化处理:

messageSource.getMessage("test.hello", null, LocaleContextHolder.getLocale())

但是在实际开发中,如果每个需要国际化处理的属性,都要自己手动操作,会很繁琐,所以 Ballcat 提供了注解 @I18nClass@I18nField,来方便的进行国际化处理。

@I18nClass 注解用来添加在有属性需要国际化处理的类上。

@I18nField 注解则添加在需要国际化处理属性上,该属性必须是 String 类型。

public @interface I18nField {

    // 同 code
	@AliasFor("code")
	String value() default "";

	// 国际化的唯一标识, 当不传值时,则使用被标注的元素的值作为 code
	@AliasFor("value")
	String code() default "";

	/**
	 * 是否进行国际化的条件判断语句(SpEL 表达式),默认为 “”,表示永远翻译
	 * @return 返回 boolean 的 SpEL 表达式
	 */
	String condition() default "";

}

注意:注解国际化只在 web 环境下生效,在 Response 响应时进行处理

使用示例

@Data
@I18nClass // 只会处理填了此注解的类
public class SysMenuPageVO {

    @Schema(value = "菜单标题,值为国际化code")
    private String title;
    
    // 只会处理填了此注解的属性,且属性必须是 String 类型
    // condition:当对象的属性 type !=2 时才会进行国际化 
	@I18nField(condition = "type != 2")
	@Schema(title = "值为国际化code,但是会进行国际化处理,实际响应出去的是国际化后的菜单名称")
	private String i18nTitle;
    
    @Schema(value = "菜单类型 (0目录,1菜单,2按钮)")
    private Integer type;
	
    // .....省略若干属性
    
}

我们将 SysMenuPageVO 对象的中的 i18nTitle 属性值设置为对应的国际化Code,在响应时,就会自动进行国际化翻译。

	@GetMapping("/list")
	@Operation(summary = "查询菜单列表", description = "查询菜单列表")
	public R<List<SysMenuPageVO>> getSysMenuPage(SysMenuQO sysMenuQO) {
		List<SysMenu> sysMenus = sysMenuService.listOrderBySort(sysMenuQO);
		if (CollectionUtil.isEmpty(sysMenus)) {
			R.ok(new ArrayList<>());
		}
        // 这一步转换处理,顺便给 i18nTitle 赋值
		List<SysMenuPageVO> voList = sysMenus.stream().map(SysMenuConverter.INSTANCE::poToPageVo)
				.collect(Collectors.toList());
		return R.ok(voList);
	}

响应数据

{
    "code": 200,
    "message": "Success",
    "data": [
        {
            "id": 100100,
            "title": "menu.system.user",
            "i18nTitle": "系统用户", // 翻译结果
            "type": 1
        },
        {
            "id": 100800,
            "title": "menu.system.menu",
            "i18nTitle": "菜单权限", // 翻译结果
            "type": 1
        }
    ]
}

动态的国际化配置

使用介绍

实际开发中我们经常还会有业务需求上的国际化,例如:

  • 国家表,可能需要在不同语言环境下展示不同语言的国家名。
  • 后台管理中的菜单,数据是存储在数据库中,我们也需要根据不同环境展示不同的语言的菜单名。

使用 Resource Bundle 中硬编码中的国际化,不利于动态的增加和修改国际化配置,BallCat 为此抽象了一个 DynamicMessageSource,其内部利用一个 I18nMessageProvider 对象来动态获取国际化配置信息。

/**
 * 国际化信息的提供者,使用者实现此接口,用于从数据库或者缓存中读取数据
 * @author hccake
 */
public interface I18nMessageProvider {

	/**
	 * 获取 I18nMessage 对象
	 * @param code 国际化唯一标识
	 * @param locale 语言
	 * @return 国际化消息
	 */
	I18nMessage getI18nMessage(String code, Locale locale);

}

用户只需注册自己的 I18nMessageProvider 实现类到 spring 容器中即可。

也可以参看快速安装一节:直接引入 ballcat-admin-i18n 业务组件,其中已经默认提供了一个 CustomI18nMessageProvider 实现类, 会利用数据库 + 缓存来读取国际化信息。

注意,业务组件会依赖 mybatis-plus 和 ballcat-spring-boot-starter-redis 组件,如果无法使用这两个组件,请参考其实现方式,自行构建和管理 I18nMessageProvider

关键依赖

如下 yaml 配置,可以读取所有的依赖下以 ”ballcat-“ 开头的 ResourceBundle:

  spring:
    messages:
      basename: "ballcat-*"     

如果使用 ballcat 的 i18n 业务模块,请务必配置以上的 basename,否则国际化处理时可能会因为找不到对应的配置而抛出异常。 当然,你可以在自己项目中配置对应国际化信息的 key value,这样也能解决

由于 starter 包是不应该介入业务的,所以没有提供默认的 I18nMessageProvider 实现,该 bean 必须由使用者自己注册。 其对应动态配置文件的存储可以是任何方式,如存在 mysql、redis、mongo、es、内存中等等。

ballcat-admin-i18n

    <dependency>
        <groupId>com.hccake</groupId>
        <artifactId>ballcat-admin-i18n</artifactId>
        <version>${lastedVersion}</version>
    </dependency>

当然,ballcat-i18n 相关的业务模块中,也提供了一种基于 mysql 存储的 I18nMessageProvider,同时添加了本地 和 redis 双重缓存,减少频繁读取国际化信息的查询开销。

所以如果不需要定制自己的 I18nMessageProvider,则可以直接引入 ballcat-admin-i18n,并在数据库中生成下表,即可完成国际化:

Excel导出的国际化

参看 Excel 导入导出的文档:http://www.ballcat.cn/guide/feature/excel.html

前端国际化

前端的国际化基于 vue-i18n,官网地址:https://kazupon.github.io/vue-i18n/zh/

国际化的关键文件

  • src/config/projectConfig.js

该文件中定义了系统的默认语言

  • src/locales/index.js

该文件中对 vue-i18n 进行了引入和实例构造,并对语言配置文件实行了懒加载处理,避免无用的网络开销。

这里默认是不支持回退语言的,否则必须先加载回退语言

  • src/core/bootstrap.js

该文件中定义了一个方法,该方法被设置为 vue 的 created 钩子函数,所以在进入或者刷新页面时必然会执行此方法,

我们在此方法中调用了 src/locales/index.js 中提供的方法,进行国际化环境的初始化。

  • src/util/request.js

    axios 配置文件,在 request interceptor 中,对所有请求添加了 Accept-Language 的 header 头

国际化配置的存储

  • src/store/modules/i18n.js
const i18n = {
state: {
  lang: defaultLanguage
},
mutations: {
  SET_LANG: (state, lang) => {
    state.lang = lang
  }
},
getters: {
  lang: state => state.lang
}
}

vuex 模块,抛出了 getters:lang,用于组件获取当前语言,动态更新

  • src/store/storage-types.js
export const APP_LANGUAGE = 'app_language'

定义了 localeStorage 中的存储 key ,方便用户选择语言后的持久化,毕竟 vuex 是存储于内存,持久化才可以防止每次刷新页面都变回默认语言

国际化的使用

基本使用

$t() 方法中传入国际化的 code 即可,$t 已被挂在到 vue 原型中:

<a-form-item :label="$t('i18n.i18nData.code')">
<a-input v-model="queryParam.code" :placeholder="$t('message.pleaseEnter')" />
</a-form-item>

<a-button type="primary" @click="reloadTable">{{ $t('action.query') }}</a-button>

更多使用方法,参看 vue-i18n 文档。

字典

Ballcat 提供的字典组件中,字典数据是存储在服务端的,所以国家化配置实际也是需要在服务端配置,字典项的 attributes 的属性下,有个 languages 对象,该对象的属性 key 为 language-tag,value 即为对应的文本值。

默认已经做好了,国际化的自适应,所以对于字典的国际化,开发者只需在字典项的设置中配置好即可,无需过多关心。

表格

表格列的国际化也很简单,将原本写在 data 下的表格列配置,移入 computed 下,变为计算属性,即可跟随语言环境切换。

computed: {
  columns () {
    return [
      {
        title: '#',
        dataIndex: 'id'
      },
      {
        title: this.$t('i18n.i18nData.languageTag.text'),
        dataIndex: 'languageTag'
      },
      {
        title: this.$t('i18n.i18nData.code.text'),
        dataIndex: 'code'
      },
      {
        title: this.$t('i18n.i18nData.message.text'),
        dataIndex: 'message'
      },
      {
        title: this.$t('common.remark'),
        dataIndex: 'remark'
      }
    ]
  }
}

路由(菜单、多页签、面包屑)

由于路由信息是从服务端返回的,所以当语言切换时,会自动重新向服务端拉取对应语言的路由信息,并重置路由。

重置路由后会发出一个 ”switch-language“ 的事件,在 MultiTab.vue 多页签组件中,监听到此事件,就会进行 title 的切换。

LangSelect 下的 switchLang 方法:

switchLang (row) {
  const newLang = row.key
  if (this.lang !== newLang) {
    // 切换国际化配置
    switchLanguage(newLang)
    // 切换菜单/路由的国际化
    this.GenerateRoutes().then(() => {
      // 重置路由
      resetRouter()
      router.addRoutes(this.userRouters)
      // 发送切换语言事件,多页签会接收此事件,进行多语言切换
      this.$bus.$emit('switch-language', newLang)
    })
  }
}