【项目实践】国际化实现

国际化实现

国际化是一种多语言支持的技术,它允许一个应用程序同时支持多种语言。应用程序需要根据根据用户所在的地区的习惯,用户需求和当地的语言响应相应的结果,这个结果可能包括 UI设计,语言。

本文要实现的是基于地区后端接口返回的文字为该对应的语言。效果如下:

js 复制代码
// language = zh-CN
result = {
    code : 200,
    message : 请求成功,
}
// language = en-US
result = {
    code : 200,
    message : request sussessful,
}

国际化实现总结为以下三步:

  1. 获取系统当前的环境
  2. 根据环境获取对应的语言资源
  3. 返回资源结果

系统环境获取

系统环境主要是指获取系统运行时的语言或者地区,本文的核心思想是前端将系统此时的语言信息放入请求头中和请求一并传给后端。

对于前端来说,当首次加载页面时根据用户当前的系统语言或浏览器语言进行初始设置,通过页面上设置按钮控制语言切换,示意的代码如下:

js 复制代码
// 首次加载页面获取浏览器界面语言,navigator.language 返回一个表示用户偏好语言(通常是浏览器界面语言)的字符串
let language = navigator.language;
// 获取页面中用户设置的语言
const languageValue = document.querySelector("#language-set").innerText;
language = languageValue || navigator.language;

const xhr = new XMLHttpRequest();
// 设置请求头
xhr.setRequestHeader("language",language);

对于后端来说,则需要从请求头中取出 language 的值,最简单的方式是通过参数注解 @RequestHeader 来获取请求头中的特定字段值,如下:

java 复制代码
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.stereotype.Controller;

@Controller
public class MyController {

    @GetMapping("/some-endpoint")
    public String getLanguage(@RequestHeader("language") String language) {
        // 处理language值
        System.out.println("Detected language: " + language);
        return "Some Response";
    }
}

通过以上方式每个接口都需要重复如上过程,代码冗余度太高,可以结合 ThreadLocal 和过滤器(Filter)来保存请求头中的language属性,来确保这个属性在整个请求处理生命周期内对当前线程可见。以下是一个简单的示例:

首先创建一个用于存储 language 属性的 ThreadLocal 类:

java 复制代码
import java.lang.ThreadLocal;

public class RequestLanguageHolder {

    private static final ThreadLocal<String> LANGUAGE_HOLDER = new ThreadLocal<>();

    public static void setLanguage(String language) {
        LANGUAGE_HOLDER.set(language);
    }

    public static String getLanguage() {
        return LANGUAGE_HOLDER.get();
    }

    public static void removeLanguage() {
        LANGUAGE_HOLDER.remove();
    }
}

接下来创建一个过滤器,用于在每次请求时从请求头中取出language并保存到ThreadLocal中:

java 复制代码
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class LanguageFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // 获取请求头中的language属性
        String language = httpServletRequest.getHeader("language");

        // 将语言设置到ThreadLocal中
        if (language != null) {
            RequestLanguageHolder.setLanguage(language);
        }

        try {
            // 继续执行过滤链
            chain.doFilter(request, response);
        } finally {
            // 在请求处理完成后移除ThreadLocal中的language
            RequestLanguageHolder.removeLanguage();
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

然后需要将这个过滤器注册到Spring Boot应用中,可以在配置类里添加如下代码:

java 复制代码
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<LanguageFilter> languageFilterRegistration() {
        FilterRegistrationBean<LanguageFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new LanguageFilter());

        // 设置过滤器应用于所有请求
        registration.addUrlPatterns("/*");
        
        return registration;
    }
}

语言资源获取

Spring 的国际化,实际上就是在 Java 国际化(可以看相关知识点)的基础之上做了一些封装,提供了一些新的能力。

使用 Spring 国际化需要我们首先提供一个 MessageSource 实例,常用的 MessageSource 实例是 ReloadableResourceBundleMessageSource,这是一个具备自动刷新能力的 MessageSource,即,用户修改了配置文件之后,在项目不重启的情况下,新的配置就能生效。

