Flask-babel을 사용해 홈페이지 국제화를 시도해보자

Flask-babel을 통한 국제화i18n(Internationalization)

Flask-Babel

Flask-Babel은 플라스크의 i18n, l10n을 지원하는 익스텐션입니다. 날짜 형식, 타임존과 관련한 기능을 가지고 있으며, gettext를 통한 번역을 가능케 합니다.

저는 히어박스의 영문, 중문, 일문 버전 웹사이트를 만들기 위해 Flask-Babel을 사용했습니다.

  • i18n(Internationalization) : 국제화, internationalization의 줄임말로 첫자리 알파벳 i 와 전체 글자 수 18자리 끝자리 알파벳 n을 사용하여 i18n이라고 부릅니다.
  • l10n(Localization) : 현지화, localization의 줄임말로 첫자리 알파벳 l과 전체 글자 수 10자리 끝자리 알파벳 n을 사용하여 l10n이라고 부릅니다.
  • 국제화와 지역화 - 위키피디아

본 글에서는 i18n과 관련한 내용만 다루니 참고 바랍니다.


바벨 4단계

  • 1단계 : 바벨 설치, 설정
  • 2단계 : 변환될 문자열 선별, 추출
  • 3단계 : 각 언어별 목록(추출된 문자열) 생성, 내용 보완
  • 4단계 : 카달로그 컴파일

바벨 1단계 - 설치

설치는 pip를 통해 간단하게 할 수 있습니다.

$ pip install Flask-Babel

참고로 Flask-Babel은 Jinja 2.5 버전 이상을 필요로합니다.

바벨 1단계 - 설정

여타 플라스크 익스텐션을 사용하는 방법과 마찬가지로 Babel의 인스턴스를 생성해줍니다.

__init__.py
from flask import Flask
from flask.ext.babel import Babel

app = Flask(__name__)
babel = Babel(app)

다음으로 자신의 웹사이트가 변환되기 원하는 언어 목록을 딕셔너리 형식으로 작성해줍니다.

config.py
LANGUAGES = {
    'en': 'English',
    'ko': 'Korean',
    'ja': 'Japanese',
    'zh': 'Chinese'
}

이제 어떤 언어를 사용할지 결정해주는 메소드를 생성합니다.

views.py
from app import babel
from config import LANGUAGES

@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(LANGUAGES.keys())

babel의 localeselector데코레이터는 request가 수행되기 전에 미리 실행되어 사용자가 어떤 언어를 사용하는지 판단합니다.

위 메소드 반환부의 request.accept_languages는 사용자 브라우저의 헤더 안에 있는 Accept-Languages의 정보를 가져옵니다. 그리고나서 best_match() 메소드를 통해 사용자가 가장 선호하는 언어를 알아냅니다.

accept_languages() 메소드는 아래와 같이 구현돼 있습니다.

