본문 바로가기
실습/[스프링 부트] 채팅 웹 사이트 만들기

[Thymeleaf] Lang 설정을 통한 다국어 페이지 제공

by 이민우 2024. 6. 10.
728x90
반응형

 

웹 사이트들을 들어가다보면, 아래와 같이 여러 언어를 지원하는 웹 사이트를 발견할 수 있다.

 

https://aws.amazon.com/ko/certification/certified-developer-associate/?ch=tile&tile=getstarted

 

지금까지는 국내에서만 사용될 웹 사이트를 개발했기에 굳이 해당 기술을 익힐 필요가 없었다. 하지만 다음 프로젝트는 해외에서 사용될 예정이기에 한국어+영어+a 를 지원하는 웹 사이트가 필요했고, 이에 다국어 웹 사이트 개발 방법을 공부해볼까 한다.

 

우선 다국어 설정 전에 html에서 사용하는 메세지를 모아놓는 messages.properties 설정 방법을 알아볼까 한다.

 

 

messages.properties를 사용한 텍스트 출력

 

자바 코드 내에서 여러 클래스에서 동시에 사용하는 변수는 static으로 만들거나 혹은 enum으로 만들어 사용하고 관리한다. THYMELEAF 개발 시에도 이렇게 여러 페이지에서 동일하게 사용되는 고정된 문구는 (등록되었습니다 등) 한 곳에 모아놓고 사용할 수 있다.

 

그러면 만약 안내문구가 변경될 경우 html을 일일히 찾아다니지 않고 하나의 .properties 파일만 수정하면 되니 유지보수가 간편해진다.

 

이제 그 방법을 알아보기 위해 우선 프로젝젝트를 생성해보자.

스프링, 자바 버전

 

디펜던시

 

그리고 타임리프 설정을 위해 아래와 같이 application.properties를 설정했다.

spring.thymeleaf.prefix=classpath:templates/
spring.thymeleaf.suffix=.html

 

우선 페이지를 return할 컨트롤러를 작성해보자.

 

CommonController.java

package com.mwlee.thymeleaf.locale.common;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class CommonController {

	private static final String USER_LIST="userList";
	private static final String NAME="name";
	private static final String AGE="age";

	@GetMapping("/users")
	public String users(Model model) {
		
		List<Map<String, Object>> userList = new ArrayList<>();
		
		Map<String, Object> user01 = new HashMap<>();
		user01.put(NAME, "LEE");
		user01.put(AGE, 30);
		userList.add(user01);
		
		Map<String, Object> user02 = new HashMap<>();
		user02.put(NAME, "KIM");
		user02.put(AGE, 25);
		userList.add(user02);
		
		Map<String, Object> user03 = new HashMap<>();
		user03.put(NAME, "PARK");
		user03.put(AGE, 35);
		userList.add(user03);
		
		model.addAttribute(USER_LIST, userList);
		
		return "users";
	}
	
}

 

 

다음으로 resources 아래에 templates 폴더를 만들고 users.html 파일을 생성한다.

만약 messages.properties를 적용하지 않고 html을 만들면 아래와 같이 만들 수 있을 것이다.

 

templates/users.html

 

users.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>         
	<table>
		<!-- 제목 -->
		<tr> <th colspan = "2"> List Users </th> <tr>
		
		<!-- 비여있으면 empty list -->
		<tr th:if="${userList.empty}">
			<th colspan = "2"> empty list </th>
		</tr>
		<!-- 데이터 존재 시 출력 -->
		<tr th:unless="${userList.empty}" th:each="user : ${userList}">
			<td> <span th:text = "${user.name}"> key </span> </td>
			<td> <span th:text = "${user.age}"> value </span> </td>
		</tr>
		
	</table>
</body>
</html>

 

결과를 확인하면 아래와 같다.

그런데 위에서 설명한대로, 고정된 값들은 굳이 html 안에 넣지 않고 messages.properties로 따로 빼서 사용이 가능하다.

 

위 html 코드 안에서는 아래 세 부분을 예시로 들 수 있을 것 같다.

동적이지 않은 값

 

이제 위 고정값들을 따로 빼기 위해 resources 아래에 messages.properties 파일을 생성하고, 각 부분에 들어갈 메시지를 저장한다.