java 复制代码
@ConfigurationProperties("i18n")
@Data
public class MessageSourceProperties {

    // 基础文件名
    private String basename = "i18n/messages";

    // 默认编码
    private String defaultEncoding = "UTF-8";

    // 是否使用代码作为默认消息
    private boolean useCodeAsDefaultMessage = true;
}

配置方式很简答,我们只需要将这个 Bean 注册到 Spring 容器中:

java 复制代码
@Configuration
// 将与配置文件绑定好的某个类注入到容器中,使其生效
@EnableConfigurationProperties(MessageSourceProperties.class)
public class MessageSourceAutoConfiguration {

    private MessageSourceProperties messageSourceProperties;

    // 构建该自动配置类时将与配置文件绑定的配置类作为入参传递进去
    public MessageSourceAutoConfiguration(MessageSourceProperties messageSourceProperties) {
        this.messageSourceProperties = messageSourceProperties;
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        Locale.setDefault(Locale.CHINA);
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding(messageSourceProperties.getDefaultEncoding());
        // 设置是否回退到系统本地
        messageSource.setFallbackToSystemLocale(false);
        // 设置是否使用代码作为默认消息
        messageSource.setUseCodeAsDefaultMessage(messageSourceProperties.isUseCodeAsDefaultMessage());
        //设置国际化文件存储路径和名称    i18n目录,messages文件名
        messageSource.setBasename(messageSourceProperties.getBasename());
        return messageSource;
    }
}

对获取资源文件内容的方法进行封装再用,封装类似下面这样:

java 复制代码
import com.auth.cloud.i18n.enums.LanguageEnum;
import org.springframework.context.MessageSource;
import org.springframework.lang.Nullable;

import java.util.Locale;

@Component
public class I18nUtil implements MessageSourceAware{
    private static MessageSource messageSource;

    private static Locale getLanguage(LanguageEnum language) {
        return new Locale(language.getLanguage(), language.getCountry());
    }

    public static String get(String code) {
        return messageSource.getMessage(code, null, Locale.getDefault());
    }

    public static String get(String code, @Nullable Object[] args) {
        return messageSource.getMessage(code, args, Locale.getDefault());
    }

    public static String get(String code, LanguageEnum language) {
        Locale lang = getLanguage(language);
        System.out.println(lang);
        return messageSource.getMessage(code, null, lang);
    }

    public static String get(String code, @Nullable Object[] args, LanguageEnum language) {
        Locale lang = getLanguage(language);
        return messageSource.getMessage(code, args, lang);
    }
    
    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
}

这个工具类实现了 MessageSourceAware 接口,这样就可以拿到 messageSource 对象,然后将 getMessage 方法进行封装。

除了通过 MessageSourceAware 接口拿到 messageSource 对象外,还可以通过 applicationContext.getBean 来获得。

java 复制代码
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    @Override
    //设置Spring上下文
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //判断SpringContextUtil.applicationContext是否为空
        if (SpringContextUtilsAutoConfiguration.applicationContext == null) {
            //如果为空,将applicationContext赋值给SpringContextUtil.applicationContext
            SpringContextUtilsAutoConfiguration.applicationContext = applicationContext;
        }
    }
}
java 复制代码
@Component
public class I18nUtil{
    private static class Lazy {
        // 使用懒加载方式实例化MessageSource对象
        private static final MessageSource messageSource = SpringContextUtil.getBean(MessageSource.class);
    }

    private static MessageSource getInstance() {
        return Lazy.messageSource;
    }
}

测试代码及结果如下:

java 复制代码
public CommonResult<String> testI18n(String role) {
        return CommonResult.success(I18nUtil.get("test", new String[]{role}, LanguageEnum.ENGLISH));
    }
java 复制代码
@Test
public void roleTest() {
   CommonResult<String> role1 = roleController.testI18n("角色");
   log.info(role1);
    //英文的结果: test,char:角色
    //中文的结果: 测试test,字符:角色
}

