Springboot + i18n国际化

在网上找了很多后端国际化的资料,总是不适合自己。因为脑袋本能的就会取拒绝吸收知识,其实认真看理解思路i18n国际化其实很简单。

那么先简单上手一下

1.首先在resources创建i18n的文件夹,文件夹内创建各个国家的脚本

@Configuration
public class ExLangConfig{

    /**
     * 默认解析器 其中locale表示默认语言
     * CookieLocalResolve 是获取Accept-Language的值,如果没有则为默认值
     * 如果不是文本中的国家,则自动展示message中的文本。要是逻辑没有错的话
     */
    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.getDefault());
        return localeResolver;
    }

    /**
     * 默认拦截器 其中lang表示切换语言的参数名
     */
    @Bean
    public WebMvcConfigurer localeInterceptor() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
                localeInterceptor.setParamName("lang");
                registry.addInterceptor(localeInterceptor);
            }
        };
    }
}

2.首先说下这种配置,这种配置需要在url上添加?lang=zh_CN

spring:
  thymeleaf:
    cache: false #关闭页面缓存
    prefix: classpath:/views/ #页面拼接路径
  messages:
    basename: static/i18n/message #相对路径 首部请勿添加斜杠

3.当然还要定义配置文件

<script th:inline="javascript">
	//获取应用路径
	var ROOT = [[${#servletContext.contextPath}]];

	//获取默认语言
	var LANG_COUNTRY = [[${#locale.language+'_'+#locale.country}]];

	//初始化i18n插件
	$.i18n.properties({
		path: ROOT + '/i18n/',//这里表示访问路径
		name: 'message',//文件名开头
		language: LANG_COUNTRY,//文件名语言 例如en_US
		mode: 'both'//默认值
	});

	//初始化i18n函数
	function i18n(msgKey) {
		try {
			return $.i18n.prop(msgKey);
		} catch (e) {
			return msgKey;
		}
	}

	//获取国际化翻译值
	console.log(i18n('user.title'));
	console.log(i18n('User Login'));
</script>

4.前端拿到i8n的方式

然后我们就可以编辑后端访问前端的接口了,修改url后的lang值就可以实现国际化了。这种博客一搜一大堆。这里就不具体演示了。

·

·

·

你以为这么快就完了吗?实际工作比这个复杂多了。

我的需求是后端的异常国际化,用cookie中的lang值来判断具体返回的文本信息

首先分析一下:国际化(如上操作),异常处理(全局异常处理类),cookie(修改翻译文本与lang值得对应关系)。

看样子蛮简单的但是实际上有很多坑,加之自己的技术菜,每次都急于求成。。。这个需求变得异常头疼。

来看看是怎么实现的吧

第一个版本

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.nacos.common.utils.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class I18nLocalResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String language = null;
        //获取请求中的语言参数
        Cookie[] cookies = request.getCookies();
        if (ObjectUtil.isNotEmpty(cookies)) {
            for(Cookie cookie : cookies) {
                String cookieName = cookie.getName();
                if ("lang".equals(cookieName)) {
                    language = cookie.getValue();
                }
            }
        }
        //如果没有返回值就用默认的
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(language)){
            if ("zh-CN".equals(language)) {
                locale = new Locale("zh", "CN");
            } else if ("ru".equals(language)) {
                locale = new Locale("ru", "RU");
            } else {
                locale = new Locale("en", "US");
            }
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

1.这里需要重写resolveLocale方法,这个方法的返回值会作用在翻译的文本语言上。

这里还需要注意的是,应为前端拿到的浏览器的语言是en,ru,zh-CN;和我们后端i18n设置的

zh-CN,ru_RU,en_US不同,所以需要特殊处理。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class I18nConfig implements WebMvcConfigurer {

    @Bean
    public LocaleResolver localeResolver() {
        return new I18nLocalResolver();
    }
}

2.将我们重写的方法注入localeResolver

到了这一步,我们定义好翻译文本后,那么全局异常处理类怎么拿到翻译文本呢?

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

public class I18nMessage {
    private static MessageSourceAccessor accessor;
    private static MessageSource messageSource;
    private static final Set<String> set = new HashSet<>();
    private static ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    static {
        // 资源配置路径
        addLocation("classpath*:i18n/*");
    }

    /**
     * 初始化资源文件的存储器
     */
    protected static void initMessageSourceAccessor() {
        String[] basenames = new String[set.size()];
        set.toArray(basenames);
        ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
        // 配置MessageSource Bean主要是配置资源文件的 baseNames,当然也可以通过 yml 属性spring.messages.basename配置
        reloadableResourceBundleMessageSource.setBasenames(basenames);
        reloadableResourceBundleMessageSource.setCacheSeconds(5);
        reloadableResourceBundleMessageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
        accessor = new MessageSourceAccessor(reloadableResourceBundleMessageSource);
        messageSource = reloadableResourceBundleMessageSource;
    }

    /**
     * 加载指定位置的资源文件
     *
     * @param locationPattern
     */
    public static void addLocation(String locationPattern) {
        try {
            Resource[] resources = resourcePatternResolver.getResources(locationPattern);
            for (Resource resource : resources) {
                String url = resource.getURL().toString();
                int lastIndex = url.lastIndexOf("/");
                String prefix = url.substring(0, lastIndex + 1);
                String suffix = url.substring(lastIndex + 1);
                suffix = suffix.split("\\.")[0].split("_")[0];
                set.add(prefix + suffix);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        initMessageSourceAccessor();
    }

    private I18nMessage() {
    }

    /**
     * 获取messageSource
     *
     */
    public static MessageSource getMessageSource() {
        return messageSource;
    }

    /**
     * 获取国际化资源信息
     *
     * @param code 消息键
     * @return String
     */
    public static String getMessage(String code) {
        return accessor.getMessage(code, LocaleContextHolder.getLocale());
    }

    /**
     * 获取国际化资源信息
     *
     * @param code
     * @param defaultMessage
     */
    public static String getMessage(String code, String defaultMessage) {
        return accessor.getMessage(code, defaultMessage, LocaleContextHolder.getLocale());
    }

    /**
     * 获取国际化资源信息
     *
     * @param code
     * @param args
     * @return String
     */
    public static String getMessage(String code, Object[] args) {
        return accessor.getMessage(code, args, LocaleContextHolder.getLocale());
    }

    public static String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
        return accessor.getMessage(code, args, defaultMessage, locale);
    }

    /**
     * 获取国际化资源信息
     *
     * @param code
     * @param args
     * @param defaultMessage
     * @return String
     */
    public static String getMessage(String code, Object[] args, String defaultMessage) {
        return accessor.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
    }
}

3.i18n里给我们了一个messageSource,我们可以从这个类中定义方法的参数和返回。

那么进入到异常处理类,找到具体需要处理报错的方法

String message = I18nMessage.getMessage(exceptionEnum.getCode(), args, MessageFormatter.arrayFormat(errMsg, argArr).getMessage());



//这是定义在i18n下具体的翻译文本,对应着枚举类的code

//zh_CN
AE000000={0} 不能为空
AE000001={0} 最大长度 {1}
AE000002={0} 范围大小 {1} - {2}

//en_US
AE000000={0} can not be empty
AE000001=List {0} max length {1}
AE000002={0} Range size {1} - {2}

//ru_RU
AE000000={0} не должен быть пустым
AE000001=Список {0}, максимальная длина {1}
AE000002={0} Размер диапазона {1}–{2}

4.将返回的信息包装一层,exceptionEnum.getCode()这个是枚举类中的code,我们将code与翻译文本相同。{0}为占位符,可以当作args传入I18nMessage.getMessage()方法

那么如何自定义报错语言,而不通过枚举类的code呢?

throw new BizException(PortalExceptionEnum.PATTERN_REGEXP, I18nMessage.getMessage("AE006.enName"));

5.直接用I18nMessage.getMessage(),在翻译文本定义AE006.enName

现在还有一种情况,就是带@Validated参数校验的怎么做呢?

@NotBlank(message = "联系人姓名不能为空")
private String contactName;

//改为
@NotBlank(message = "AE001.fieldContactName")
private String contactName;

然后一样再异常处理类中,定义字段检验报错的方法中,包一层i18nMessage。这里其实用的是继承@Validated的报错的类,ValidationException,然后重写里面的方法。

到这里国际化异常的差不多了,自己更改cookie值,然后测试就OK了。。。

直到我以为我弄完了的时候,组长跟我说需要修改一下,不用i18n了。改成直接使用枚举类,再后面多添加一些语言,然后报错的时候直接在全局异常里面判断lang值,返回对应枚举就行了。

所以我一开始为什么要用i18n呢?特此记录

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值