werkzeug/wrappers.py
class AcceptMixin(object):

    @cached_property
    def accept_languages(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_LANGUAGE'), LanguageAccept)

이제 다시 Babel설정으로 돌아올 때가 되었습니다.
babel.cfg라는 이름의 새로운 파일을 생성하세요.

babel.cfg
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

바벨이 어떤 파일을 검색할지 정해주는 부분으로 파이썬 파일과 html파일을 검색하도록 설정하였습니다. 마지막 줄은 바벨이 Jinja2 템플릿 안에서 번역될 문구를 찾게 하기 위한 익스텐션을 설정한 부분입니다. 이 글의 아래부분엣허 익스텐션이 하나 더 추가됩니다.

이제 바벨과 관련된 설정은 어느정도 마무리가 되었습니다.

바벨 2단계 - 번역될 텍스트 선별

가장 지루하고도 빡센 작업이 될 것으로 예상합니다.(물론 저도 그랬구요) 제가 만든 코드로 예시를 보여드리겠습니다.

템플릿 속 한글과 컨트롤러(views.py)에서 전달되는 한글을 모두 골라주시면 됩니다.

gettext 메소드를 사용하여 번역되길 원하는 텍스트를 감싸줍니다.

views.py
from hereboxweb import babel
from config import LANGUAGES
from flask.ext.babel import gettext

@app.route('/alert_new_area', methods=['POST'])
def alert_new_area():
    area = request.form.get('area')
    contact = request.form.get('contact')
    if not area:
        return bad_request(gettext(u'지역을 입력해주세요'))
    return redirect(url_for('alert_new_area'))

다음은 템플릿 부분에서 문자를 선별하는 작업입니다.
일반 문자열의 경우 변환되기 원하는 문자를 {{ _(' ') }} 사이에 놓아 줍니다.

마지막 부분은 form을 사용하여 입력창을 만드는 경우 placeholder역시 지정 가능합니다.

login.html
{% extends "base.html" %}
{% block content %}
        
{{ form.email(placeholder=_("이메일 주소")) }}
{% endblock %}

바벨 2단계 - 지정 문자열 추출하기

pybabel 명령어를 이용하면 위에서 선택한 문자열을 손쉽게 가져올 수 있습니다.

커맨드 창에 다음과 같이 명령어를 입력해주세요.

$(venv) pybabel extract -F babel.cfg -o messages.pot app

그러고 나면 messages.pot 이라는 이름의 파일이 생성됩니다.

messages.pot
#: hereboxweb/views.py:76
msgid "지역을 입력해주세요"
msgstr ""

views.py에 있던 ‘지역을 입력해주세요’ 문자열을 가져온 것을 확인할 수 있습니다. 그런데 login.html에 있는 ‘로그인’과 ‘이메일 주소’는 가져오지 못했군요.

이 문제를 알아보려고 여러가지 테스트를 해본 결과 {% block body %} {% endblock %} 내부에 지정된 문자열은 가져오지 못한다는 것을 알게 되었고,
해결 방법은 바벨 설정파일에 webassets.ext.jinja2.AssetsExtension 추가해주면 됩니다.

babel.cfg
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_,webassets.ext.jinja2.AssetsExtension

바벨 설정 파일 변경 후 다시 커맨드창에 추출 명령어를 입력합니다.

$(venv) pybabel extract -F babel.cfg -o messages.pot app
messages.pot
#: hereboxweb/views.py:76
msgid "지역을 입력해주세요"
msgstr ""

#: hereboxweb/auth/templates/login.html:4
msgid "로그인"
msgstr ""

#: hereboxweb/auth/templates/login.html:7
msgid "이메일 주소"
msgstr ""

원하는 문자열이 모두 있네요.

바벨 3단계 - 언어별(일본어, 중국어, 영어) 목록 생성하기

$(venv) pybabel init -i messages.pot -d app/translations -l en
$(venv) pybabel init -i messages.pot -d app/translations -l ja
$(venv) pybabel init -i messages.pot -d app/translations -l zh

입력을 하고 나면 translations 폴더안에 en, ja, zh 라는 이름으로 폴더가 생성됩니다. 각각의 폴더안에는 LC_MESSAGES 폴더가 또 있고,
그 안에 messages.mo. messages.po 파일이 있습니다.
폴더 구조

영어 폴더의 messages.po 파일을 보면

app/translations/en/LC_MESSAGES/messages.po
#: hereboxweb/views.py:76
msgid "지역을 입력해주세요"
msgstr ""

#: hereboxweb/auth/templates/login.html:4
msgid "로그인"
msgstr ""

#: hereboxweb/auth/templates/login.html:7
msgid "이메일 주소"
msgstr ""

messages.pot과 동일한 내용이 있습니다. 일본어 폴더, 중국어 폴더 역시 마찬가지 입니다. 이제 해야할 일은 각각의 한국어 문자열에 대응하는 각국 언어를 넣어줘야합니다.

app/translations/en/LC_MESSAGES/messages.po
#: hereboxweb/views.py:76
msgid "지역을 입력해주세요"
msgstr "Please enter your location."

#: hereboxweb/auth/templates/login.html:4
msgid "로그인"
msgstr "SignIn"

#: hereboxweb/auth/templates/login.html:7
msgid "이메일 주소"
msgstr "Email Address"

바벨 4단계 - 언어목록 컴파일

이제 완성된 언어 목록을 컴파일 하면 됩니다. 아래 명령어를 입력하면 .po 파일을 가지고 .mo 파일을 만들어냅니다.

$(venv) pybabel compile -d app/translations

.mo 파일은 변환된 문자열을 가지고 있는 파일입니다. 이제 바벨이 성공적으로 변환된 문자열을 가져올 수 있습니다.


테스트

저는 바벨이 잘 되는지 테스트하기 위해 크롬 익스텐션 ‘Quick Language Switcher’를 설치했습니다. 익스텐션 설치 없이 확인하는 방법은 다음과 같습니다.

Quick Language Switcher

views.py
@babel.localeselector
def get_locale():
    return 'en'  

get_locale() 메소드의 반환값을 변경하여 쉽게 테스트 할 수 있습니다.

새롭게 추가된 문자열 반영하기

새롭게 기능을 추가하거나, 템플릿의 내용을 변경하는 경우 매번 다음과 같은 방법으로 변경된 내용을 업데이트 할 수 있습니다.

$(venv) pybabel extract -F babel.cfg -o messages.pot app
$(venv) pybabel update -i messages.pot -d app/translations
$(venv) pybabel compile -d app/translations

위 명렁어 3개를 보시면 extract로 새로 추가된 문자열을 messages.pot에 추가하고, update한 후 compile하는 것을 볼 수 있습니다.

fabfile.py
def babeling():
    local('pybabel extract -F babel.cfg -o messages.pot app')
    local('pybabel update -i messages.pot -d app/translations')
    local('pybabel compile -d app/translations')

매번 3줄씩 치기 귀찮은 경우에는 fabric에 넣어두고 명령어 하나로 해결할 수 있습니다.

$(venv) fab babeling

실제 적용

views.py
@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(LANGUAGES.keys())

get_locale()을 정상화한 후 웹사이트에 접속해도 브라우저 내 언어 정보가 한국어로 돼 있을 가능성이 높기에 별다른 변화는 없을 것입니다.

저는 서비스 이용과 관련하여 사용자에게 언어 선택을 할 수 있도록 하기 위해 네비게이션바에 선택창을 넣어주었고, 사용자가 선택한 언어정보를 쿠키에 저장했습니다.

<select id="language" onchange="setCookie('language', this.value, 10)">
    <option value="ko">Korean</option>
    <option value="ja">Japanese</option>
    <option value="en">English</option>
    <option value="zh">Chinese</option>
</select>

<script>
function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays*24*60*60*1000));
    var expires = "expires="+ d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
    window.location.reload();
}
</script>