resources/messages.properties

 

messages.properties

user.title=테스트용 페이지
user.table.title=사용자 목록
user.table.empty=사용자가 존재하지 않습니다.

 

이제 messages.properties에 작성한 내용을 html에 집어넣어보자. 방법은 th:text="#{변수명}" 을 태그 안에 넣는 것이다.

 

users.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{user.title}"></title>
</head>
<body>         
	<table>
		<!-- 제목 -->
		<tr> <th colspan = "2" th:text="#{user.table.title}"></th> <tr>
		
		<!-- 비여있으면 empty list -->
		<tr th:if="${userList.empty}">
			<th colspan = "2" th:text="#{user.table.empty}"></th>
		</tr>
		<!-- 데이터 존재 시 출력 -->
		<tr th:unless="${userList.empty}" th:each="user : ${userList}">
			<td> <span th:text = "${user.name}"> key </span> </td>
			<td> <span th:text = "${user.age}"> value </span> </td>
		</tr>
		
	</table>
</body>
</html>

 

프로젝트를 재실행하고 페이지에 진입해보면 문구가 잘 설정되었음을 확인할 수 있다.

결과

 

 

다국어 설정하

그러면 이제 오늘의 본론인 "다국어로 페이지 구성하기"를 시작해보자.

 

앞서 messages.properties에서 생성한대로, 똑같은 변수명을 가지는 영어/한글 properties를 따로 만들고 html 안에서 사용하면 굳이 html을 두 개 (영문판/한글판) 만들지 않아도 여러 언어를 제공하는 웹 사이트를 개발할 수 있다.

 

위에서 만든 한글 버전에 더해 영어 버전을 생하기 위해 messages_en,properties를 만들 것이다.

 

다만 모든 messages.properties를 /resources 밑에 넣으면 폴더 구조가 번잡해지니, messages라는 폴더를 만들어 properties를 모아놓자.

messages/messages*.properties

 

messages_en.properties

user.title=Page For Test
user.table.title=List Of Users
user.table.empty=User Not Exists.

 

위에서는 messages.properties가 /resources 밑에 존재하니 별도의 설정을 해주지 않아도 Spring Boot이 자동으로 메시지 위치를 인식해서 사용했다.

 

하지만 지금은 messages라는 폴더를 만들어 그 안에 프로퍼티들을 모아놓았으니 이에 대한 설정을 추가해야다.

 

application.properties에 아래 설정을 추가한다.

 

application.properties

# messages/messages*.properties 이므로 아래처럼 작성
spring.messages.basename=messages/messages
# messages*.properties의 파일 포맷
spring.messages.encoding=UTF-8
# locale 미식별 시 시스템 locale 사용 여부. 즉, en인데 _en이 없을 경우에 fallback 방지
spring.messages.fallback-to-system-locale=false
# 메시지 형식에서 MessageFormat 사용 여부.
spring.messages.always-use-message-format=false

 

위와 같이 설정하면 이제 messages/messages*.properties를 스프링 부트가 인식할 수 있다.

 

설정이 끝났으니 코딩을 해보자. lang 설정의 대표적인 방법으로, LocaleChangeInterceptor 인터셉터를 통해 lang 키워드로 들어온 파라미터를 받아 지역을 설정하는 코드이다.

 

LangConfig.java

package com.mwlee.thymeleaf.locale.config;

