-
2019-01-18 11:27:18
1 Java国际化的思路
Java程序的国际化的思路是将程序中的标签、提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对,每个资源文件中的key是不变的,但value则随不同国家、语言改变
2. Java程序的国际化主要通过如下三个类完成
Ø java.util.ResourceBundle:用于加载一个国家、语言资源包。
Ø java.util.Locale:用于封装一个特定的国家/区域、语言环境。
Ø java.text.MessageFormat:用于格式化带占位符的字符串。
为了实现程序的国际化,必须先提供程序所需要的资源文件。资源文件的内容是很多key-value对。其中key是程序使用的部分,而value则是程序界面的显示字符串。
资源文件的命名可以有如下三种形式:
Ø baseName _ language _country.properties
Ø baseName _language.properties
Ø baseName.properties
其中baseName是资源文件的基本名,用户可以自由定义。而language和country都不可随意变化,必须是Java所支持的语言和国家。
3 Java支持的语言和国家
事实上,Java不可能支持所有国家和语言,如需要获取Java所支持的语言和国家,可调用Locale类的getAvailableLocale方法获取,该方法返回一个Locale数组,该数组里包含了Java所支持的语言和国家
package com.homecredit.smt.tipper;
import java.util.Locale;
import java.util.ResourceBundle;public class LocaleList {
public static void main(String[] args) {
// 返回Java所支持的全部国家和语言的数组
Locale[] localeList = Locale.getAvailableLocales();
// 遍历数组的每个元素,依次获取所支持的国家和语言
for (int i = 0; i < localeList.length; i++) {
// 打印出所支持的国家和语言
System.out.println(localeList[i].getDisplayCountry() + "=" + localeList[i].getCountry() + " "
+ localeList[i].getDisplayLanguage()
+ "=" + localeList[i].getLanguage());
}
// 取得系统默认的国家/语言环境
Locale myLocale = Locale.getDefault();
// 根据指定国家/语言环境加载资源文件
ResourceBundle bundle = ResourceBundle.getBundle("messages", myLocale);
// 打印从资源文件中取得的消息
System.out.println(bundle.getString("customer.name.label.default"));
}
}
------------------------------------------------------------------------------------------------------------------= =
阿拉伯联合酋长国=AE 阿拉伯文=ar
约旦=JO 阿拉伯文=ar
叙利亚=SY 阿拉伯文=ar
克罗地亚=HR 克罗地亚文=hr
比利时=BE 法文=fr
巴拿马=PA 西班牙文=es
马耳他=MT 马耳他文=mt
委内瑞拉=VE 西班牙文=es
= 保加利亚文=bg
台湾地区=TW 中文=zh
= 意大利文=it
= 朝鲜文=ko
= 乌克兰文=uk
= 拉托维亚文(列托)=lv
丹麦=DK 丹麦文=da
波多黎哥=PR 西班牙文=es
越南=VN 越南文=vi
美国=US 英文=en
黑山=ME 塞尔维亚文=sr
瑞典=SE 瑞典文=sv
玻利维亚=BO 西班牙文=es
新加坡=SG 英文=en
巴林=BH 阿拉伯文=ar
= 葡萄牙文=pt
沙特阿拉伯=SA 阿拉伯文=ar
= 斯洛伐克文=sk
也门=YE 阿拉伯文=ar
印度=IN 印地文=hi
= 爱尔兰文=ga
马耳他=MT 英文=en
芬兰=FI 芬兰文=fi
= 爱沙尼亚文=et
= 瑞典文=sv
= 捷克文=cs
波斯尼亚和黑山共和国=BA 塞尔维亚文=sr
= 希腊文=el
乌克兰=UA 乌克兰文=uk
= 匈牙利文=hu
瑞士=CH 法文=fr
= 印度尼西亚文=in
阿根廷=AR 西班牙文=es
埃及=EG 阿拉伯文=ar
日本=JP 日文=ja
萨尔瓦多=SV 西班牙文=es
巴西=BR 葡萄牙文=pt
= 白俄罗斯文=be
冰岛=IS 冰岛文=is
捷克共和国=CZ 捷克文=cs
= 西班牙文=es
波兰=PL 波兰文=pl
= 土耳其文=tr
西班牙=ES 加泰罗尼亚文=ca
塞尔维亚及黑山=CS 塞尔维亚文=sr
马来西亚=MY 马来文=ms
= 克罗地亚文=hr
= 立陶宛文=lt
西班牙=ES 西班牙文=es
哥伦比亚=CO 西班牙文=es
保加利亚=BG 保加利亚文=bg
= 阿尔巴尼亚文=sq
= 法文=fr
= 日文=ja
波斯尼亚和黑山共和国=BA 塞尔维亚文=sr
= 冰岛文=is
巴拉圭=PY 西班牙文=es
= 德文=de
厄瓜多尔=EC 西班牙文=es
美国=US 西班牙文=es
苏丹=SD 阿拉伯文=ar
= 英文=en
罗马尼亚=RO 罗马尼亚文=ro
菲律宾=PH 英文=en
= 加泰罗尼亚文=ca
突尼斯=TN 阿拉伯文=ar
黑山=ME 塞尔维亚文=sr
危地马拉=GT 西班牙文=es
= 斯洛文尼亚文=sl
韩国=KR 朝鲜文=ko
塞浦路斯=CY 希腊文=el
墨西哥=MX 西班牙文=es
俄罗斯=RU 俄文=ru
洪都拉斯=HN 西班牙文=es
香港=HK 中文=zh
挪威=NO 挪威文=no
匈牙利=HU 匈牙利文=hu
泰国=TH 泰文=th
伊拉克=IQ 阿拉伯文=ar
智利=CL 西班牙文=es
= 芬兰文=fi
摩洛哥=MA 阿拉伯文=ar
爱尔兰=IE 爱尔兰文=ga
= 马其顿文=mk
土耳其=TR 土耳其文=tr
爱沙尼亚=EE 爱沙尼亚文=et
卡塔尔=QA 阿拉伯文=ar
= 塞尔维亚文=sr
葡萄牙=PT 葡萄牙文=pt
卢森堡=LU 法文=fr
阿曼=OM 阿拉伯文=ar
= 泰文=th
阿尔巴尼亚=AL 阿尔巴尼亚文=sq
多米尼加共和国=DO 西班牙文=es
古巴=CU 西班牙文=es
= 阿拉伯文=ar
= 俄文=ru
新西兰=NZ 英文=en
塞尔维亚=RS 塞尔维亚文=sr
瑞士=CH 德文=de
乌拉圭=UY 西班牙文=es
= 马来文=ms
希腊=GR 希腊文=el
以色列=IL 希伯来文=iw
南非=ZA 英文=en
泰国=TH 泰文=th
= 印地文=hi
法国=FR 法文=fr
奥地利=AT 德文=de
= 荷兰文=nl
挪威=NO 挪威文=no
澳大利亚=AU 英文=en
= 越南文=vi
荷兰=NL 荷兰文=nl
加拿大=CA 法文=fr
拉脱维亚=LV 拉托维亚文(列托)=lv
卢森堡=LU 德文=de
哥斯达黎加=CR 西班牙文=es
科威特=KW 阿拉伯文=ar
= 塞尔维亚文=sr
利比亚=LY 阿拉伯文=ar
= 马耳他文=mt
瑞士=CH 意大利文=it
= 丹麦文=da
德国=DE 德文=de
阿尔及利亚=DZ 阿拉伯文=ar
斯洛伐克=SK 斯洛伐克文=sk
立陶宛=LT 立陶宛文=lt
意大利=IT 意大利文=it
爱尔兰=IE 英文=en
新加坡=SG 中文=zh
= 罗马尼亚文=ro
加拿大=CA 英文=en
比利时=BE 荷兰文=nl
= 挪威文=no
= 波兰文=pl
中国=CN 中文=zh
日本=JP 日文=ja
希腊=GR 德文=de
塞尔维亚=RS 塞尔维亚文=sr
= 希伯来文=iw
印度=IN 英文=en
黎巴嫩=LB 阿拉伯文=ar
尼加拉瓜=NI 西班牙文=es
= 中文=zh
马其顿王国=MK 马其顿文=mk
白俄罗斯=BY 白俄罗斯文=be
斯洛文尼亚=SI 斯洛文尼亚文=sl
秘鲁=PE 西班牙文=es
印度尼西亚=ID 印度尼西亚文=in
英国=GB 英文=en
hello4. 利用URLClassLoader 来读取指定文件路径下的配置
public static void main(String[] args) throws MalformedURLException {
String messageFolder = "D:/MyWorkSpacePractice/TipService/tipper-service-conf/src/main/resources/conf/dev/tipper-service-conf/i18n";
URI urI = new File(messageFolder).toURI();
URL messageFileUrl = urI.toURL();
ClassLoader classLoader = new URLClassLoader(new URL[] { messageFileUrl });
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.getDefault(), classLoader);// 打印从资源文件中取得的消息
System.out.println(bundle.getString("customer.name.label.default"));
}更多相关内容 -
Java Spring项目国际化(i18n)详细方法与实例
2020-08-19 11:39:14主要介绍了Java Spring项目国际化详细方法与实例,需要的朋友可以参考下 -
Java国际化的工具 propedit
2016-03-20 19:23:39用于Java国际化的工具 -
java国际化&时间处理
2019-04-18 01:37:19NULL 博文链接:https://hoochiang.iteye.com/blog/1848534 -
java实现国际化
2019-03-22 01:36:17NULL 博文链接:https://yuyu456.iteye.com/blog/903505 -
Java国际化配置
2014-04-13 22:38:27用Java写的一个国际化语言配置模块,可实现简单的国际化配置。 -
java 国际化转换
2011-11-23 15:18:05java国际化操作,主要是讲字符转化为ASIIC -
Java 后端国际化设计方案
2021-01-27 15:47:31Java 后端国际化设计方案设计需求 设计需求 国际化配置集中到数据库中进行管理,包含前端部分国际化 最好可动态添加国际化的语种 好用易用 高效Java 后端国际化设计方案
前言
代码就不放全了,还在公司上跑着呢,就放一点非核心代码,工具类封装之类的
设计需求
- 国际化配置集中到数据库中进行管理,包含前端部分国际化
- 最好可动态添加国际化的语种
- 好用易用
- 高效
设计思路
- 利用自定义注解来启用国际化,拦截所有返回请求进行处理
- 大数据量处理使用多线程并行处理
- 国际化数据保存在 Redis 中视为热点数据
- 使用手动刷新方式,保证无缝刷新缓存
- 国际化部分数据以 Json 形式来保存,保证扩展性
- 语种以配置的形式保存,必要可添加语种
- 需要多语言切换的数据全部以占位符代替,通过自定义注解统一替换
- 当前语言环境通过前端带在请求头里给后端,后端默认为英文
数据库设计
- type: 类型,非空
- module: 模块,可为空
- label: 标签,非空
- langs: 国际化 Json String,非空
- to_web: 是否返回前端,不返回的就只是后端使用,将数据切成两半
其中 type.module.label 的组合为唯一标识
需要国际化翻译的数据保存形式
由于后端部分为自动生成的,因此 label 使用 UUID()
后端返回数据部分,比如:异常提示这部分的翻译
功能设计
用到的工具类
自定义注解
个人认为只需要一个启动开关即可,没必要做成那种一个个接口去加
/** * 开启国际化注解 */ @Import(TranslationAspect.class) @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EnableTranslation { }
使用
切面开发
TranslationAspect
/** * 国际化实现 * @Author: linjinp * @Date: 2021/1/18 15:47 */ @Slf4j @Aspect public class TranslationAspect { // 默认语种 private final static String DEFAULT_LANGUAGE = "en"; // 切点 // 指定拦截的包路径 @Pointcut("execution(* com.xxx..*.*(..))") private void pointcut() {} @Around("pointcut()") private Object around(ProceedingJoinPoint pjp) throws Throwable { // ... // TODO // ... return ... }
从请求头获取当前语言环境
获取当前返回值的类型
/** * 获取列表中数据类型 * @param obj * @return */ private static Class getArrayListClass(Object obj) { // 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型) if (obj != null && ArrayList.class.equals(obj.getClass()) && ((List) obj).size() > 0) { return ((List) obj).get(0).getClass(); } return null; } /** * 获取数据类型 * @param obj * @return */ private static Class getClass(Object obj) { // 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型) if (obj != null && !ArrayList.class.equals(obj.getClass())) { return obj.getClass(); } return null; }
将返回值转为 Json String 后,统一获取其中的占位符
使用 StringBuilder 保存,防止大量的对象被创建
/** * 获取字符串中所有的变量参数 * @param str 字符串/对象Json * @return */ private static List<String> findParams(String str) { List<String> params = new ArrayList<>(); // 转化为二进制 char[] chars = str.toCharArray(); // 找到标志的索引 int findIndex = -1; for (int i = 0; i < chars.length; i ++) { // 判断 ${ 组合 // i <= chars.length - 3 防越界,${A,假如以此结尾,$ 在 length - 3 位置 if (i <= chars.length - 3 && chars[i] == '$' && chars[i + 1] == '{') { // 获取首个变量的下标索引 findIndex = i + 2; } // 判断 } 且,已经存在索引下标,防止前面单独出现 } 的情况 if (chars[i] == '}' && findIndex != -1) { // 添加变量 params.add(new String(Arrays.copyOfRange(chars, findIndex, i))); // 重置标识 findIndex = -1; } } return params; }
替换返回值中所有的占位符为对应语言
/** * 数据处理 * @param lang 语言环境 * @param data 返回数据 * @param languages 语言包 * @param params 需要替换的参数列表 * @return */ private static StringBuilder dataProcess(String lang, StringBuilder data, List<MultiLanguage> languages, List<String> params) { // 循环数据 for (MultiLanguage language : languages) { // 有配置语言,非空对象,为后端使用的标签 if (StringUtils.isNotBlank(language.getLangs()) && !"{}".equals(language.getLangs())) { for (String param : params) { // 如果标签组合匹配 if (language.equalsCombination(param)) { // 假如当前环境非默认语种,判断当前语种是否已经配置,如果没配置或为空,使用默认语种数据 if (!DEFAULT_LANGUAGE.equals(lang) && JsonUtils.toMap(language.getLangs()).containsKey(lang) && StringUtils.isNotBlank((String) JsonUtils.toMap(language.getLangs()).get(lang))) { data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(lang)))); } else { data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(DEFAULT_LANGUAGE)))); } } } } } return data; } /** * 正则内容替换 * @param source 数据 * @param key 国际化标签 * @param value 国际化对应值 * @return */ private static String replaceRegex(String source, String key, String value) { String regex = "\\$\\{"+key+"\\}"; return source.replaceAll(regex, value); }
最后要保证返回值的类型正确
也是为了保证旧代码的兼容,比如你后来才加的国际化,这也是之前获取数据类型的原因
数据缓存
构建线程池
<!-- hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.10</version> </dependency>
/** * 线程池配置 * * @Author: linjinp * @Date: 2020/9/29 10:54 */ @Slf4j @Component public class ExecutorConfig { public static ExecutorService executor; // 初始线程数量 private final static int DEFAULT_NUM = 5; // 最大线程数 private final static int MAX_NUM = 10; // 最大等待线程数 private final static int MAX_WAITING = 100; @Bean public ExecutorService createExecutor() { this.executor = ExecutorBuilder.create() // 默认初始化 5 个线程 .setCorePoolSize(DEFAULT_NUM) // 最大线程数 10 .setMaxPoolSize(MAX_NUM) // 最大等待线程数 100 .setWorkQueue(new LinkedBlockingQueue<>(MAX_WAITING)) .build(); log.info("\n初始化线程池\n默认初始线程数:{}\n最大线程数:{}\n最大等待线程数:{}", DEFAULT_NUM, MAX_NUM, MAX_WAITING); return this.executor; } }
数据缓存到 Redis
刷新时将数据保存到 Redis 中
这里使用 多线程 + 闭锁 的方式同步进行两方的处理,闭锁保证两个线程都完成后才继续执行,返回前端成功
/** * 刷新国际化配置缓存 * @return */ @ApiOperation("刷新国际化配置缓存") @GetMapping(value = "/refresh") public ErrorMsg<Map<String, Object>> refresh() throws InterruptedException { // 利用闭锁保证两个线程都执行完毕后返回 final CountDownLatch countDownLatch = new CountDownLatch(2); // 前端国际化数据,多线程处理 ExecutorConfig.executor.execute(new Runnable(() -> { try { // 构建前端所需的数据格式 Map<String, Object> languageMap = buildLangToWeb(); // 保存前端国际化部分数据 Map redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_ZONE, languageMap); } finally { countDownLatch.countDown(); } })); // 后端国际化数据,多线程处理 ExecutorConfig.executor.execute(new Runnable(() -> { try { // 获取后端所需的数据列表 List<MultiLanguage> languageList = buildLangToJava(); // 保存后端国际化部分数据 List redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_JAVA, languageList); } finally { countDownLatch.countDown(); } })); // 闭锁阻塞 countDownLatch.await(); return ErrorMsg.SUCCESS; }
项目启动初始化国际化数据
// 开启语言翻译 @EnableTranslation public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); } @Autowired private MultiLanguageController multiLanguageController; @Bean public CommandLineRunner runner() { return args -> { log.info("开始初始化国际化数据:{}", new Date()); multiLanguageController.refresh(); log.info("国际化初始化完成:{}", new Date()); }; } }
效果展示
中文返回
英文返回
-
java国际化i18n
2015-11-03 15:24:38java 国际化 i18n test -
java国际化i8n
2016-09-21 11:39:16本Demo使用配置文件实现java语言的国际化,没有过多的花哨,简易明了,稍微知道点java语言的同学都可以看得明白,有不明白的地方可随时留言, -
详解Java中用于国际化的locale类
2020-09-02 05:56:15Java中也有用于转换和划分地区的国际化类java.lang.Locale,国际化在程序中设置语言和时间等时非常有用,下面我们就来详解Java中用于国际化的locale类 -
JAVA国际化文件生成工具
2011-06-10 15:01:31多语言国际化资源文件生成工具,支持JAVA,DEPGLI等 -
java 实现国际化 中英文语言切换
2013-11-29 11:25:42java实现国际化中英文语言切换 java语言切换JSP国际化 java实现国际化中英文语言切换 java语言切换JSP国际化 -
java国际化中文乱码问题解决包
2012-12-19 18:41:35java国际化中文乱码问题解决包ResourceBundleEditor_v0.8.0.zip -
JAVA国际化
2012-12-15 12:29:56JAVA WEB国际化代码,能够使用英语和汉语,JAVA WEB国际化代码,能够使用英语和汉语,JAVA WEB国际化代码,能够使用英语和汉语, -
java实现国际化I18N简单实例,没用任何框架.zip
2018-07-16 11:37:33java实现国际化I18N简单实例,没用任何框架.zip java实现国际化I18N简单实例,没用任何框架.zip -
最简单的java国际化例子
2011-10-21 15:37:18最简单的java国际化例子最简单的java国际化例子最简单的java国际化例子 -
Java国际化及Spring国际化解决方法
2015-11-03 15:08:55假设我们正在开发一个支持多国语言的Web应用程序,要求系统能够根据客户端的系统的语言类型返回对应的界面:英文的操作系统返回英文界面,而中文的操作系统则返回中文界面——这便是典型的i18n国际化问题。...假设我们正在开发一个支持多国语言的Web应用程序,要求系统能够根据客户端的系统的语言类型返回对应的界面:英文的操作系统返回英文界面,而中文的操作系统则返回中文界面——这便是典型的i18n国际化问题。
国际化(internationalization)又称为 i18n(读法为i 18 n,据说是因为internationalization(国际化)这个单词从i到n之间有18个英文字母,i18n的名字由此而来)。
对于有国际化要求的应用系统,我们不能简单地采用硬编码的方式编写用户界面信息、报错信息等内容,而必须为这些需要国际化的信息进行特殊处理。简单来说,就是为每种语言提供一套相应的资源文件,并以规范化命名的方式保存在特定的目录中,由系统自动根据客户端语言选择适合的资源文件。基础知识
“国际化信息”也称为“本地化信息”,一般需要两个条件才可以确定一个特定类型的本地化信息,它们分别是“语言类型”和“国家/地区的类型”。如中文本地化信息既有中国大陆地区的中文,又有中国台湾、中国香港地区的中文,还有新加坡地区的中文。Java通过java.util.Locale类表示一个本地化对象,它允许通过语言参数和国家/地区参数创建一个确定的本地化对象。
语言参数使用ISO标准语言代码表示,这些代码是由ISO-639标准定义的,每一种语言由两个小写字母表示。在许多网站上都可以找到这些代码的完整列表,下面的网址是提供了标准语言代码的信息:http://www.loc.gov/standards/iso639-2/php/English_list.php。
国家/地区参数也由标准的ISO国家/地区代码表示,这些代码是由ISO-3166标准定义的,每个国家/地区由两个大写字母表示。用户可以从以下网址查看ISO-3166的标准代码:http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html。
表5-2给出了一些语言和国家/地区的标准代码:
Locale
java.util.Locale是表示语言和国家/地区信息的本地化类,它是创建国际化应用的基础。下面给出几个创建本地化对象的示例:
//①带有语言和国家/地区信息的本地化对象 Locale locale1 = new Locale("zh","CN"); //②只有语言信息的本地化对象 Locale locale2 = new Locale("zh"); //③等同于Locale("zh","CN") Locale locale3 = Locale.CHINA; //④等同于Locale("zh") Locale locale4 = Locale.CHINESE; //⑤获取本地系统默认的本地化对象 Locale locale5= Locale.getDefault();
用户既可以同时指定语言和国家/地区参数定义一个本地化对象①,也可以仅通过语言参数定义一个泛本地化对象②。Locale类中通过静态常量定义了一些常用的本地化对象,③和④处就直接通过引用常量返回本地化对象。此外,用户还可以获取系统默认的本地化对象,如⑤所示。
在测试时,如果希望改变系统默认的本地化设置,可以在启动JVM时通过命令参数指定:java -Duser.language=en -Duser.region=US MyTest。本地化工具类
JDK的java.util包中提供了几个支持本地化的格式化操作工具类:NumberFormat、DateFormat、MessageFormat。下面,我们分别通过实例了解它们的用法:
NumberFormat:Locale locale = new Locale("zh", "CN"); NumberFormat currFmt = NumberFormat.getCurrencyInstance(locale); double amt = 123456.78; System.out.println(currFmt.format(amt));
上面的实例通过NumberFormat按本地化的方式对货币金额进行格式化操作,运行实例,输出以下信息:¥123,456.78
DateFormat:
Locale locale = new Locale("en", "US"); Date date = new Date(); DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); System.out.println(df.format(date));
通过DateFormat#getDateInstance(int style,Locale locale)方法按本地化的方式对日期进行格式化操作。该方法第一个入参为时间样式,第二个入参为本地化对象。运行以上代码,输出以下信息:
Jan 8, 2007
MessageFormat在NumberFormat和DateFormat的基础上提供了强大的占位符字符串的格式化功能,它支持时间、货币、数字以及对象属性的格式化操作。下面的实例演示了一些常见的格式化功能:
MessageFormat://①信息格式化串 String pattern1 = "{0},你好!你于 {1} 在工商银行存入 {2} 元。"; String pattern2 = "At {1,time,short} On {1,date,long},{0} paid {2,number, currency}."; //②用于动态替换占位符的参数 Object[] params = {"John", new GregorianCalendar().getTime(), 1.0E3}; //③使用默认本地化对象格式化信息 String msg1 = MessageFormat.format(pattern1, params); //④使用指定的本地化对象格式化信息 MessageFormat mf = new MessageFormat(pattern2, Locale.US); String msg2 = mf.format(params); System.out.println(msg1); System.out.println(msg2);
pattern1是简单形式的格式化信息串,通过{n}占位符指定动态参数的替换位置索引,{0}表示第一个参数,{1}表示第二个参数,以此类推。
pattern2格式化信息串比较复杂一些,除参数位置索引外,还指定了参数的类型和样式。从pattern2中可以看出格式化信息串的语法是很灵活的,一个参数甚至可以出现在两个地方:如 {1,time,short}表示从第二个入参中获取时间部分的值,显示为短样式时间;而{1,date,long}表示从第二个入参中获取日期部分的值,显示为长样式时间。关于MessageFormat更详细的使用方法,请参见JDK的Javadoc。
在②处,定义了用于替换格式化占位符的动态参数,这里,我们使用到了JDK5.0自动装包的语法,否则必须采用封装类表示基本类型的参数值。
在③处,通过MessageFormat的format()方法格式化信息串。它使用了系统默认的本地化对象,由于我们是中文平台,因此默认为Locale.CHINA。而在④处,我们显式指定MessageFormat的本地化对象。
运行上面的代码,输出以下信息:John,你好!你于 14-7-7 下午11:29 在工商银行存入 1,000 元。 At 11:29 PM On July 7, 2014,John paid $1,000.00.
如果应用系统中某些信息需要支持国际化功能,则必须为希望支持的不同本地化类型分别提供对应的资源文件,并以规范的方式进行命名。国际化资源文件的命名规范规定资源名称采用以下的方式进行命名:
<资源名><语言代码><国家/地区代码>.properties
其中,语言代码和国家/地区代码都是可选的。<资源名>.properties命名的国际化资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。
<资源名>_<语言代码>.properties命名的国际化资源文件是某一语言默认的资源文件,即某个本地化类型在系统中找不到精确匹配的资源文件,将采用相应语言默认的资源文件。
举一个例子:假设资源名为resource,则语言为英文,国家为美国,则与其对应的本地化资源文件命名为resource_en_US.properties。信息在资源文件以属性名/值的方式表示:greeting.common=How are you! greeting.morning = Good morning! greeting.afternoon = Good Afternoon!
对应语言为中文,国家/地区为中国大陆的本地化资源文件则命名为resource_zh_ CN.properties,资源文件内容如下:
greeting.common=\u60a8\u597d\uff01 greeting.morning=\u65e9\u4e0a\u597d\uff01 greeting.afternoon=\u4e0b\u5348\u597d\uff01
本地化不同的同一资源文件,虽然属性值各不相同,但属性名却是相同的,这样应用程序就可以通过Locale对象和属性名精确调用到某个具体的属性值了。
读者可能已经注意到,上面中文的本地化资源文件内容采用了特殊的编码表示中文字符,这是因为资源文件对文件内容有严格的要求:只能包含ASCII字符。所以必须将非ASCII字符的内容转换为Unicode代码的表示方式。如上面中文的resource_zh_CN.properties资源文件的三个属性值分别是“您好!”、“早上好!”和“下午好!”三个中文字符串对应的Unicode代码串。
如果在应用开发时,直接采用Unicode代码编辑资源文件是很不方便的,所以,通常我们直接使用正常的方式编写资源文件,在测试或部署时再采用工具进行转换。JDK在bin目录下为我们提供了一个完成此项功能的native2ascii工具,它可以将中文字符的资源文件转换为Unicode代码格式的文件,命令格式如下:
native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]
resource_zh_CN.properties包含中文字符并且以UTF-8进行编码,假设将该资源文件放到d:\目录下,通过下面的命令就可以将其转换为Unicode代码的形式:D:\>native2ascii -encoding utf-8 d:\resource_zh_CN.properties d:\resource_zh_CN_1.properties
由于原资源文件采用UTF-8编码,所以必须显式通过-encoding指定编码格式。
通过native2ascii命令手工转换资源文件,不但在操作上不方便,转换后资源文件中的属性内容由于采用了ASCII编码,阅读起来也不方便。很多IDE开发工具都有属性编辑器的插件,插件会自动将资源文件内容转换为ASCII形式的编码,同时以正常的方式阅读和编辑资源文件的内容,这给开发和维护带来了很大的便利。
对于MyEclipse来说,使用MyEclipse Properties Editor编辑资源属性文件;
对于Intellij IDEA来说,无须安装任何插件就自然支持资源属性文件的这种编辑方式了。
如果应用程序中拥有大量的本地化资源文件,直接通过传统的File操作资源文件显然太过笨拙。Java为我们提供了用于加载本地化资源文件的方便类java.util.ResourceBoundle。ResourceBoundle为加载及访问资源文件提供便捷的操作,下面的语句从相对于类路径的目录中加载一个名为resource的本地化资源文件:
ResourceBundle rb = ResourceBundle.getBundle("com/baobaotao/i18n/resource", locale)
通过以下的代码即可访问资源文件的属性值:
rb.getString("greeting.common")
来看下面的实例:
ResourceBundle rb1 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.US); ResourceBundle rb2 = ResourceBundle.getBundle("com/baobaotao/i18n/resource", Locale.CHINA); System.out.println("us:"+rb1.getString("greeting.common")); System.out.println("cn:"+rb2.getString("greeting.common"));
rb1加载了对应美国英语本地化的resource_en_US.properties资源文件;而rb2加载了对应中国大陆中文的resource_zh_CN.properties资源文件。运行上面的代码,将输出以下信息:
us:How are you! cn:你好!
加载资源文件时,如果不指定本地化对象,将使用本地系统默认的本地化对象。所以,在中文系统中,ResourceBundle.getBundle(“com/baobaotao/i18n/resource”)语句也将返回和代码清单5-14中rb2相同的本地化资源。
ResourceBundle在加载资源时,如果指定的本地化资源文件不存在,它按以下顺序尝试加载其他的资源:本地系统默认本地化对象对应的资源→默认的资源。
上面的例子中,假设我们使用ResourceBundle.getBundle(“com/baobaotao/i18n/resource”,Locale.CANADA)加载资源,由于不存在resource_en_CA.properties资源文件,它将尝试加载resource_zh_CN.properties的资源文件,假设resource_zh_CN.properties资源文件也不存在,它将继续尝试加载resource.properties的资源文件,如果这些资源都不存在,将抛出java.util.MissingResourceException异常。在资源文件中使用格式化串
在上面的资源文件中,属性值都是一般的字符串,它们不能结合运行时的动态参数构造出灵活的信息,而这种需求是很常见的。要解决这个问题很简单,只须使用带占位符的格式化串作为资源文件的属性值并结合使用MessageFormat就可以满足要求了。
上面的例子中,我们仅向用户提供一般性问候,下面我们对资源文件进行改造,通过格式化串让问候语更具个性化:greeting.common=How are you!{0},today is {1} greeting.morning = Good morning!{0},now is {1 time short} greeting.afternoon = Good Afternoon!{0} now is {1 date long}
将该资源文件保存在fmt_resource_en_US.properties中,按照同样的方式编写对应的中文本地化资源文件fmt_resource_zh_CN.properties。
下面,我们联合使用ResourceBoundle和MessageFormat得到美国英文的本地化问候语://①加载本地化资源 ResourceBundle rb1 = ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.US); ResourceBundle rb2 = ResourceBundle.getBundle("com/baobaotao/i18n/fmt_ resource",Locale.CHINA); Object[] params = {"John", new GregorianCalendar().getTime()}; String str1 = new MessageFormat(rb1.getString("greeting.common"),Locale.US).format(params); String str2 =new MessageFormat(rb2.getString("greeting.morning"),Locale.CHINA).format(params); String str3 =new MessageFormat(rb2.getString("greeting.afternoon"),Locale.CHINA).format(params); System.out.println(str1); System.out.println(str2); System.out.println(str3);
运行以上的代码,将输出以下信息:
How are you!John,today is 1/9/07 4:11 PM 早上好!John,现在是下午4:11 下午好!John,现在是2007年1月9日
MessageSource
Spring定义了访问国际化信息的MessageSource接口,并提供了几个易用的实现类。首先来了解一下该接口的几个重要方法:
String getMessage(String code, Object[] args, String defaultMessage, Locale locale) code表示国际化资源中的属性名;args用于传递格式化串占位符所用的运行期参数;当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;locale表示本地化对象;String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。
MessageSource的类结构
MessageSource分别被HierarchicalMessageSource和ApplicationContext接口扩展,这里我们主要看一下HierarchicalMessageSource接口的几个实现类,如图5-7所示:
HierarchicalMessageSource接口添加了两个方法,建立父子层级的MessageSource结构,类似于前面我们所介绍的HierarchicalBeanFactory。该接口的setParentMessageSource (MessageSource parent)方法用于设置父MessageSource,而getParentMessageSource()方法用于返回父MessageSource。HierarchicalMessageSource接口最重要的两个实现类是ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。它们基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源。ReloadableResourceBundleMessageSource提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息。StaticMessageSource主要用于程序测试,它允许通过编程的方式提供国际化信息。而DelegatingMessageSource是为方便操作父MessageSource而提供的代理类。
ResourceBundleMessageSource
该实现类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。在前面的代码清单中,我们通过JDK的基础类完成了本地化的操作,下面我们使用ResourceBundleMessageSource来完成相同的任务。读者可以比较两者的使用差别,并体会Spring所提供的国际化处理功能所带给我们的好处:
通过ResourceBundleMessageSource配置资源<bean id="myResource" class="org.springframework.context.support.ResourceBundleMessageSource"> <!--①通过基名指定资源,相对于类根路径--> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property>
启动Spring容器,并通过MessageSource访问配置的国际化资源,如下代码清单所示:
访问国际化消息:ResourceBundleMessageSource:String[] configs = {"com/baobaotao/i18n/beans.xml"}; ApplicationContext ctx = new ClassPathXmlApplicationContext(configs); //①获取MessageSource的Bean MessageSource ms = (MessageSource)ctx.getBean("myResource"); Object[] params = {"John", new GregorianCalendar().getTime()}; //②获取格式化的国际化信息 String str1 = ms.getMessage("greeting.common",params,Locale.US); String str2 = ms.getMessage("greeting.morning",params,Locale.CHINA); String str3 = ms.getMessage("greeting.afternoon",params,Locale.CHINA); System.out.println(str1); System.out.println(str2); System.out.println(str3);
比较代码清单中的代码,我们发现最主要的区别在于我们无须再分别加载不同语言、不同国家/地区的本地化资源文件,仅仅通过资源名就可以加载整套的国际化资源文件。此外,我们无须显式使用MessageFormat操作国际化信息,仅通过MessageSource# getMessage()方法就可以完成操作了。这段代码的运行结果与前面的代码的运行结果完全一样。
ReloadableResourceBundleMessageSource
前面,我们提到该实现类比之于ResourceBundleMessageSource的唯一区别在于它可以定时刷新资源文件,以便在应用程序不重启的情况下感知资源文件的变化。很多生产系统都需要长时间持续运行,系统重启会给运行带来很大的负面影响。这时,通过该实现类就可以解决国际化信息更新的问题。请看下面的配置:
通过ReloadableResourceBundleMessageSource配置资源:<bean id="myResource" lass="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property> <!--① 刷新资源文件的周期,以秒为单位--> <property name="cacheSeconds" value="5"/> </bean>
在上面的配置中,我们通过cacheSeconds属性让ReloadableResourceBundleMessageSource每5秒钟刷新一次资源文件(在真实的应用中,刷新周期不能太短,否则频繁的刷新将带来性能上的负面影响,一般不建议小于30分钟)。cacheSeconds默认值为-1表示永不刷新,此时,该实现类的功能就蜕化为ResourceBundleMessageSource的功能。
我们编写一个测试类对上面配置的ReloadableResourceBundleMessageSource进行测试:
String[] configs = {"com/baobaotao/i18n/beans.xml"}; ApplicationContext ctx = new ClassPathXmlApplicationContext(configs); MessageSource ms = (MessageSource)ctx.getBean("myResource"); Object[] params = {"John", new GregorianCalendar().getTime()}; for (int i = 0; i < 2; i++) { String str1 = ms.getMessage("greeting.common",params,Locale.US); System.out.println(str1); Thread.currentThread().sleep(20000); //①模拟程序应用,在此期间,我们更改资源文件 }
在①处,我们让程序睡眠20秒钟,在这期间,我们将fmt_resource_zh_CN.properties资源文件的greeting.common键值调整为:---How are you!{0},today is {1}---
我们将看到两次输出的格式化信息分别对应更改前后的内容,也即本地化资源文件的调整被自动生效了:
How are you!John,today is 1/9/07 4:55 PM ---How are you!John,today is 1/9/07 4:55 PM---
容器级的国际化信息资源
在如图5-7所示的MessageSource类图结构中,我们发现ApplicationContext实现了MessageSource的接口。也就是说ApplicationContext的实现类本身也是一个MessageSource对象。
将ApplicationContext和MessageSource整合起来,乍一看挺让人费解的,Spring这样设计的意图究竟是什么呢?原来Spring认为:在一般情况下,国际化信息资源应该是容器级。我们一般不会将MessageSource作为一个Bean注入到其他的Bean中,相反MessageSource作为容器的基础设施向容器中所有的Bean开放。只要我们考察一下国际化信息的实际消费场所就更能理解Spring这一设计的用意了。国际化信息一般在系统输出信息时使用,如Spring MVC的页面标签,控制器Controller等,不同的模块都可能通过这些组件访问国际化信息,因此Spring就将国际化消息作为容器的公共基础设施对所有组件开放。
既然一般情况下我们不会直接通过引用MessageSource Bean使用国际信息,那如何声明容器级的国际化信息呢?我们其实在5.1.1节讲解Spring容器的内部工作机制时已经埋下了伏笔:在介绍容器启动过程时,我们通过代码清单5-1对Spring容器启动时的步骤进行剖析,④处的initMessageSource()方法所执行的工作就是初始化容器中的国际化信息资源:它根据反射机制从BeanDefinitionRegistry中找出名称为“messageSource”且类型为org.springframework.context.MessageSource的Bean,将这个Bean定义的信息资源加载为容器级的国际化信息资源。请看下面的配置:
容器级资源的配置:<!--①注册资源Bean,其Bean名称只能为messageSource --> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>com/baobaotao/i18n/fmt_resource</value> </list> </property> </bean>
下面,我们通过ApplicationContext直接访问国际化信息,如下代码清单所示:
String[] configs = {"com/baobaotao/i18n/beans.xml"}; ApplicationContext ctx = new ClassPathXmlApplicationContext(configs); //①直接通过容器访问国际化信息 Object[] params = {"John", new GregorianCalendar().getTime()}; String str1 = ctx.getMessage("greeting.common",params,Locale.US); String str2 = ctx.getMessage("greeting.morning",params,Locale.CHINA); System.out.println(str1); System.out.println(str2);
运行以上代码,输出以下信息:
How are you!John,today is 1/9/07 5:24 PM 早上好!John,现在是下午5:24
假设MessageSource Bean名字没有命名为“messageSource”,以上代码将抛出NoSuchMessageException异常。
-
Java Web国际化及乱码解决方案
2019-09-27 08:39:24一、背景 项目过程中,难免需要提示中文或者英文提示信息,有了国际化,方便切换; 实际项目中,一般都不允许直接把中文提示信息写在代码中,避免其他国家程序猿看不懂(国际化...Java SDK自带国际化API:java.uti...一、背景
- 项目过程中,难免需要提示中文或者英文提示信息,有了国际化,方便切换;
- 实际项目中,一般都不允许直接把中文提示信息写在代码中,避免其他国家程序猿看不懂(国际化公司和开源项目涉及),也容易招来其他国家的恶意攻击。比如菊花公司把中文写在代码中就算成非常严重的违规。
二、目标
- 在项目中引入简单易用的国际化框架,方便同事使用。
三、步骤
- Java SDK自带国际化API:java.util.ResourceBundle,使用也特别简单,目前就选定使用它;
- 对应的国际化资源目录结构如下:
说明下:我这里是采用maven作为代码构建工具,资源默认路径是在src/main/resources,编译后对应路径为$bin/classes目录。$bin表示代码生成class的根路径。$bin在maven中实际默认为:与src同级的target目录。 - 了解了下ResourceBundle的用法,编写测试类:
import org.junit.Test; import java.io.UnsupportedEncodingException; import java.util.Locale; import java.util.ResourceBundle; import static org.junit.Assert.assertTrue; public class I18nTest { @Test public void testI18n() throws UnsupportedEncodingException { Locale locale = Locale.getDefault(); System.out.println("current locale:" + locale.toString()); ResourceBundle bundle = ResourceBundle.getBundle("i18n/tips", locale); String msg = bundle.getString("test.failed"); System.out.println("current msg:" + msg); assertTrue(null != msg); } }
其中ResourceBundle.getBundle的第一个参数叫baseName,就是相对于classes目录的文件路径,因为涉及国际化,所以不能带上国家(区域)后缀,也不需要带上porperties这个文件类型后缀。记住baseName是相对路径,不要在i18n/tips前面加上‘/’。
4. 国家(区域)后缀有标准的定义。也有网友总结的参照表。
5. 执行上述测试代码过程中,发现输入中文时,会出现乱码。原因:IDE工具默认的字符编码不是UTF-8。建议把IDE的所有文件类型编码都设置成UTF-8,同时设置properties内容转换为ASCII码。我使用的是IDEA,设置如下:
6. 设置后,会发现tips_zh_CN.properties中文展示如下:
7. 再次执行,乱码消失了。
8. 验证通过后,封装对应的工具类:public final class I18nUtils { /** * 获取国际化值 * * @param key * @return */ public static String get(String key) { String value = BUNDLE.getString(key); if (StringUtils.isEmpty(value)) { value = StringUtils.EMPTY; } LOGGER.info("i18n:[{}]={}", key, value); return value; } private static final Logger LOGGER = LogManager.getLogger(I18nUtils.class); //国际化文件路径 private static final String I18N_FILE = "i18n/tips"; //国际化bundle private static ResourceBundle BUNDLE; static { BUNDLE = ResourceBundle.getBundle(I18N_FILE, Locale.getDefault()); } private I18nUtils() { } }
- 在springboot框架下使用该工具类给页面返回中文结果时,发现还是存在乱码。springboot的application.properties编码配置如下:
server.tomcat.uri-encoding=UTF-8 spring.banner.charset=UTF-8 spring.messages.encoding=UTF-8 spring.http.encoding.charset=UTF-8 spring.http.encoding.force=true spring.http.encoding.enabled=true
给前台返回结果的代码(过滤器)如下:
Writer writer = response.getWriter(); String failedMsg = I18nUtils.get(“test.failed”); String failedJson = JsonUtils.toJson(failedMsg); writer.write(failedJson); writer.flush();
- 刚开始考虑还有没有什么编码设置给遗漏了,包括是不是springboot集成的springmvc部件没有指定编码,结果发现根本不起作用。后面只能怀疑是response响应数据时,没有指定编码。
- 在上述代码前加上ContentType设置(“application/json;utf-8”):
response.setContentType(ContentType.APPLICATION_JSON.toString());
问题解决。
四、总结
- 在这么多年的Java Web项目中,碰到了很多次乱码的问题,而且每次问题都还不一样,解决方案也不一样T_T;
- Java自带的国际化工具比想象的好用;
- 网上关于Java自带国际化的使用大多只说了部分问题和部分代码,基本上没有看到像我这样完整写出来的,希望对你有帮助;
-
Java 国际化问题
2009-08-21 08:49:02Java 国际化 Java 国际化 Java 国际化 -
java实现i18n国际化
2021-01-29 17:40:33internationalization (国际化)简称 i18n,因为在i和n之间还有18个字符,localization(本地化 ),简称L10n。 一般用语言_地区的形式表示一种语言,如 zh_CN, zh_TW. 常见的有: zh_cn: 简体中文 zh_hk: 繁体中文... -
跟我学Java入门到精通培训教程——第5章Java国际化技术及应用实例.doc
2020-10-12 19:24:44跟我学Java入门到精通培训教程第5章Java国际化技术及应用实例 教学目标 在本讲中希望您能掌握和了解如下知识点Java中如何解决国际化的问题实现国际化程序的基本思路Java中与国际化相关的包与类利用資源字串实现国际... -
深入理解Java国际化
2014-07-09 23:26:25假设我们正在开发一个支持多国语言的Web应用程序,要求系统能够根据客户端的系统的语言类型返回对应的界面:英文的操作系统返回英文界面,而中文的操作系统则返回中文界面——这便是典型的i18n国际化问题。... -
Java国际化处理
2018-11-22 23:14:38最近在做Kettle8.1的国际化工作,闲暇之余,就看了看Java的国际化处理,明白程序怎么样找到对应的国际化文件。 说到国际化,经常看到一个东西叫i18n,其实是internationalization的缩写(ps:以后起昵称什么的就... -
java国际化之时区问题处理
2019-05-08 10:27:59在国际化的项目中需要处理的日期时间问题主要有两点: 1、日期时间的国际化格式问题处理; 2、日期时间的时区问题处理,这两个问题要区分开,不要弄混了。 日期时间国际化化格式处理 对应的关键词:Locale ... -
Java 中的国际化
2018-08-06 17:46:45国际化 ,英文叫 internationalization 单词太长 ,又被简称为 i18n(取头取尾中间有18个字母)不经大声呼喊 ,这都行 !接着看什么是国际化 , 国际化是指让产品或是程序在无需做出...在 Java 中实现国际化主要是借... -
Java web 国际化
2010-11-03 18:37:26Java web 国际化 -
java国际化自定义MessageSource
2018-03-20 10:35:52spring boot国际化的东西就不多说,百度粘贴复制的的东西一大堆,不知道谁是原创,这里贴出我能找到的最早的博文链接供大家学习: 58. Spring Boot国际化(i18n)【从零开始学Spring Boot】本文仅是自定义... -
Java国际化(i18n)字符串与Unicode转换
2021-03-18 10:25:18在java中,文本是以Unicode格式内部存储的。 如果输入/输出格式不同,则需要转换。转换以下示例将展示将Unicode字符串转换为UTF8字节,以及将UTF8字节转为Unicode字节转换。文件:IOTester.java -import java.io.... -
Java 国际化操作
2010-09-10 11:44:59Java 国际化操作Java 国际化操作Java 国际化操作Java 国际化操作Java 国际化操作Java 国际化操作Java 国际化操作