相关知识点

java 的国际化用法

Java 的国际化(Internationalization, i18n)是指在开发软件时,确保其能够适应不同国家和地区的语言、文化习惯以及技术要求的过程。通过 Java 国际化,应用程序可以根据用户的本地设置动态地显示不同的文本、日期、货币等格式,从而提高用户体验并扩大软件的市场范围。

以下是在 Java 中实现国际化的关键组件和步骤:

  • Locale 类:java.util.Locale 表示特定的语言环境,它包含两个主要信息:语言代码(如 "en" 代表英语,"zh" 代表中文)和国家/地区代码(如 "US" 代表美国,"CN" 代表中国)。Locale 对象用于确定用户的文化偏好,例如日期、时间、数字和货币的格式。
  • ResourceBundle 类:java.util.ResourceBundle 是一个用于加载本地化资源的工具类。开发者将文本和其他本地化数据存储在.properties或.xml文件中,每个文件对应一种特定的语言和区域。根据当前的 LocaleResourceBundle 可以加载相应的资源文件,并从中获取与之对应的字符串。
  • MessageFormat 类:java.text.MessageFormat 提供了格式化消息的能力,允许插入参数化的变量到预定义的消息模板中。这样可以轻松地处理多语言下带占位符的消息,例如"你好,{0}!"在不同语言环境中,"{0}"的位置和内容会根据实际需要替换为具体的名字或其他值。
  • DateFormat 类 和 NumberFormat 类:这些类及其子类用于格式化和解析日期、时间和数字。例如,java.text.SimpleDateFormatjava.text.DecimalFormat 根据给定的 Locale 设置,可以生成符合当地习惯的日期、时间或数字格式。
  • Properties 文件:Properties 文件是一种常见的配置文件格式,用于存储键值对。在 Java 国际化中,可以使用 Properties 文件来存储本地化文本和其他资源的键值对。

其他相关的 API:还包括 CurrencyCollatorChoiceFormat 等类,它们分别帮助处理货币格式、字符排序规则以及基于条件的选择性消息输出等。

在实际应用中,国际化通常涉及以下几个步骤:

  1. 创建资源文件,如 messages_en.properties (英文)、messages_zh_CN.properties (简体中文)。

  2. 在资源文件中存储本地化后的文本字符串。

  3. 在代码中根据当前用户的 Locale 加载正确的 ResourceBundle。

  4. 使用 MessageFormat 进行动态消息格式化。

  5. 在需要的地方使用 DateFormat 或 NumberFormat 来格式化日期、时间或数字。

资源文件的创建和使用

首先我们需要定义自己的资源文件,资源文件命名方式是:

资源名_语言名称_国家/地区名称.properties

其中 _语言名称_国家/地区名称 可以省略,如果省略的话,这个文件将作为默认的资源文件。在 resources 目录下创建如下2个资源文件

接下来我们看下 Java 代码如何加载:

java 复制代码
// 定义 Locale 对象,这个 Locale 对象相当于定义本地环境,说明自己当前的语言环境和地区信息
Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
// ResourceBundle.getBundle 方法去加载配置文件,该方法第一个参数就是资源的名称,第二个参数则是当前的环境
// 配置的 locale 实际上并不存在,那么就会读取 content.properties 文件中的内容(相当于这就是默认的配置)
ResourceBundle res = ResourceBundle.getBundle("messages", localeZh);
String success = res.getString("success");
System.out.println("success = " + success);

获取的资源格式化

Java 中的国际化还提供了一些 Format 对象,用来格式化传入的资源。

Format 主要有三类,分别是:

  • MessageFormat:这个是字符串格式化,可以在资源中配置一些占位符,在提取的时候再将这些占位符进行填充。
  • DateFormat:这个是日期的格式化。
  • NumberFormat:这个是数字的格式化。

不过这三个完全可以单独当成工具类来使用,并非总是要结合 I18N 一起来用,实际上我们在日常的开发中,就会经常使用 DateFormat 的子类 SimpleDateFormat。

