소프트웨어의 국제화일반적으로 우리가 쓰는 대부분의 프로그램은 미국을 중심으로 한 영어권 국가에서 영어를 사용하여 만들어진다. 따라서 이들 프로그램은 사용자가 영어에 익숙한 것으로 가정한다. 반면에 영어에 친숙하지 않은 비영어권 사용자들은 프로그램 사용에 어려움을 느끼며, 영어를 반드시 배워야한다는 부담을 갖게 된다. 거기에다 시간이 흐르면서 전세계적인 정보 교류가 활발해지자, 여러 문화권에서 쓰일 수 있는 단일 소프트웨어에 대한 욕구가 증대하게 되었다. 최근 일어나고 있는 인터넷의 급성장은 이러한 움직임을 가속화하고 있다.
프로그램을 여러 문화권에서 쓸 수 있도록 만드는 과정에는 두 가지 단계가 있다. 하나는 프로그램에서 특정 언어뿐만 아니라 여러 언어를 사용할 수 있게 하는 과정으로, 이를 국제화 (internationalization, 줄여서 i18n)라 한다. 또 한 가지 단계는 현지화 (localization, 줄여서 l10n)라고 하는 것으로, 이미 국제화된 소프트웨어가 현지의 문화적 관습-이를 로케일(locale)이라 한다-을 지원하도록 하는 것을 뜻한다. 로케일에는 문자 코드, 입출력 방식, 고유어 메시지, 화폐, 날짜 표시법 등이 모두 포함된다. 또, 이 두 과정을 포괄하는 것으로 고유어 지원 (native language support, 줄여서 NLS)이란 말을 쓰기도 한다.
POSIX 호환 시스템에서의 국제화는 ISO C-1989와 POSIX 1003.2, 1003.2b에 정의되어 있다. 이 모델은 현지화 정보를 담고 있는 여섯 개의 범주로 구분되는 로케일을 정의한다.
로케일을 사용하기 위해 호출하는 함수로, <locale.h>에 선언되어 있다.
char *setlocale(int category, const char *locale);
첫번째 인수는 여섯 개 범주 중 하나이며, 모든 것을 포함하려고 할 땐 LC_ALL을 쓰면 된다. 두번째 인수는 어떤 로케일을 사용할 것인지 지정한다. 이것을 ""로 해두면 범주와 같은 이름의 환경 변수나 LANG 등에 따라 로케일이 결정된다. 특정 로케일을 설정하고자 할 땐 "identifier"를 지정해 주면 된다. 이것은 보통 ISO 639 에 정의된 언어 코드로서, 한국어의 경우 "ko", 독일어의 경우 "de"가 된다.
앞서 말했듯이, 일반 컴퓨터 사용자들이 겪는 가장 큰 문제는 사용하는 프로그램의 메시지가 낯선 언어로 출력된다는 점이다. 이 점을 해결하는 가장 간단한 방법은 소스 코드 상의 모든 메시지를 수작업을 통해 고유어로 바꾸는 것으로, 지금까지 대부분의 소프트웨어가 이 방법을 택해 왔다. 그러나 원본 프로그램이 갱신될 때마다 많은 양의 번역 메시지를 다시 입력하는 등 소모적인 노동이 많이 필요하고, 그럼으로써 원어판과 고유어판의 발표 간격이 너무 길어진다는 단점이 있다.
오늘날 유닉스 호환 시스템 상에서는 세 가지 방법이 널리 알려져 있다:
X 윈도우 시스템에서 쓰는 방법으로, 버튼이나 메뉴바 같은 위젯과 연관된 메시지를 애플리케이션 디폴트 파일에 담아 놓고 편집할 수 있게 하는 것이다. 소스 파일의 단순 변경보다는 조금 발전된 형태지만, X 윈도우에서만 쓸 수 있다는 단점이 있다.
이 방법은 X/Open Portability Guide 3권에 정의되어 있으며, 오늘날 거의 모든 유닉스 시스템 상에 구현되어 있다. 하지만 몇몇 유닉스 판매업체들이 그들만의 독자적인 버전을 고수함에 따라 표준화가 지연되고 있다. 이 방법의 기본 개념은 번역 가능한 메시지마다 번호를 부여하는 것이다. 번역자가 데이터 베이스에 저장된 번역문에 번호를 매겨 놓으면, 프로그램이 실행될 때 catgets 함수를 통해 적합한 메시지가 선택된다.
catgets 인터페이스는 사용하고자 하는 메시지 목록 파일을 여는 데 쓰이는 catopen, 메시지 표를 읽어 오는 데 쓰이는 catgets, 모든 일을 마친 뒤 열었던 파일을 닫는 catclose 등, 파일 액세스에 쓰이는 세 개의 함수로 이루어져 있다.. 이 함수들의 원형(prototype)은 <nl_types.h> 파일에 들어 있다.
catopen 함수는 아래처럼 쓰인다:
nl_catd catd = catopen("catalog_name", 0);
첫번째 인수는 목록의 이름을 나타내는데, 이것은 대개 프로그램이나 패키지의 이름을 지칭한다. 두번째 인수는 표준안에 정의되어 있지 않으므로 보통 0을 쓴다. 리턴 값은 메시지 목록에 대한 기술자 (descriptor)이며, open 함수에서 리턴하는 파일 기술자와 쓰임새가 같다.
실제로 번역문을 선택할 때에는 catgets를 사용한다.
char *translation = catgets(catd, set_no, msg_id, "original string");
첫번째 인수는 파일 기술자, 두번째 인수는 msg_id에 의해 지정되는 메시지가 속해 있는 집합을 나타낸다. 네번째 인수는 번역문을 찾는 데 실패했을 경우 내정값으로 쓰이는 문자열이다.
작업을 종료할 때에는 catclose을 사용한다.
catclose(catd);
catgets는 상당히 간단하고 합리적인 방법같지만, 실제로 각 메시지에 고유한 번호 (msg_id)를 부여하기란 여간 어려운 일이 아니다. 번호를 기호로 치환한다든지 하는 시도가 있었으나 근본적인 해결책은 되지 못했다. 따라서 현재 이 방법을 사용하는 프로그램은 그리 많지 않다.
이 방식은 원래 유니포럼(Uniforum)에서 제안되었으며, 그 후 주요 유닉스 판매업체 중의 하나인 썬 마이크로시스템 (Sun Microsystems, Inc.)이 이를 채택하고 구현했다. 그러나 아직까지 공식적인 표준은 발표되지 않은 상태이다.이 방법의 가장 큰 특징은 일반적인 파일 처리 과정 (열고-쓰고-닫는)을 명시적으로 거치지 않으며, 메시지 자체를 ID로 한다는 데 있다.
이 방법을 쓰려면 우선 textdomain 함수를 써서 영역명을 초기화한다.
char *textdomain(const char *domain_name);
domain_name은 대개 현재 프로그램이 속해 있는 패키지를 가리킨다. 여기에 NULL을 지정하여 함수를 호출하면 현재 값을 돌려준다. 메시지 목록이 속해 있는 디렉토리를 지정해 줄 필요가 있을 땐 bindtextdomain을 사용한다.
char *bindtextdomain(const char *domain_name, const char *dir_name);
한편 가장 기본적인 함수의 원형은 다음과 같다:
const char *gettext(const char *msgid);
gettext 함수는 msgid와 대응되는 번역문을 목록에서 읽어들여 돌려 준다. 대응하는 문장을 찾지 못했을 때에는 msgid를 돌려 준다. 이와 같이 gettext는 간단한 필터로 동작하기 때문에, 번역할 문자열을 둘러 싸는 것만으로 국제화된 메시지를 출력할 수 있다는 장점이 있다.
국제화된 프로그램이 일단 실행되면, 아래의 메시지 목록 파일을 읽어 들여 사용하게 된다:
msg-path/locale/LC_MESSAGES/messasges.mo
msg-path는 컴파일 시 결정되는 값으로, 보통 /usr/local/share나 /usr/share를 가리킨다. locale은 현재 선택되어 있는 로케일을 가리키며, 변수 LANG의 값을 갖는다.
.po 파일은 수많은 엔트리를 담고 있으며, 각 엔트리는 원래의 번역되지 않은 메시지와 그에 대응하는 번역문으로 이루어져 있다. 엔트리의 구조는 다음과 같다.
white-space
# translator-comments
#. automatic-comments
#: reference...
msgid untranslated-string
msgstr translated-string
공백 문자와 주석은 무시된다. 주석에는 두 가지 종류가 있는데, 하나는 주석 기호 바로 뒤에 문자가 오는 경우이고, 다른 하나는 그렇지 않은 경우이다. 전자는 프로그램에서 부가한 것으로, 메시지 목록을 갱신할 때 보존되지 않는다. 반면 후자는 번역자가 부가한 것으로, 별도의 옵션이 주어지지 않는 한 보존된다. 문자열은 C 언어의 문법을 지원한다. 따라서 다음과 같은 문자열이 가능하다.
msgid "%s: %d files found\n"
msgstr "%s: %d 개의 파일을 찾았습니다\n"
msgid ""
"Here is an example of how one might continue a very long string\n"
"for the common case the string represents multi-line output.\n"
GNU gettext는 메시지 번역에 필요한 모든 것을 가지고 있는 강력한 도구로서, 리눅스, 프리비에스디, 솔라리스 등 대부분의 유닉스 환경을 지원한다.
GNU gettext가 설치되어 있다고 가정하고, 다음과 같은 프로그램을 국제화하려고 한다.
main()
{
printf("Hello everyone!\n");
}
초기화를 한 뒤 번역할 문장을 gettext로 둘러 싸고, 필요한 헤더 파일을 포함시켜 주면 아래와 같이 된다.
#include <libintl.h>
#include <locale.h>
main()
{
setlocale(LC_ALL, "");
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
printf(gettext("Hello everyone!\n");
}
이제 xgettext를 실행하면 표시된 문자열이 po 파일로 추출된다.
다음 매크로를 쓰면 훨씬 편리하다:
#define _(String) gettext(String)
...
printf(_("Hello everyone!\n"));
...
void func(int idx)
{
const char *tab[] = {_("one"), _("two"), _("three")};
puts(tab[idx]);
}
와
void func(int idx)
{
const char *tab[] = {"one", "two", "three"};
puts(_(tab[idx]);
}
느 모두 제대로 실행되지 않는다: 전자는 배열의 초기값을 알 수 없고, 후자는 문자열이 마크되지 않아 xgettext로 추출할 수 없기 때문이다. 이 문제는 다음과 같이 함으로써 해결할 수 있다:
void func(int idx)
{
const char *tab[] = {N_("one"), N_("two"),
N_("three")};
puts(_(tab[idx]));
}
N_은 GNU 패키지에서 흔히 쓰이는 매크로로, 다음과 같이 정의되어 있다:
#define N_(String) (String)
이 매크로는 xgettext가 마크를 인식하고 메시지를 추출할 수 있도록 하기 위한 것이다.
비영어권 알파벳 문자를 표현하기 위한 8비트 확장 ASCII 코드이다. 대부분의 유럽 언어는 이 문자 집합을 사용하고 있다.
EUC는 유닉스에서 한국어, 중국어, 일본어 등의 아시아권 문자를 지원하기 위한 방법이다.이것은 ISO-2022 방식에 몇가지 제약을 가한 것으로, 한 로케일이 4 개의 문자 집합을 사용할 수 있다. 각 문자 집합은 미리 정해진 94, 94*94, 94*94*94, ... 개의 조합 형태를 가질 수 있다. 한국어의 경우 조합 가능한 글자의 개수가 11172자이므로 2 바이트 (94*94=8836)로 모든 문자를 수용할 수 없다. 따라서 이런 방식을 쓰는 현재의 유닉스 시스템에서는 2바이트 조합형 한글을 사용하기 힘들다. 자모마다 2바이트씩 부여하는 방법(최대 6바이트)도 있으나 일반적으로 쓰기엔 메모리 낭비가 너무 크다. 또 한 가지 이 방식의 단점은 한중일 세 나라의 코드가 전혀 호환성이 없다는 점이다. 같은 한자 문화권임에도 불구하고 코드표 상에서 한자의 위치가 모두 다르다.
최근에 공식 발표된 유니코드 2.0은 2 바이트의 코드 안에 전세계 주요 언어를 배열한 방법이다. 한국어의 경우 11172자가 모두 채택되었으며, 조합형 자모가 함께 제공되어 고어도 표현할 수 있다. 한자의 경우 부수 순서에 따라 배치되었으며, 각 나라의 코드가 겹치는 일은 더 이상 발생하지 않는다. 이 방식은 전세계의 주요 언어를 충돌없이 함께 표현할 수 있는 유일한 코드이지만 아직 널리 쓰이지는 않고 있다. 현재 리눅스를 비롯한 몇몇 유닉스에서 지원하고 있다.