gettext 로 국제화(i18n)와 지역화(L10N) 된 SW 개발하기


SW 나 서비스를 개발하다보면 사용자의 환경에 따라 다른 locale 로 처리해야 하는 하는 경우가 생길수 있습니다.

Unix 운영 체제(Linux 포함)는 이런 요구 사항을 만족하기 위해 이미 오래전부터 국제화와 지역화 기능을 지원하고 있습니다.


최근에는 Java 를 많이 사용하므로 Java 의 Resource bundle 을 이용하여 메시지를 번역하는 방식을 우선적으로 생각할수 있지만 메시지 번역은 국제화된 SW 의 일부일뿐 통화나 시간 표시, 숫자 표기 방법, 단수/복수, 명사의 성별등 다양한 항목을 고려해야 합니다.


gettext 는 오래된 기술이지만 국제화와 지역화에 대한 개념을 이해하고 적용하기 좋고 PHP 나 Python 등에서도 지원하므로 배워두면 유용한 지식인데 마침 gettext 를 사용하는 아주 오래된 시스템을 수정할 일이 생겨서 정리해 봅니다.

유명한 PHP CMS인 wordpress 도 국제화 부분을 gettext 로 처리하고 있습니다.


gettext 란?

gettext 는 Unix 계열 운영체제에서 국제화(Internalization; 줄여서 i18n), 지역화(Localization; 줄여서 l10n) 를 지원하는 SW 를 만들기 위한 시스템으로 이제는 오라클에 인수된 Sun Micros ystems 가 개발했습니다.

gettext 는 번역과 프로그래밍을 분리해서 SW 내 메시지 번역을 위해 개발자가 소스를 수정할 필요가 없으며 번역자는 소스 코드가 없어도 메시지를 번역할 수 있습니다.


gettext 의 runtime library 은 libintl 이며 python 이나 PHP 사용자는  libintl 이 필요한 외부 패키지를 경우를 본 적이 있을겁니다.

gettext 의 유명한 구현물중 하나는 GNU gettext 이며 이 문서도 GNU gettext 를 기준으로 설명합니다.


life cycle

gettext 를 사용해서 국제화/지역화를 구현하는 work flow 는 다음과 같습니다.



1. 먼저 소스 코드내에 국제화가 필요한 부분을 gettext 함수로 감싸서 표시해 줍니다.

#define THIS_PACKAGE_NAME "hello" 

char* locale_dir = "locale"; // locale 파일을 읽어올 디렉터리

setlocale (LC_ALL, "");

// 패키지 이름. locale 이름의 폴더 밑에 hello.mo 가 있어야 함.
bindtextdomain (THIS_PACKAGE_NAME, locale_dir);
textdomain (THIS_PACKAGE_NAME);

// 번역되서 보여질 부분은 gettext 로 감싸줌
printf(gettext("My name is %s.\n"), my_name);

// gettext 대신 _ macro 사용해도 됨.
printf(_("my age is %d.\n"), my_age);


2. 소스 수정이 끝났으면 소스에서 gettext 로 표시한 부분을 추출해 주는 xgettext 를 이용해서 메시지를 추출한후에 저장합니다. 아래는 추출한 메시지 파일을 hello.pot 로 저장하는 예제입니다.

xgettext -c hello.c hello.h -o hello.pot


3. pot 파일을 에디터로 열어보면 아래와 같이 메타 정보가 있고 마지막에 msgid 와 msgstr 이 보일겁니다.

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-29 22:23-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: hello.c:18
#, c-format
msgid "my name is %s.\n"
msgstr ""

msgid 가 소스 코드에 있는 메시지이고 msgstr 은 번역할 메시지입니다.


4. msginit 명령을 이용해서 번역할 언어의 locale 에 맞게 메시지 카탈로그 파일을 만듭니다. 메시지 카탈로그는 .po 확장자를 가지며 po 는 Portable Object 입니다.
po 파일은 문자열로 구성되어 있어서 선호하는 에디터로 수정해도 되지만 아래에서 설명할 전문 에디터를 사용하는 게 좋습니다.  아래는 한글과 독일어 로캘용 po 파일을 만드는 예시입니다.

$ msginit -i hello.pot -o ko.po -l ko_KR.utf8
$ msginit -i hello.pot -o de.po -l de_DE

-i : 입력 소스를 설정합니다.
-o : 저장할 po 파일 명을 설정하며 일반적으로 파일명은 LOCALE_NAME.po 형식으로 적어줍니다.
-l : locale 을 지정합니다.


msginit 를 실행하면 아래와 같이 새로운 메시지 카탈로그에 넣을 email 주소를 물어보는데 확인하고 엔터를 치면 

The new message catalog should contain your email address, so that users can
give you feedback about the translations, and so that maintainers can contact
you in case of unexpected technical problems.

Is the following your email address?
  lesstif@gmail.com
Please confirm by pressing Return, or enter your email address.


5. 이제 Po Edit 같은 도구를 사용해서 locale 별 파일(예: ko.po)을 수정하고 저장해 줍니다.


6. .po 수정이 끝났으면 이제 mo(Machine Object) 로 컴파일해야 gettext 에서 run time 에 locale 별 언어 파일을 로딩할 수 있습니다. Po edit 의 경우 파일  → "Mo로 컴파일" 메뉴에서 .mo 파일로 컴파일해줍니다.

리눅스 커맨드에서는 msgfmt 명령으로 po 를 mo 로 컴파일할 수 있습니다.

$ mkdir -p locale/ko/LC_MESSAGES/
$ msgfmt ko.po -o locale/ko/LC_MESSAGES/hello.mo

locale 용 데이터가 실행 파일(hello)이 있는 폴더의 하위에 locale/LOCALE/LOCALE_TYPE/ 경로로 있을 경우입니다.


이제 locale 을 변경없이 hello 를 실행하면 기본 설정인 영어로 표시됩니다.

$ ./hello john 50 

my name is john.
my age is 50.


제대로 동작하는지 확인하기 위해 locale 을 ko_KR 로 설정하고 locale 명령어로 확인해 봅니다.

$ export LANG=ko_KR
$ locale

LANG=ko_KR
LC_CTYPE="ko_KR"
LC_NUMERIC="ko_KR"
LC_TIME="ko_KR"
LC_COLLATE="ko_KR"
LC_MONETARY="ko_KR"
LC_MESSAGES="ko_KR"
LC_PAPER="ko_KR"
LC_NAME="ko_KR"
LC_ADDRESS="ko_KR"
LC_TELEPHONE="ko_KR"
LC_MEASUREMENT="ko_KR"
LC_IDENTIFICATION="ko_KR"
LC_ALL=


다시 hello 를 실행하면 메시지가 한글로 표시되는 걸 확인할 수 있습니다.

$ ./hello 똠방 50

내 이름은 똠방입니다.
내 나이는 50입니다.

C 언어 예시

C 로 만든 gettext 사용 예제를 https://github.com/lesstif/gettext-example 에 올려놨으니 참고하세요.


같이 보기

Ref