Java/JSP界面实现多国语言支持,支持插入变量,还要考虑名词单复数

在Java/JSP中,通常使用.properties文件定义各语言的文本,里面可以用{0},{1},{2}表示待插入的变量值(之所以用数字,不用%s、%d等占位符,是因为不同语言的语序不同)。
用java.util.ResourceBundle类的ResourceBundle.getBundle方法读取.properties文件。
用java.text.MessageFormat类替换{0},{1},{2}等占位符。在字符串中,{和}字符本身用'{和}'表示(在前面或后面加单引号),单引号本身用双单引号''表示。
用java.text.ChoiceFormat实现名词单复数的功能。中文没有名词的数的概念,但英语分单复数,阿拉伯语的名词还分为单数、双数和复数。
默认情况下,Java里面中文数字是有千分隔符的,但我们中国人的真实习惯是不显示千分隔符。需要在NumberFormat类里面用setGroupingUsed(false)关闭千分隔符的显示。
这几个类都是Java默认支持的类,不需要额外导入其他jar包。

Java官网关于处理单复数(Handling Plurals)的文档:
https://docs.oracle.com/javase/tutorial/i18n/format/choiceFormat.html

第一节 在文本中替换变量占位符,并处理名词单复数

import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Locale;

public class Test {

	public static void main(String[] args) {
		test(getChineseMessage());
		test(getEnglishMessage());
	}
	
	public static MessageFormat getChineseMessage() {
		// 中文没有单复数
		MessageFormat msgfmt = new MessageFormat("{1}有{0}个文件。");
		NumberFormat nf = NumberFormat.getInstance(Locale.SIMPLIFIED_CHINESE);
		nf.setGroupingUsed(false); // 显示数字时不显示千分隔符
		msgfmt.setFormatByArgumentIndex(0, nf);
		return msgfmt;
	}
	
	public static MessageFormat getEnglishMessage() {
		// 英文有单复数之分
		double[] pluralRule = {0, 1, ChoiceFormat.nextDouble(1)};
		String[] choices = {"are no files", "is one file", "are {0} files"};
		
		MessageFormat msgfmt = new MessageFormat("There {0} on {1}.");
		ChoiceFormat cf = new ChoiceFormat(pluralRule, choices);
		msgfmt.setFormatByArgumentIndex(0, cf);
		return msgfmt;
	}
	
	public static void test(MessageFormat msgfmt) {
		Object[] values = {1, "A"};
		String str = msgfmt.format(values);
		System.out.println(str);
		
		values = new Object[] {1.01, "B"};
		str = msgfmt.format(values);
		System.out.println(str);
		
		values[0] = 12345;
		str = msgfmt.format(values);
		System.out.println(str);
		
		values[0] = 0;
		str = msgfmt.format(values);
		System.out.println(str);
		
		values[0] = -1;
		str = msgfmt.format(values);
		System.out.println(str);
	}

}

程序运行结果:

A有1个文件。
B有1.01个文件。
B有12345个文件。
B有0个文件。
B有-1个文件。
There is one file on A.
There are 1.01 files on B.
There are 12,345 files on B.
There are no files on B.
There are no files on B.

提示:数字去掉千分隔符,有一个简便方法,那就是在{}里面追加“,number,0”。比如把{0}改写成{0,number,0},千分隔符就没了。 

第二节 从.properties文件中读取多语言文本

新建两个.properties文件:haha_en_US.properties和haha_zh_CN.properties。
这两个文件要放到src文件夹里面。

haha_en_US.properties:

test.msg = There {0} on {1}.
test.msg.c00 = are no files
test.msg.c01 = is one file
test.msg.c02 = are {0} files

haha_zh_CN.properties:

请注意在.properties文件里面,所有的汉字都需要ISO-8859-1转义。

test.msg = {1}\u6709{0}\u4E2A\u6587\u4EF6\u3002

测试代码:

import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.ResourceBundle;

public class Test2 {