import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class LangConfig implements WebMvcConfigurer {

    private static final String LANG_PARAM_NM = "lang";

    /**
     * default lang 설정 : 한국기준이므로 한국어로 설정
     * @return
     */
    @Bean
    public LocaleResolver localeResolver() {

        CookieLocaleResolver localeResolver = new CookieLocaleResolver(LANG_PARAM_NM);
        // Locale.KOREA도 있는데, 그건 ko_KR이고, 얘는 ko로 인식
        // default이므로 messages.properties를 가져오게 됨. 고로 해당 파일에 한국어 작성
        localeResolver.setDefaultLocale(Locale.KOREAN);
        localeResolver.setCookieHttpOnly(true);

        return localeResolver;
    }

    /**
     * 인터셉터를 통해 parameter에서 "lang"이 들어오면 해당 Language로 변경
     * @return
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName(LANG_PARAM_NM);
        interceptor.setIgnoreInvalidLocale(true);
        return interceptor;
    }

    /**
     * 위에서 선언한 인터셉터 추가
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor())
        		// 연습용이라 css, js 등을 안쓰긴 하는데, 일단 작성
                .excludePathPatterns("/css/**", "/js/**", "/images/**","/fonts/**")
                // 그 외 모든 url에 적용
                .addPathPatterns("/**");

    }

}

 

그리고 ?lang=ko 등을 보이지 않게 하기 위해 Lang 변경 전용 컨트롤러를 별도로 생성했다.

?lang=ko 등이 별로 신경쓰이지 않는다면 굳이 작성할 이유는 없다.

 

CommonContreller.java 에 추가

	/**
	 * @ResponseBody 미탑재 시 
	 * java.lang.IllegalArgumentException: Unknown return value type: java.lang.Boolean
	 * 에러 발생
	 * 
	 * @return
	 */
	@GetMapping("/change/lang")
	public @ResponseBody boolean changeLang() {
		return true;
	}

 

마지막으로 html에 en, ko를 설정할 수 있는 a 태그와, a 태그 동작에 대한 javascript를 작성한다.

 

users.html에 추가

	<table>
		<tr>
			<td><a href="javascript:changeLang('en')">ENG</a></td>
			<td><a href="javascript:changeLang('ko')">한국어</a></td>
		</tr>
	</table>
    
    
    ...
    
    
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
	<script th:inline="javascript">
		function changeLang(lang) {
			param = {
					lang : lang
			};
			
			$.ajax({
		        type: "GET",
		        url: "/change/lang",
		        data: {
		        	lang : lang
		        },
		        success: function(data) {
		        	// lang 변경 후 새로고침
		        	// 사실 바로 현재페이지?lang=ko로 해도 되는데, url에 lang이 남는게 꼴보기 싫어서 reload 한 번 수행
	                location.reload();
	            }
		    });
		}
	</script>

 

이제 한 번 구동해보자.

 

우선 메인 페이지 진입 시 설정대로 디폴트 값인 한국어로 출력된다.

디폴트 페이지

 

ENG를 누르면 아래와 같이 영문으로 나오게 된다.

ENG 클릭 시

 

이제 사용자가 없는 경우도 봐보자.

Controller에서 사용자를 return하지 않고 웹 페이지에 진입해보자.

 

user가 아무 데이터고 return하지 않도록 수정

 

 

한글-사용자 없음

 

영어 - 사용자 없음

 

 

정상적으로 한글/영문이 잘 적용되었음을 확인할 수 있다.

 

 

 

프로젝트 전체 구조 및 코드

마지막으로 전체 구조와 코드를 공유하며 마칠까 한다.

 

구조

 

application.properties

spring.application.name=ThymeleafLocale

spring.thymeleaf.prefix=classpath:templates/
spring.thymeleaf.suffix=.html

# messages/messages*.properties 이므로 아래처럼 작성
spring.messages.basename=messages/messages
# messages*.properties의 파일 포맷
spring.messages.encoding=UTF-8
# locale 미식별 시 시스템 locale 사용 여부. 즉, en인데 _en이 없을 경우에 fallback 방지
spring.messages.fallback-to-system-locale=false
# 메시지 형식에서 MessageFormat 사용 여부. 
spring.messages.always-use-message-format=false

 

CommonController.java

package com.mwlee.thymeleaf.locale.common;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class CommonController {

	private static final String USER_LIST="userList";
	private static final String NAME="name";
	private static final String AGE="age";

	@GetMapping("/users")
	public String users(Model model) {
		
		List<Map<String, Object>> userList = new ArrayList<>();
		
		Map<String, Object> user01 = new HashMap<>();
		user01.put(NAME, "LEE");
		user01.put(AGE, 30);
		userList.add(user01);
		
		Map<String, Object> user02 = new HashMap<>();
		user02.put(NAME, "KIM");
		user02.put(AGE, 25);
		userList.add(user02);
		
		Map<String, Object> user03 = new HashMap<>();
		user03.put(NAME, "PARK");
		user03.put(AGE, 35);
		userList.add(user03);
		
		model.addAttribute(USER_LIST, userList);
		
		return "users";
	}
	
	/**
	 * @ResponseBody 미탑재 시 
	 * java.lang.IllegalArgumentException: Unknown return value type: java.lang.Boolean
	 * 에러 발생
	 * 
	 * @return
	 */
	@GetMapping("/change/lang")
	public @ResponseBody boolean changeLang() {
		return true;
	}
}

 

LangConfig.java

package com.mwlee.thymeleaf.locale.config;

import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class LangConfig implements WebMvcConfigurer {

    private static final String LANG_PARAM_NM = "lang";

    /**
     * default lang 설정 : 한국기준이므로 한국어로 설정
     * @return
     */
    @Bean
    public LocaleResolver localeResolver() {

        CookieLocaleResolver localeResolver = new CookieLocaleResolver(LANG_PARAM_NM);
        // Locale.KOREA도 있는데, 그건 ko_KR이고, 얘는 ko로 인식
        // default이므로 messages.properties를 가져오게 됨. 고로 해당 파일에 한국어 작성
        localeResolver.setDefaultLocale(Locale.KOREAN);
        localeResolver.setCookieHttpOnly(true);

        return localeResolver;
    }

    /**
     * 인터셉터를 통해 parameter에서 "lang"이 들어오면 해당 Language로 변경
     * @return
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName(LANG_PARAM_NM);
        interceptor.setIgnoreInvalidLocale(true);
        return interceptor;
    }

    /**
     * 위에서 선언한 인터셉터 추가
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor())
        		// 연습용이라 css, js 등을 안쓰긴 하는데, 일단 작성
                .excludePathPatterns("/css/**", "/js/**", "/images/**","/fonts/**")
                // 그 외 모든 url에 적용
                .addPathPatterns("/**");

    }
}

 

messages_en.properties

user.title=Page For Test
user.table.title=List Of Users
user.table.empty=User Not Exists.

 

messages.properties

user.title=테스트용 페이지
user.table.title=사용자 목록
user.table.empty=사용자가 존재하지 않습니다.

 

users.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{user.title}"></title>
</head>
<body>         
	<table>
		<tr>
			<td><a href="javascript:changeLang('en')">ENG</a></td>
			<td><a href="javascript:changeLang('ko')">한국어</a></td>
		</tr>
	</table>
	<table>
		<!-- 제목 -->
		<tr> <th colspan = "2" th:text="#{user.table.title}"></th> <tr>
		
		<!-- 비여있으면 empty list -->
		<tr th:if="${userList.empty}">
			<th colspan = "2" th:text="#{user.table.empty}"></th>
		</tr>
		<!-- 데이터 존재 시 출력 -->
		<tr th:unless="${userList.empty}" th:each="user : ${userList}">
			<td> <span th:text = "${user.name}"> key </span> </td>
			<td> <span th:text = "${user.age}"> value </span> </td>
		</tr>
		
	</table>
	
	<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
	<script th:inline="javascript">
		function changeLang(lang) {
			param = {
					lang : lang
			};
			
			$.ajax({
		        type: "GET",
		        url: "/change/lang",
		        data: {
		        	lang : lang
		        },
		        success: function(data) {
		        	// lang 변경 후 새로고침
		        	// 사실 바로 현재페이지?lang=ko로 해도 되는데, url에 lang이 남는게 꼴보기 싫어서 reload 한 번 수행
	                location.reload();
	            }
		    });
		}
	</script>
</body>
</html>

 

 

 

 

+) 추가 (html 태그를 messages.properties에서 사용)

그렇다면 아래처럼 html 태그가 삽입된 코드는 어떻게 messages.properties로 뺄 수 있을까?

 

<br> 태그가 포함된 메시지

 

당연한 말이지만 <br> 태그를 그대로 messages.properties에 삽입하면 인식을 할 수 없다.

test.br=안녕하세요.<br>반갑습니다.
<div th:text="#{test.br}"></div>

결과

 

이는 th:text가 태그를 문자로 인식하기에 벌어지는 현상이다.

 

고로 html 태그를 문자가 아니라 태그 본연의 의미로 인식하는 방법이 필요한데, 이 때 사용하는 것이 th:utext이다.

한 번 적용해보자.

<div th:utext="#{test.br}"></div>

결과

 

잘 적용되었음을 확인할 수 있다.

728x90
반응형