Chào mừng bạn đến với hướng dẫn về quốc tế hóa và bản địa hóa trong Spring.
Đối với bất kỳ ứng dụng web nào có người dùng trên toàn thế giới, quốc tế hóa (Internationalization – i18n) hoặc bản địa hóa (Localization – L10n) là yếu tố rất quan trọng nhằm nâng cao trải nghiệm người dùng.
Hầu hết các framework ứng dụng web hiện nay đều cung cấp những cách đơn giản để bản địa hóa ứng dụng dựa trên thiết lập ngôn ngữ của người dùng.
Spring cũng tuân theo mô hình đó và cung cấp hỗ trợ mạnh mẽ cho internationalization (i18n) thông qua việc sử dụng:
- Spring Interceptors
- Locale Resolvers
- Resource Bundles cho các ngôn ngữ khác nhau.
Quố tế hóa (i18n)
Bây giờ, chúng ta sẽ tạo một dự án Spring MVC đơn giản, trong đó:
- Sử dụng tham số từ request để lấy thông tin locale của người dùng.
- Dựa vào locale đó, hiển thị các label trên trang phản hồi bằng tệp resource bundle tương ứng với ngôn ngữ.
Hãy tạo một dự án Spring MVC trong Spring Tool Suite (STS) để có phần mã nền ban đầu cho ứng dụng.
Dự án sau khi thêm các thay đổi liên quan đến localization sẽ có giao diện giống như hình dưới đây.
Chúng ta sẽ đi qua từng phần của ứng dụng một cách chi tiết.
Cấu hình Maven cho Spring i18n
File pom.xml của dự án Spring MVC của chúng ta như sau:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<https://maven.apache.org/POM/4.0.0>" xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<https://maven.apache.org/POM/4.0.0> <https://maven.apache.org/maven-v4_0_0.xsd>">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev</groupId>
<artifactId>spring</artifactId>
<name>Springi18nExample</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.6</java-version>
<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
<org.aspectj-version>1.7.4</org.aspectj-version>
<org.slf4j-version>1.7.5</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Phần lớn mã trong dự án được STS (Spring Tool Suite) tự động sinh ra, ngoại trừ việc tôi đã cập nhật phiên bản Spring lên bản mới nhất là 4.0.2.RELEASE.
Chúng ta có thể xóa bớt hoặc cập nhật các dependency khác nếu muốn, nhưng để đơn giản, tôi giữ nguyên như mặc định.
Spring Resource Bundle
Để đơn giản, giả sử ứng dụng của chúng ta chỉ hỗ trợ hai ngôn ngữ: en (tiếng Anh) và fr (tiếng Pháp).
Nếu không có locale nào được người dùng chỉ định, chúng ta sẽ sử dụng tiếng Anh làm mặc định.
Hãy tạo các resource bundle tương ứng với hai ngôn ngữ trên, và các tệp này sẽ được dùng trong trang JSP.
Mã của file messages_en.properties
được trình bày bên dưới:
label.title=Login Page
label.firstName=First Name
label.lastName=Last Name
label.submit=Login
Mã của file messages_fr.properties
được trình bày bên dưới:
label.title=Connectez-vous page
label.firstName=Pr\\u00E9nom
label.lastName=Nom
label.submit=Connexion
Lưu ý rằng tôi đang sử dụng mã Unicode cho các ký tự đặc biệt trong tệp resource bundle của ngôn ngữ Pháp, để đảm bảo chúng được hiển thị đúng trong HTML phản hồi gửi về cho trình duyệt của người dùng.
Một điểm quan trọng khác cần lưu ý là:
Cả hai tệp resource bundle đều nằm trong classpath của ứng dụng và được đặt tên theo mẫu:
messages_{locale}.properties
Chúng ta sẽ tìm hiểu lý do vì sao điều này quan trọng ở phần sau.
Lớp Controller cho Spring i18n
Lớp controller của chúng ta rất đơn giản, nó chỉ thực hiện việc ghi log locale của người dùng và trả về trang home.jsp làm phản hồi.
package com.journaldev.spring;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Handles requests for the application home page.
*/
@Controller
public class HomeController {
private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
/**
* Simply selects the home view to render by returning its name.
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Locale locale, Model model) {
logger.info("Welcome home! The client locale is {}.", locale);
return "home";
}
}
Trang JSP cho Spring i18n
Mã của trang home.jsp của chúng ta được trình bày như sau:
<%@taglib uri="<https://www.springframework.org/tags>" prefix="spring"%>
<%@ page session="false"%>
<html>
<head>
<title><spring:message code="label.title" /></title>
</head>
<body>
<form method="post" action="login">
<table>
<tr>
<td><label> <strong><spring:message
code="label.firstName" /></strong>
</label></td>
<td><input name="firstName" /></td>
</tr>
<tr>
<td><label> <strong><spring:message
code="label.lastName" /></strong>
</label></td>
<td><input name="lastName" /></td>
</tr>
<tr>
<spring:message code="label.submit" var="labelSubmit"></spring:message>
<td colspan="2"><input type="submit" value="${labelSubmit}" /></td>
</tr>
</table>
</form>
</body>
</html>V
Điểm đáng chú ý duy nhất trong trang JSP là việc sử dụng spring:message để lấy nội dung thông điệp dựa trên code tương ứng.
Hãy đảm bảo rằng tag libraries của Spring đã được khai báo đúng bằng chỉ thị taglib
trong JSP.
Spring sẽ tự động xử lý việc nạp resource bundle phù hợp với locale hiện tại và cung cấp nội dung thông điệp để JSP sử dụng.
Cấu hình Bean cho quốc tế hóa
File cấu hình bean của Spring chính là nơi diễn ra tất cả “phép màu”.
Đây là điểm mạnh của Spring Framework, nó giúp chúng ta tập trung vào logic nghiệp vụ, thay vì phải viết nhiều đoạn mã xử lý lặp lại.
Hãy cùng xem file cấu hình servlet-context.xml của chúng ta trông như thế nào, và sau đó sẽ phân tích từng bean một.
Mã của servlet-context.xml được trình bày bên dưới:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="<https://www.springframework.org/schema/mvc>"
xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>" xmlns:beans="<https://www.springframework.org/schema/beans>"
xmlns:context="<https://www.springframework.org/schema/context>"
xsi:schemaLocation="<https://www.springframework.org/schema/mvc> <https://www.springframework.org/schema/mvc/spring-mvc.xsd>
<https://www.springframework.org/schema/beans> <https://www.springframework.org/schema/beans/spring-beans.xsd>
<https://www.springframework.org/schema/context> <https://www.springframework.org/schema/context/spring-context.xsd>">
<!-- DispatcherServlet Context: defines this servlet's request-processing
infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources
in the /WEB-INF/views directory -->
<beans:bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<beans:bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<beans:property name="basename" value="classpath:messages" />
<beans:property name="defaultEncoding" value="UTF-8" />
</beans:bean>
<beans:bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<beans:property name="defaultLocale" value="en" />
<beans:property name="cookieName" value="myAppLocaleCookie"></beans:property>
<beans:property name="cookieMaxAge" value="3600"></beans:property>
</beans:bean>
<interceptors>
<beans:bean
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<beans:property name="paramName" value="locale" />
</beans:bean>
</interceptors>
<context:component-scan base-package="com.journaldev.spring" />
</beans:beans>
- Thẻ annotation-driven cho phép sử dụng mô hình lập trình Controller; nếu không có thẻ này, Spring sẽ không nhận diện được lớp HomeController là một handler xử lý yêu cầu từ client.
- Thẻ context:component-scan chỉ định package mà Spring sẽ tìm kiếm các thành phần được chú thích để tự động đăng ký chúng làm bean trong Spring container.
- Bean messageSource được cấu hình để kích hoạt i18n cho ứng dụng của chúng ta.
- Thuộc tính basename dùng để xác định vị trí các resource bundles.
classpath:messages
nghĩa là các resource bundles nằm trong classpath và được đặt tên theo mẫumessages_{locale}.properties
.- defaultEncoding định nghĩa bảng mã sử dụng để đọc các message.
- Bean localeResolver có kiểu
org.springframework.web.servlet.i18n.CookieLocaleResolver
được dùng để đặt một cookie trong request của client, giúp các request tiếp theo dễ dàng nhận diện locale của người dùng.Ví dụ: khi người dùng mở ứng dụng lần đầu và chọn ngôn ngữ, ta có thể lưu thông tin đó vào cookie để từ đó gửi phản hồi phù hợp với locale.Ta cũng có thể cấu hình:
- Locale mặc định
- Tên của cookie
- MaxAge của cookie trước khi nó hết hạn và bị trình duyệt xóa.
- Nếu ứng dụng của bạn duy trì các session, bạn cũng có thể sử dụng
org.springframework.web.servlet.i18n.SessionLocaleResolver
thay vìCookieLocaleResolver
.Trong trường hợp này, locale sẽ được lưu trong session attribute của người dùng.Cấu hình cho hai loại resolver này là tương tự nhau.
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="en" />
</bean>
Nếu chúng ta không cấu hình bất kỳ localeResolver nào, thì Spring sẽ sử dụng mặc định là AcceptHeaderLocaleResolver, resolver này sẽ xác định locale của người dùng bằng cách kiểm tra tiêu đề accept-language
trong HTTP request từ client.
- Interceptor
org.springframework.web.servlet.i18n.LocaleChangeInterceptor
được cấu hình để chặn yêu cầu từ người dùng và xác định locale mà người dùng muốn sử dụng.Tên của tham số có thể tùy chỉnh, và trong ví dụ này chúng ta sử dụng tên tham số request là ****locale. Nếu không có interceptor này, chúng ta sẽ không thể thay đổi locale của người dùng và gửi phản hồi dựa trên thiết lập ngôn ngữ mới. Interceptor này phải được khai báo trong phần tử **interceptors**, nếu không Spring sẽ không cấu hình nó như một interceptor hợp lệ. Nếu bạn đang thắc mắc phần cấu hình nào thông báo cho Spring Framework biết để nạp file cấu hình context, thì nó được khai báo trong file deloyment descriptor của ứng dụng MVC.
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Chúng ta có thể thay đổi vị trí hoặc tên của file cấu hình context bằng cách điều chỉnh cấu hình trong file web.xml.
Ứng dụng Spring i18n của chúng ta giờ đã sẵn sàng, chỉ cần triển khai nó trong bất kỳ servlet container nào.
Thông thường, tôi sẽ xuất ứng dụng dưới dạng file WAR và đặt vào thư mục webapps của máy chủ web Tomcat độc lập.
Dưới đây là ảnh chụp màn hình của trang chủ ứng dụng với các ngôn ngữ khác nhau.
Trang chủ mặc định (locale en):
Truyền locale dưới dạng tham số (locale fr):
Các yêu cầu tiếp theo không cần truyền lại locale:
Như bạn có thể thấy trong hình trên, mặc dù chúng ta không truyền thông tin locale trong request từ client, nhưng ứng dụng vẫn xác định đúng locale của người dùng.
Chắc hẳn đến đây bạn đã đoán được nhờ bean CookieLocaleResolver mà chúng ta đã cấu hình trong file cấu hình bean của Spring.
Tuy nhiên, bạn cũng có thể kiểm tra dữ liệu cookie trong trình duyệt để xác nhận điều này.
Tôi đang sử dụng Chrome, và hình dưới đây hiển thị cookie được ứng dụng lưu lại.
Lưu ý rằng thời gian hết hạn của cookie là một giờ, tức 3600 giây, như đã được cấu hình thông qua thuộc tính cookieMaxAge.
Nếu bạn kiểm tra log của server, bạn sẽ thấy locale đang được ghi lại.
INFO : com.journaldev.spring.HomeController – Welcome home! The client locale is en.
INFO : com.journaldev.spring.HomeController – Welcome home! The client locale is fr.
INFO : com.journaldev.spring.HomeController – Welcome home! The client locale is fr.