	public static void main(String[] args) {
		// 定义参数
		Object[] values = {12345678, "A"};
		
		// 显示简体中文文本
		ResourceBundle res = ResourceBundle.getBundle("haha", Locale.SIMPLIFIED_CHINESE);
		String fmt = res.getString("test.msg");
		MessageFormat mfmt = new MessageFormat(fmt);
		NumberFormat nf = NumberFormat.getInstance(Locale.SIMPLIFIED_CHINESE);
		nf.setGroupingUsed(false); // 显示数字时不显示千分隔符
		mfmt.setFormatByArgumentIndex(0, nf);
		String str = mfmt.format(values);
		System.out.println(str);
		
		// 显示英语(美国)文本
		res = ResourceBundle.getBundle("haha", Locale.US);
		fmt = res.getString("test.msg");
		mfmt = new MessageFormat(fmt);
		double[] pluralRule = {0, 1, ChoiceFormat.nextDouble(1)}; // 0、单数、复数判定方法
		String[] fmtList0 = new String[3]; // 0、单数、复数对应的文本
		fmtList0[0] = res.getString("test.msg.c00");
		fmtList0[1] = res.getString("test.msg.c01");
		fmtList0[2] = res.getString("test.msg.c02");
		ChoiceFormat cf = new ChoiceFormat(pluralRule, fmtList0);
		mfmt.setFormatByArgumentIndex(0, cf);
		str = mfmt.format(values);
		System.out.println(str);
	}

}

程序运行结果:

A有12345678个文件。
There are 12,345,678 files on A.

第三节 在MVC视图层中用jstl fmt标签显示多国语言文字

在MVC架构中,通常V(View,视图层)由jsp页面充当,jsp页面中含有很多代替变量的jstl标签。C(Controler,控制层)由Servlet充当,负责给jstl标签代替的变量赋值,并处理页面中的表单提交。M(Model)是数据层,也是持久层,用来访问数据库中的数据。
如果不考虑单复数的话,在视图层里面用标准JSTL标签库的fmt标签就可以完成多国语言文字的显示和变量的插入,不需要控制层参与。

