JSP 国际化
fmt 标签库的 setLocale、setBundle、message,以及资源包组织。
资源包
WEB-INF/classes/
├── messages.properties # 默认(英文)
├── messages_zh_CN.properties # 简体中文
└── messages_ja.properties # 日文# messages.properties
app.title=Demo App
nav.home=Home
nav.orders=Orders
welcome=Welcome, {0}!
cart.count=You have {0} items.# messages_zh_CN.properties(UTF-8,需在 JSP 里指定 basename 的读取方式或用转义)
app.title=演示应用
nav.home=首页
nav.orders=订单
welcome=欢迎,{0}!
cart.count=购物车里有 {0} 件商品。页面里使用
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%-- 按浏览器 Accept-Language 自动选,或手动指定 --%>
<fmt:setLocale value="${param.lang != null ? param.lang : pageContext.request.locale}"/>
<fmt:setBundle basename="messages"/>
<!DOCTYPE html>
<html><head><title><fmt:message key="app.title"/></title></head>
<body>
<nav>
<a href="/"><fmt:message key="nav.home"/></a>
<a href="/orders"><fmt:message key="nav.orders"/></a>
</nav>
<p><fmt:message key="welcome">
<fmt:param value="${sessionScope.userName}"/>
</fmt:message></p>
<p><fmt:message key="cart.count">
<fmt:param value="${fn:length(sessionScope.cart)}"/>
</fmt:message></p>
</body></html>(Accept-Language: zh-CN)
演示应用
首页 订单
欢迎,张三!
购物车里有 3 件商品。<fmt:param> fills {0}, {1} in order. The numeric placeholders need no escaping, matching MessageFormat.
中文 properties 的编码
<%-- 办法一:让 fmt:setBundle 按 UTF-8 读 --%>
<fmt:setBundle basename="messages" var="bundle"/>
<fmt:setLocale value="zh_CN"/>
<%-- 办法二:直接写 Unicode 转义(最兼容) --%>
<%-- welcome=\u6b22\u8fce\uff0c{0}\uff01 --%>
<%-- 办法三:用 ResourceBundle 显式按 UTF-8 读 --%>
<%
try (java.io.Reader r = new java.io.InputStreamReader(
application.getResourceAsStream("/WEB-INF/classes/messages_zh_CN.properties"),
java.nio.charset.StandardCharsets.UTF_8)) {
java.util.ResourceBundle rb = new java.util.PropertyResourceBundle(r);
pageContext.setAttribute("msg", rb.getString("app.title"));
}
%>ResourceBundle reads .properties as ISO-8859-1 by default, garbling Chinese. Unicode escapes are the most compatible (works on old JDKs); an explicit Reader is the cleanest.
数字与日期
<fmt:setLocale value="zh_CN"/>
<fmt:formatNumber value="1234567.891" pattern="#,##0.00"/>
<fmt:formatNumber value="0.256" type="percent"/>
<fmt:formatNumber value="1234.5" type="currency"/>
<fmt:formatDate value="${now}" pattern="yyyy年M月d日"/>
<fmt:setLocale value="en_US"/>
<fmt:formatNumber value="1234.5" type="currency"/>1,234,567.89
26%
¥1,234.50
2026年11月3日
$1,234.50type="currency" uses the locale's symbol, so switching locale changes the currency shown. If the currency is fixed by business rules (CNY only), hardcode the symbol instead of using currency.
语言切换
<%
String lang = request.getParameter("lang");
if (lang != null && lang.matches("[a-zA-Z_]{2,5}")) {
session.setAttribute("jakarta.servlet.jsp.jstl.fmt.locale.session", lang);
response.sendRedirect(request.getHeader("Referer"));
return;
}
%>
<a href="?lang=zh_CN">中文</a> | <a href="?lang=en_US">English</a>JSTL stores the chosen locale in the session attribute jakarta.servlet.jsp.jstl.fmt.locale.session. Setting it switches every page in the session without passing a parameter around.
messages.properties solves about a third of it.资源包
WEB-INF/classes/
├── messages.properties # 默认(英文)
├── messages_zh_CN.properties # 简体中文
└── messages_ja.properties # 日文# messages.properties
app.title=Demo App
nav.home=Home
nav.orders=Orders
welcome=Welcome, {0}!
cart.count=You have {0} items.# messages_zh_CN.properties(UTF-8,需在 JSP 里指定 basename 的读取方式或用转义)
app.title=演示应用
nav.home=首页
nav.orders=订单
welcome=欢迎,{0}!
cart.count=购物车里有 {0} 件商品。页面里使用
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%-- 按浏览器 Accept-Language 自动选,或手动指定 --%>
<fmt:setLocale value="${param.lang != null ? param.lang : pageContext.request.locale}"/>
<fmt:setBundle basename="messages"/>
<!DOCTYPE html>
<html><head><title><fmt:message key="app.title"/></title></head>
<body>
<nav>
<a href="/"><fmt:message key="nav.home"/></a>
<a href="/orders"><fmt:message key="nav.orders"/></a>
</nav>
<p><fmt:message key="welcome">
<fmt:param value="${sessionScope.userName}"/>
</fmt:message></p>
<p><fmt:message key="cart.count">
<fmt:param value="${fn:length(sessionScope.cart)}"/>
</fmt:message></p>
</body></html>(Accept-Language: zh-CN)
演示应用
首页 订单
欢迎,张三!
购物车里有 3 件商品。<fmt:param> 按顺序填 {0}、{1}。消息里的数字占位符不用转义,直接写 {0} 就行——这一点和 MessageFormat 一致。
中文 properties 的编码
<%-- 办法一:让 fmt:setBundle 按 UTF-8 读 --%>
<fmt:setBundle basename="messages" var="bundle"/>
<fmt:setLocale value="zh_CN"/>
<%-- 办法二:直接写 Unicode 转义(最兼容) --%>
<%-- welcome=\u6b22\u8fce\uff0c{0}\uff01 --%>
<%-- 办法三:用 ResourceBundle 显式按 UTF-8 读 --%>
<%
try (java.io.Reader r = new java.io.InputStreamReader(
application.getResourceAsStream("/WEB-INF/classes/messages_zh_CN.properties"),
java.nio.charset.StandardCharsets.UTF_8)) {
java.util.ResourceBundle rb = new java.util.PropertyResourceBundle(r);
pageContext.setAttribute("msg", rb.getString("app.title"));
}
%>ResourceBundle 默认按 ISO-8859-1 读 .properties,中文会乱码。三个办法里,Unicode 转义最兼容(老版本 JDK 也能用),显式 Reader 最干净。
数字与日期
<fmt:setLocale value="zh_CN"/>
<fmt:formatNumber value="1234567.891" pattern="#,##0.00"/>
<fmt:formatNumber value="0.256" type="percent"/>
<fmt:formatNumber value="1234.5" type="currency"/>
<fmt:formatDate value="${now}" pattern="yyyy年M月d日"/>
<fmt:setLocale value="en_US"/>
<fmt:formatNumber value="1234.5" type="currency"/>1,234,567.89
26%
¥1,234.50
2026年11月3日
$1,234.50type="currency" 自动用当前 Locale 的货币符号。所以切换 Locale 后,同一个数字会显示成不同的货币——如果业务上货币是固定的(比如只收人民币),就不要用 currency,直接写死符号。
语言切换
<%
String lang = request.getParameter("lang");
if (lang != null && lang.matches("[a-zA-Z_]{2,5}")) {
session.setAttribute("jakarta.servlet.jsp.jstl.fmt.locale.session", lang);
response.sendRedirect(request.getHeader("Referer"));
return;
}
%>
<a href="?lang=zh_CN">中文</a> | <a href="?lang=en_US">English</a>JSTL 把选中的 Locale 存在会话属性 jakarta.servlet.jsp.jstl.fmt.locale.session 里。写这个属性后,同一会话的所有页面都跟着变,不用每页传参数。
messages.properties 只解决了三成问题。