Пример интернационализации (i18n) и локализации (L10n) Spring MVC

Добро пожаловать на учебное пособие по весенней интернационализации (i18n). Для любого веб-приложения с пользователями со всего мира интернационализация (i18n) или локализация (L10n) очень важны для более эффективного взаимодействия с пользователем. Большинство веб-фреймворков предоставляют простые способы локализации приложения на основе настроек локали пользователя. Spring также следует этому шаблону и предоставляет обширную поддержку интернационализации (i18n) через использование перехватчиков Spring, разрешителей локали и ресурсных пакетов для различных локалей. Некоторые ранние статьи о i18n на Java.

Интернационализация Spring i18n

Давайте создадим простой проект Spring MVC, где мы будем использовать параметр запроса для получения локали пользователя, и на основе этого устанавливать значения меток страницы ответа из ресурсных пакетов, специфичных для локали. Создайте проект Spring MVC в среде разработки Spring Tool Suite, чтобы у нас был базовый код для нашего приложения. Если вы не знакомы с Spring Tool Suite или проектами Spring MVC, пожалуйста, прочтите Пример Spring MVC. Наш конечный проект с изменениями локализации выглядит как на изображении ниже. Мы рассмотрим все части приложения поочередно.

Конфигурация Spring i18n с помощью Maven

Наш файл pom.xml для Spring MVC выглядит следующим образом.

<?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>

Большая часть кода сгенерирована автоматически средой STS, за исключением того, что я обновил версию Spring до последней, 4.0.2.RELEASE. Мы можем удалить зависимости или обновить версии других зависимостей, но я оставил их без изменений для простоты.

Ресурсный пакет Spring

Для упрощения давайте предположим, что наше приложение поддерживает только две локали – en и fr. Если не указана локаль пользователя, мы будем использовать английский как локаль по умолчанию. Давайте создадим ресурсные пакеты Spring для обеих этих локалей, которые будут использоваться на странице JSP. Код для файла messages_en.properties:

label.title=Login Page
label.firstName=First Name
label.lastName=Last Name
label.submit=Login

, код для файла messages_fr.properties:

label.title=Connectez-vous page
label.firstName=Pr\u00E9nom
label.lastName=Nom
label.submit=Connexion

. Обратите внимание, что я использую Unicode для специальных символов в ресурсных пакетах локали французского, чтобы они правильно интерпретировались в HTML-ответе, отправляемом клиентским запросам. Еще важный момент: оба ресурсных пакета находятся в класспасе приложения, и их имя имеет шаблон “messages_{locale}.properties”. Мы увидим, почему это важно позже.

Класс контроллера Spring i18n

Наш класс контроллера очень прост, он просто записывает локаль пользователя и возвращает страницу home.jsp в качестве ответа.

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";
	}
	
}

Страница JSP Spring i18n

Код нашей страницы home.jsp выглядит следующим образом.

<%@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>

Единственная часть, на которую стоит обратить внимание, – это использование spring:message для получения сообщения с заданным кодом. Убедитесь, что библиотеки тегов Spring настроены с использованием директивы taglib jsp directive. Spring берет на себя загрузку соответствующего набора ресурсов сообщений и делает его доступным для использования в JSP-страницах.

Международная локализация Spring i18n – Файл конфигурации бина

Файл конфигурации бина Spring – это место, где происходит вся магия. В этом красота Spring Framework, поскольку он помогает нам сосредоточиться больше на бизнес-логике, а не на написании кода для тривиальных задач. Давайте посмотрим, как выглядит наш файл конфигурации бина Spring, и мы рассмотрим каждый из бинов поочередно. Код servlet-context.xml:

<?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>
  1. annotation-driven тег позволяет использовать модель программирования контроллера, без него Spring не распознает наш HomeController как обработчик запросов клиента.

  2. context:component-scan указывает пакет, в котором Spring будет искать аннотированные компоненты и автоматически регистрировать их как бины Spring.

  3. messageSource настроен для включения i18n в наше приложение. Свойство basename используется для указания расположения ресурсных пакетов. classpath:messages означает, что ресурсные пакеты находятся в classpath и следуют шаблону имен как messages_{locale}.properties. Свойство defaultEncoding используется для определения кодировки, используемой для сообщений.

  4. localeResolver бин типа org.springframework.web.servlet.i18n.CookieLocaleResolver используется для установки cookie в запросе клиента, чтобы дальнейшие запросы могли легко распознавать локаль пользователя. Например, мы можем попросить пользователя выбрать локаль при первом запуске веб-приложения, и с использованием cookie мы можем определить локаль пользователя и автоматически отправить ответ, специфичный для локали. Мы также можем указать локаль по умолчанию, имя cookie и максимальный срок действия cookie до его истечения и удаления клиентским браузером. Если ваше приложение поддерживает пользовательские сеансы, то вы также можете использовать org.springframework.web.servlet.i18n.SessionLocaleResolver в качестве localeResolver для использования атрибута локали в сеансе пользователя. Конфигурация аналогична CookieLocaleResolver.

    <bean id="localeResolver"
    	class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
    	<property name="defaultLocale" value="en" />
    </bean>
    

    Если мы не зарегистрируем ни одного “localeResolver”, по умолчанию будет использоваться AcceptHeaderLocaleResolver, который определяет локаль пользователя, проверяя заголовок accept-language в клиентском HTTP-запросе.

  5. Интерцептор org.springframework.web.servlet.i18n.LocaleChangeInterceptor настроен для перехвата запроса пользователя и определения языковой среды пользователя. Имя параметра настраиваемое, и мы используем имя параметра запроса для языка – “locale”. Без этого интерцептора мы не сможем изменить языковую среду пользователя и отправить ответ на основе новых настроек языка пользователя. Он должен быть частью элемента interceptors, иначе Spring не сконфигурирует его как интерцептор.

Если вам интересна конфигурация, которая сообщает фреймворку Spring загрузить наши конфигурации контекста, она присутствует в дескрипторе развертывания нашего приложения 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>

Мы можем изменить местоположение или имя файла контекста, изменив конфигурацию web.xml. Наше приложение Spring i18n готово, просто разверните его в любом контейнере сервлетов. Обычно я экспортирую его как файл WAR в каталог webapps автономного веб-сервера Tomcat. Вот скриншоты домашней страницы нашего приложения с разными локалями. Стандартная домашняя страница (локаль en): Передача локали в качестве параметра (локаль fr): Дальнейшие запросы без указания локали: Как видно на изображении выше, мы не передаем информацию о локали в запросе клиента, но наше приложение все равно определяет локаль пользователя. Вы, вероятно, уже догадались, что это происходит из-за бина CookieLocaleResolver, который мы настроили в нашем файле конфигурации бина Spring. Тем не менее, вы можете проверить данные cookie вашего браузера, чтобы подтвердить это. Я использую Chrome, и на изображении ниже показаны данные cookie, хранящиеся приложением. Обратите внимание, что время истечения срока действия cookie – один час, т.е. 3600 секунд, как настроено свойством cookieMaxAge. Если вы проверите журналы сервера, вы увидите, что локаль регистрируется.

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.

Вот все для примера весеннего приложения i18n. Загрузите пример проекта по ссылке ниже и поиграйтесь с ним, чтобы узнать больше.

Загрузить проект Spring i18n

Source:
https://www.digitalocean.com/community/tutorials/spring-mvc-internationalization-i18n-and-localization-l10n-example