그리고나서 get_locale() 메소드를 마지막으로 다음과 같이 수정했습니다. 쿠키에 language 정보가 있는 경우 사용자가 선택한 언어를 반환하고, 없는경우 브라우저가 가지고 있는 언어정보를 반환합니다.

@babel.localeselector
def get_locale():
    try:
        if request.cookies['language']:
            return request.cookies['language']
    except:
        return request.accept_languages.best_match(LANGUAGES.keys())

추가 내용

바벨 추가 후 서비스를 시작하려 하는데 에러메세지가 나오게 됐다. 바벨 적용한 코드 부분에서 발생한 문제로 추측하고 변경한 부분을 살펴보던 중 한 가지 더 알게됐다.
{{ _('') }} 따옴표 안에 들어가는 내용에 %(퍼센트)가 입력되면 오류가 난다는 사실을…

gettext안에 % 사용하는방법

%를 쓰고 싶을 때는 %% 이렇게 써주시면 아무 문제없이 사용할 수 있습니다.. 너무 간단해서 당황했습니다.

<div>
    <p>{{ _('히어박스는 보관 기간에 따른 할인율을 적용하고 있습니다. (자동결제 제외)') }}</p>
    <p>{{ _('6~8개월 : 총 보관 비용의 5% 할인') }}</p>

    <p>{{ _('6~8개월 : 총 보관 비용의 5%% 할인') }}</p>
</div>  

마무리

대부분의 내용이 Flask-Babel 공식홈페이지와 Miguel님의 블로그와 동일, 유사합니다.
한글로 작성한다는 점에 의의를 두었고, {% block %}{% endblock %} 내부 텍스트를 가져오지 못하는 부분과 쿠키에 저장하고, get_locale()에 추가한 부분은 직접 작성한 내용입니다.

참고사이트 목록