MessageFormat 占位符的使用:

java 复制代码
Locale localeEn = new Locale("en", "US");
Locale localeZh = new Locale("zh", "CN");
ResourceBundle res = ResourceBundle.getBundle("messages", localeZh);
String test = res.getString("test");
MessageFormat format = new MessageFormat(test);
Object[] arguments = new Object[]{"java国际化"};
String s = format.format(arguments);
System.out.println("test = " + t);

附表:

语言简称表

语言 简称
简体中文(中国) zh_CN
繁体中文(中国台湾) zh_TW
繁体中文(中国香港) zh_HK
英语(中国香港) en_HK
英语(美国) en_US
英语(英国) en_GB
英语(全球) en_WW
英语(加拿大) en_CA
英语(澳大利亚) en_AU
英语(爱尔兰) en_IE
英语(芬兰) en_FI
芬兰语(芬兰) fi_FI
英语(丹麦) en_DK
丹麦语(丹麦) da_DK
英语(以色列) en_IL
希伯来语(以色列) he_IL
英语(南非) en_ZA
英语(印度) en_IN
英语(挪威) en_NO
英语(新加坡) en_SG
英语(新西兰) en_NZ
英语(印度尼西亚) en_ID
英语(菲律宾) en_PH
英语(泰国) en_TH
英语(马来西亚) en_MY
英语(阿拉伯) en_XA
韩文(韩国) ko_KR
日语(日本) ja_JP
荷兰语(荷兰) nl_NL
荷兰语(比利时) nl_BE
葡萄牙语(葡萄牙) pt_PT
葡萄牙语(巴西) pt_BR
法语(法国) fr_FR
法语(卢森堡) fr_LU
法语(瑞士) fr_CH
法语(比利时) fr_BE
法语(加拿大) fr_CA
西班牙语(拉丁美洲) es_LA
西班牙语(西班牙) es_ES
西班牙语(阿根廷) es_AR
西班牙语(美国) es_US
西班牙语(墨西哥) es_MX
西班牙语(哥伦比亚) es_CO
西班牙语(波多黎各) es_PR
德语(德国) de_DE
德语(奥地利) de_AT
德语(瑞士) de_CH
俄语(俄罗斯) ru_RU
意大利语(意大利) it_IT
希腊语(希腊) el_GR
挪威语(挪威) no_NO
匈牙利语(匈牙利) hu_HU
土耳其语(土耳其) tr_TR
捷克语(捷克共和国) cs_CZ
斯洛文尼亚语 sl_SL
波兰语(波兰) pl_PL
瑞典语(瑞典) sv_SE
西班牙语(智利) es_CL

参考:

梳理一下 Spring 国际化!从用法到源码!

优雅集成i18n实现国际化信息返回_springboot i18n数据库内容

相关推荐
程序员爱钓鱼31 分钟前
Python 编程实战:环境管理与依赖管理(venv / Poetry)
后端·python·trae
w***488232 分钟前
Spring Boot3.x集成Flowable7.x(一)Spring Boot集成与设计、部署、发起、完成简单流程
java·spring boot·后端
程序员爱钓鱼33 分钟前
Python 编程实战 :打包与发布(PyInstaller / pip 包发布)
后端·python·trae
IT_陈寒1 小时前
Redis 性能提升30%的7个关键优化策略,90%开发者都忽略了第3点!
前端·人工智能·后端
Victor3561 小时前
Redis(137)Redis的模块机制是什么?
后端
Victor3561 小时前
Redis(136)Redis的客户端缓存是如何实现的?
后端
不知更鸟7 小时前
Django 项目设置流程
后端·python·django
黄昏恋慕黎明8 小时前
spring MVC了解
java·后端·spring·mvc
G探险者10 小时前
为什么 VARCHAR(1000) 存不了 1000 个汉字? —— 详解主流数据库“字段长度”的底层差异
数据库·后端·mysql
百锦再10 小时前
第18章 高级特征
android·java·开发语言·后端·python·rust·django