把jstl标签库的4个jar包放到WebContent/WEB-INF/lib里面。(下载地址:Apache Taglibs - Apache Standard Taglib: JSP[tm] Standard Tag Library (JSTL) implementations
我们在src下面建一个i18n_jsp_test的java包(package),把haha_en_US.properties和haha_zh_CN.properties放到i18n_jsp_test包里面。
再在WebContent下面建立一个名为hello.jsp的jsp页面。
(图中的Test.java、TestServlet.java和test.jsp与本节内容无关)

hello.jsp的内容如下:

<%@ page contentType="text/html; charset=utf-8" language="java" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<fmt:setLocale value="zh_CN" />
<fmt:setBundle basename="i18n_jsp_test.haha" var="myset" />
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><fmt:message key="test.hello" bundle="${myset}" /></title>
</head>

<body>
<fmt:message key="test.haha" bundle="${myset}" /><br />

<fmt:message key="test.msg" bundle="${myset}" /><br />

<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="1234567" />
  <fmt:param value="ABCD" />
</fmt:message><br />

<fmt:formatNumber value="12345678" type="number" var="price" />
<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="${price}" />
  <fmt:param value="ABCD" />
</fmt:message>
</body>
</html>

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>语句导入jstl fmt标签库。
fmt:setLocale标签用来设置当前页面使用的语言名称,zh_CN是简体中文。
fmt:setBundle标签用来读取properties文件,basename="i18n_jsp_test.haha"表示读取i18n_jsp_test包下面的haha_zh_CN.properties文件,请注意fmt:setLocale必须要放到fmt:setBundle前面才能生效。var="myset"表示后面可以用${myset}来引用这个properties文件。

fmt:message标签用来显示文本。
显示myset里面的test.hello文本,不替换任何{}占位符:
<fmt:message key="test.hello" bundle="${myset}" />
显示myset里面的test.hello文本,将{0}替换为1234567,将{1}替换为ABCD:
<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="1234567" />
  <fmt:param value="ABCD" />
</fmt:message><br />

将12345678数字本地化(也就是加上千分隔符)后,存到${price}变量中,然后替换{0}:
<fmt:formatNumber value="12345678" type="number" var="price" />
<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="${price}" />
  <fmt:param value="ABCD" />
</fmt:message>
type还可以为"currency"(货币格式)。

如果要显示的文本要放到HTML标签的属性里面(比如按钮文字),那就先在外面用<fmt:message>标签存到一个var变量里面,再在里面用${var}引用。
<fmt:message key="test.hello" bundle="${myset}" var="hellotext" />
<input type="submit" value="${hellotext}" />

src/i18n_jsp_test/haha_zh_CN.properties文件的内容:

test.hello = \u4F60\u597D\uFF0C\u4E16\u754C\uFF01
test.haha = \u54C8\u54C8\uFF01\uFF01\uFF01\uFF01
test.msg = {1}\u6709{0}\u4E2A\u6587\u4EF6\u3002

程序运行结果:

第四节 在MVC控制层提前准备好要显示的多国语言文本

如果要考虑单复数的话,我们可以在控制层(Servlet)里面把要显示的带单复数的文本先准备好,通过request.setAttribute传递给视图层(jsp页面)显示。
控制层里面加载完properties文件后得到的ResourceBundle对象,如果视图层也想用(用来显示其他不带单复数的文本),也可以通过request.setAttribute传递过去,这样视图层就不用再加载一次并设置locale了。传递前要先包装成LocalizationContext对象,不然jstl fmt标签无法识别。

src/i18n_jsp_test/TestServlet.java:

package i18n_jsp_test;

import java.io.IOException;
import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.jstl.fmt.LocalizationContext;

/**
 * Servlet implementation class TestServlet
 */
public class TestServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * Default constructor. 
     */
    public TestServlet() {
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 定义参数
		String who = request.getParameter("who");
		int num = Integer.valueOf(request.getParameter("num"));
		Object[] values = {num, who};
		
		// 生成简体中文文本
		ResourceBundle resZhCN = ResourceBundle.getBundle("i18n_jsp_test.haha", Locale.SIMPLIFIED_CHINESE);
		String fmt = resZhCN.getString("test.msg");
		MessageFormat mfmt = new MessageFormat(fmt);
		NumberFormat nf = NumberFormat.getInstance(Locale.SIMPLIFIED_CHINESE);
		nf.setGroupingUsed(false); // 显示数字时不显示千分隔符
		mfmt.setFormatByArgumentIndex(0, nf);
		String str = mfmt.format(values);
		request.setAttribute("str1", str);
		
		// 生成英语(美国)文本
		ResourceBundle resEnUS = ResourceBundle.getBundle("i18n_jsp_test.haha", Locale.US);
		fmt = resEnUS.getString("test.msg");
		mfmt = new MessageFormat(fmt);
		double[] pluralRule = {0, 1, ChoiceFormat.nextDouble(1)}; // 0、单数、复数判定方法
		String[] fmtList0 = new String[3]; // 0、单数、复数对应的文本
		fmtList0[0] = resEnUS.getString("test.msg.c00");
		fmtList0[1] = resEnUS.getString("test.msg.c01");
		fmtList0[2] = resEnUS.getString("test.msg.c02");
		ChoiceFormat cf = new ChoiceFormat(pluralRule, fmtList0);
		mfmt.setFormatByArgumentIndex(0, cf);
		str = mfmt.format(values);
		request.setAttribute("str2", str);
		
		// 将其中一个properties文件拿给jsp页面的fmt标签使用
		// 想拿哪个都可以, 这里我们拿的是resEnUS
		LocalizationContext myset = new LocalizationContext(resEnUS);
		request.setAttribute("myset", myset);
		
		// 转发到视图层(jsp页面)
		RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/test.jsp");
		dispatcher.forward(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
	}

}

src/i18n_jsp_test/haha_en_US.properties:

test.hello = Hello World!
test.haha = haha!!!!
test.msg = There {0} on {1}.
test.msg.c00 = are no files
test.msg.c01 = is one file
test.msg.c02 = are {0} files

WebContent/WEB-INF/test.jsp:
请注意jsp页面不再用fmt:setLocale和fmt:setBundle标签重新加载properties文件,直接使用Servlet传过来的${myset}对象。
${str1}和${str2}是Servlet里面预先生成好的带单复数的文本。

<%@ page contentType="text/html; charset=utf-8" language="java" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><fmt:message key="test.hello" bundle="${myset}" /></title>
</head>

<body>
${str1}<br />
${str2}<br />
<br />
<fmt:message key="test.hello" bundle="${myset}" /><br />
<fmt:message key="test.haha" bundle="${myset}" /><br />
<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="1234567" />
  <fmt:param value="ABCD" />
</fmt:message><br />
<fmt:formatNumber value="12345678" type="number" var="price" />
<fmt:message key="test.msg" bundle="${myset}">
  <fmt:param value="${price}" />
  <fmt:param value="ABCD" />
</fmt:message>
</body>
</html>

程序运行结果:

第五节 没有翻译的文本显示成英文

有时我们有这样的需求:若某种语言忘记翻译了某个文本,则该项文本显示为英文文本。
假设资源名称为test,我们可以把英文文本全部写到test.properties里面,然后删除test_en_US.properties文件。
test_zh_CN.properties里面写中文文本,test_es_AR.properties里面写西班牙语文本。

[test.properties]
title = the days of the week
jan = January
feb = February
mar = March

[test_zh_CN.properties]
title = \u4E00\u5468\u7684\u6BCF\u4E00\u5929
jan = \u4E00\u6708
mar = \u4E09\u6708

[test_es_AR.properties]
title = los días de la semana
jan = enero
feb = febrero

英文文本是全的,放到test.properties里面。
中文文本放到test_zh_CN.properties里面,缺了二月的翻译。
西班牙文文本放到test_es_AR.properties里面,缺了三月的翻译。

Java测试代码:

package i18n_test2;

import java.util.Locale;
import java.util.ResourceBundle;

public class Test {

	public static void main(String[] args) {
		Locale.setDefault(Locale.US); // 如果getBundle找不到对应语言的文件, 则读取英文文件
		
		ResourceBundle res = ResourceBundle.getBundle("i18n_test2.test", Locale.SIMPLIFIED_CHINESE);
		System.out.println(res.getString("title") + ": " + res.getString("jan") + " " + res.getString("feb") + " " + res.getString("mar"));
		
		res = ResourceBundle.getBundle("i18n_test2.test", Locale.US);
		System.out.println(res.getString("title") + ": " + res.getString("jan") + " " + res.getString("feb") + " " + res.getString("mar"));
		
		res = ResourceBundle.getBundle("i18n_test2.test", Locale.forLanguageTag("es-ar"));
		System.out.println(res.getString("title") + ": " + res.getString("jan") + " " + res.getString("feb") + " " + res.getString("mar"));
		
		res = ResourceBundle.getBundle("i18n_test2.test", Locale.CANADA_FRENCH);
		System.out.println(res.getString("title") + ": " + res.getString("jan") + " " + res.getString("feb") + " " + res.getString("mar"));
	}
}

程序运行结果:

一周的每一天: 一月 February 三月
the days of the week: January February March
los días de la semana: enero febrero March
the days of the week: January February March

法语文件不存在,所以所有本文全显示成英文。

第六节 直接在模式字符串中定义数字和日期时间格式

在实际应用中,我们可以把所有的格式信息(包括自定义的格式)和单复数字符串全部写到相应语言的properties文件中,而Java程序里面直接调用可变参数的MessageFormat.format(fmt, arg1, arg2, ...)函数产生格式化后的字符串。每一种语言都可以在自己的properties文件中随意自定义格式。这也是笔者最推荐的做法。

import java.text.MessageFormat;
import java.util.Date;

public class Test {

	public static void main(String[] args) {
		String pattern = "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.";
		Date date = new Date();
		String str = MessageFormat.format(pattern, 7, date, "a disturbance in the Force");
		System.out.println(str);
		
		pattern = "The disk {1} contains {0} file(s)."; // 带千分隔符
		str = MessageFormat.format(pattern, 1273, "MyDisk");
		System.out.println(str);
		
		pattern = "The disk {1} contains {0,number,0} file(s)."; // 去除千分隔符
		str = MessageFormat.format(pattern, 1273, "MyDisk");
		System.out.println(str);
		str = MessageFormat.format(pattern, 0, "MyDisk");
		System.out.println(str);
		str = MessageFormat.format(pattern, 1048576, "MyDisk");
		System.out.println(str);
		
		pattern = "There {0,choice,0#are no files|1#is one file|1<are {0,number,0} files}.";
		System.out.println(MessageFormat.format(pattern, 0));
		System.out.println(MessageFormat.format(pattern, 1));
		System.out.println(MessageFormat.format(pattern, 10));
		System.out.println(MessageFormat.format(pattern, 1048576));
		System.out.println(MessageFormat.format(pattern, -1));
		
		pattern = "{0,date}; {0,date,short}; {0,date,long}; {0,date,full}";
		System.out.println(MessageFormat.format(pattern, date));
		pattern = "{0,time}; {0,time,short}; {0,time,long}; {0,time,full}; {0,time,HH:mm}";
		System.out.println(MessageFormat.format(pattern, date));
		pattern = "{0,date,yyyy-M-d HH:mm}"; // 这个格式很常用
		System.out.println(MessageFormat.format(pattern, date));
		
		// 官方资料请参阅: https://docs.oracle.com/javase/8/docs/api/java/text/MessageFormat.html
	}

}

程序运行结果:

At 21:28:39 on 2024-5-16, there was a disturbance in the Force on planet 7.
The disk MyDisk contains 1,273 file(s).
The disk MyDisk contains 1273 file(s).
The disk MyDisk contains 0 file(s).
The disk MyDisk contains 1048576 file(s).
There are no files.
There is one file.
There are 10 files.
There are 1048576 files.
There are no files.
2024-5-16; 24-5-16; 2024年5月16日; 2024年5月16日 星期四
21:28:39; 下午9:28; 下午09时28分39秒; 下午09时28分39秒 CST; 21:28
2024-5-16 21:28

第七节 直接把单复数形式的内容写到一个字符串里面

import java.text.MessageFormat;

public class Test4 {

	// 参考资料:
	// https://docs.oracle.com/javase/8/docs/api/java/text/MessageFormat.html
	// https://docs.oracle.com/javase/8/docs/api/java/text/ChoiceFormat.html
	public static void main(String[] args) {
		String msg = "There {0,choice,0#are no files|1#is one file|1<are {0} files} on {1}.";
		test(msg);
		msg = "{1}有{0,number,0}个文件。";
		test(msg);
	}
	
	public static void test(String msg) {
		System.out.println(MessageFormat.format(msg, 0, "my table"));
		System.out.println(MessageFormat.format(msg, 1, "his table"));
		System.out.println(MessageFormat.format(msg, 2, "her table"));
		System.out.println(MessageFormat.format(msg, 3, "these tables"));
		System.out.println(MessageFormat.format(msg, 1048576, "those tables"));
		System.out.println(MessageFormat.format(msg, -2, "the tree"));
	}

}

import java.text.MessageFormat;

public class Test4 {

	public static void main(String[] args) {
		// There be 就近原则
		String msg = "There {0,choice,1#is {0} topic|1<are {0} topics} and {1,choice,1#{1} post|1<{1} posts}.";
		test(msg);
	}
	
	public static void test(String msg) {
		System.out.println(MessageFormat.format(msg, 0, 0));
		System.out.println(MessageFormat.format(msg, 0.9, 10));
		System.out.println(MessageFormat.format(msg, 1, 5));
		System.out.println(MessageFormat.format(msg, 1.1, 1));
		System.out.println(MessageFormat.format(msg, 2, 0.5));
		System.out.println(MessageFormat.format(msg, 3, 0));
		System.out.println(MessageFormat.format(msg, 1048576, 1024));
		System.out.println(MessageFormat.format(msg, -2, 2048));
	}

}

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值