프로그래머의 관점에서 본 gettext

프로그래머의 관점

 

GNU gettext에 들어 있는 현재와 같은 메세지 목록이 구현된 한 가지 목적은, 설치하는 사람이 시스템 메세지 목록을 사용하고 싶을 때 그렇게 하기 위함이었다. 그러므로 우리는 먼저 우리가 이미 알고 있는 몇 가지 방법들을 살펴봐야 할 것이다. POSIX 위원회의 사람들은 우리가 아래에서 설명할 거의 공식적인 표준의 한 가지에 대해 의견을 일치하지 못했다. 사실 그 사람들은 어떤 것에 대해서도 의견일 일치할 수 없었고, 그 어느 것도 인터페이스의 사용예를 포함하는 것조차 결정하지 못했다. 주요 유닉스 공급자들은 가장 중요한 두가지 스펙중 어느것을 사용하느냐에 따라 두개로 갈렸다: 한 가지는 X/Open의 catgets이고 또 하나는 Uniforums의 gettext 인터페이스이다. 우리는 두가지 모두를 설명하고, 나중에 이 딜레마에 대한 우리의 방법을 설명하겠다.

catgets에 대하여

catgets는 X/Open Portability Guide, Volume 3, XSI Supplementary Definitions, Chapter 5에 정의되어 있다. 하지만 몇몇 유닉스 공급자들은 이 표준을 그대로 따른다면 너무 느릴 것이라고 생각해서 이 표준의 최초 버전에 대해 유닉스 공급자들 자신들이 직접 구현했다. 물론 이 현상은 플랫폼 독립적인 프로그램을 작성할 때의 문제로 이어졌다: catgets를 사용하더라도 유일한 인터페이스를 보장하지 못한다.

한 가지 여기에 대한 개인적인 의견으로, 오직 몇몇 위원회 멤버만이 이 인터페이스를 구현할 수 있었다. 그들은 정말로 이 인터페이스를 사용해서 프로그램하려고 하지 않았다. catgets는 빠르고, 메모리를 절약하도록 구현되었고, 사용자는 catgets에 만족할 수 있다. 하지만 프로그래머들은 catgets를 증오한다 (최소한 나와 몇몇 다른 사람들은 그렇다...)

하지만 한 가지 잊어서는 안 된다: 그 사람들이 최소한 관계되어 있는 Unix(tm)에 대한 모든 권리가 (catgets 스펙을 낸 곳이기도 한) X/Open으로 넘어가는 데 따른 문제이다. 이렇게 되면 이 인터페이스가 미래의 유닉스 표준이 될 수도 있고 (예를 들어 Spec1170) 모든 유닉스 구현물의 (구현물, 이 구현물은 Unix라는 이름을 씌우도록 허락된 OS들이다) 일부가 될 수 있다.

인터페이스

catgets의 인터페이스는 파일 접근과 관련된 3개의 함수로 구성되어 있다: 사용할 목록을 여는 catopen, 메세지 테이블을 접근하는 catgets, 그리고 작업이 끝났을 경우에 쓰는 catclose이다. 이 함수들에 대한 프로토타입과 필요한 정의문들은 <nl_types.h> 헤더 파일에 있다.

catopen은 다음과 같이 쓰인다:

nl_catd catd = catopen ("catalog_name", 0);

이 함수는 목록의 이름을 인자로 받는다. 이 이름은 보통 프로그램이나 패키지의 이름이다. 두번째 인자는 catgets 표준에서 지정되어 있지 않다. 나는 이 두번째 인자가 여러 가지 시스템들 사이에 동일하게 구현되어 있는지도 알지 못한다. 그러므로 보통 이 값으로 0을 사용하라고 조언한다. 리턴값은 메세지 목록의 핸들로, open이 리턴하는 파일의 핸들과 동일하다.

이 핸들은 물론 다음과 같이 catgets 함수에서 사용된다:

char *translation = catgets (catd, set_no, msg_id, "original string");

첫번째 인수는 목록 디스크립터이다. 두번째 인수는 이 목록에 들어 있는 메세지의 집합을 지정하는데, 이 목록 안에서 msg_id에 설명된 메세지를 얻을 수 있다. 즉 catgets는 다음과 같이 세 단계에 걸쳐 번역문을 찾는다:

목록 이름 => 메세지 집합 번호 => 메세지 고유번호 => 번역문

네 번째 인자는 번역문을 찾는데 쓰이지 않는다. 네번째 인자는 위의 단계중에 하나라도 실패했을 때 기본값으로 사용된다. 반드시 기억해야 할 중요한 점은 catgets의 리턴 타입이 char *이지만 그 리턴된 문자열을 바꾸어서는 안 된다. 이 타입이 const char *이면 좋겠지만, 불행히도 catgets 표준은 1988년에 ANSI C 표준보다 1년 전에 발표되었다.

이 3가지 함수중에 마지막 함수는 누구나 예상할 수 있는 대로 동작한다:

catclose (catd);

이 함수를 사용한 뒤에는 이 디스크립터를 사용한 어떤 catgets도 사용할 수 없다.

catgets 인터페이스의 문제?!

이 디스크립터는 정말로 쉬워보인다 -- 이 디스크립터가 우리가 문제가 있다고 말하려는 부분이다. 사실 이 인터페이스는 아무런 문제없이 사용될 수도 있지만, 메세지 목록을 만드는 작업이이 고통스럽다. 그 이유는 catgets의 세번째 인자에 있다: 유일한 메세지 ID가 있어야 한다. 이 값은 어떤 한 가지 집합 내에서 모든 메세지들에 대해 유일한 숫자 값을 가져야 한다. 아마 소스 코드를 바꾸면서 이 리스트를 유지해야 할 때 문제점을 예상할 수 있을 것이다. 여기저기에서 새로운 메세지를 추가하고, 지우고 할 경우를 생각해 보자. 물론 이런 혼란을 없애는 많은 도구들이 개발되었지만, 어떤 도구는 추가하는 부분에서 제대로 동작하지 않고 또 어떤 도구는 지우는 부분에서 문제를 일으킨다. 우리는 또 다른 접근방법이 문제가 없다고 말하려는 것은 아니지만, 적어도 catgets보다는 훨씬 더 사용하기 쉽다.

gettext에 대하여

gettext 인터페이스의 정의는 한 유니포럼 제안서에서 나와서 최소한 한개의 주요 유닉스 공급자(썬)가 최근의 개발품들에서 사용하고 있다. 그럼에도 불구하고 어떤 공식적인 표준으로 지정되지는 않았다.

이 방법의 중요한 점은 표준적인 파일 처리 방법(열기-사용하기-닫기)을 따르지 않고, 프로그래머가 그런 많은 작업, 특히 유일한 키 처리와 같은 작업을 하는 부담을 지우지 않는다. 물론 유일한 키는 필요하지만, 이 키는 그 메세지 자체이다(그 메세지의 길이가 길던 짧던 간에). 이 두가지 방법에 대한 보다 자세한 비교는 See section 두가지 인터페이스의 비교.

다음에서 이 인터페이스에 대한 보다 자세히 설명한다. 이 인터페이스는 GNU gettext 라이브러리가 선택한 인터페이스이므로 자세히 설명해 놓았다. 이 라이브러리를 사용하고자 하는 프로그래머는 아래 설명에 관심을 가질 것이다.

인터페이스

인터페이스에 필요한 최소한의 기능은 (가) 이 문자열이 어디서 나왔는지를 도메인을 선택 (모든 프로그램에 한개의 도메인을 쓰는 건, 만들고 관리하는 데 어렵기 때문에 좋지 않다. 아마도 이렇게 만들고 관리하기는 불가능할 것이다), 그리고 (나) 선택된 도메인 내의 문자열을 읽는 것이다.

여기에서는 gettext 인터페이스의 기초를 설명한다. gettext에는 사용할 도메인을 제한하는 한개의 전역 도메인이 있다. 물론 이 도메인은 따로 선택할 수 있다.

char *textdomain (const char *domain_name);

textdomain으로 LC_MESSAGES 범주의 현재 전역 도메인의 상태를 바꾸거나 상태를 알아볼 수 잇다. 인자는 null로 끝나는 문자열로, 파일 이름으로 사용할 수 있는 문자로 이루어져야 한다. 만약 domain_name 인자가 NULL이면, 이 함수는 현재 값을 리턴한다. 어떤 값이 지정되어 있지 않으면, 기본 도메인의 이름이 리턴된다: 이 기본도메인의 값은 messages이다. textdomain의 값은 char *이지만 이 리턴 주소내의 값을 바꾸면 안 된다. 또, 그 값이 정말로 사용가능한지 검사하지 않는다는 걸 명심해야 한다. 만약 어떤 이름을 사용할 수 없는 경우에는, 메세지 번역은 일어나지 않을 걸로 사용할 수 없다는 걸 알 수 있다..

textdomain으로 결정된 도메인을 사용하려면 다음을 사용한다.

char *gettext (const char *msgid);

이 함수는 누구든 무슨 일을 하는지 알아 챌 수 있는 가장 간단한 함수이다. msgid 문자열에 대한 번역이 현재 도메인에 있으면 그 번역문이 리턴된다. 번역문이 없으면 첫번째 인수값 자체가 리턴된다. 인자가 NULL일 경우 그 결과는 정의되지 않았다.

한 가지 명심해야 할 점은 어떤 도메인을 사용할지 명확히 지정하지 않는다는 점이다. 현재 LC_MESSAGES 로케일에 대한 도메인 값이 사용된다. 만약 프로그램 내에 똑같은 gettext 호출 사이에 이 값이 달라진다면, 이 두번의 호출은 각각 다른 메세지 목록을 참조한다.

가장 쉬운 경우, 국제화된 패키지에서 보통 사용되는 방법은, 일단 teextdomain을 실행한 다음 도메인을 유일한 이름으로 결정하는 것이다. 보통 그 도메인 이름은 패키지의 이름이 된다. 이렇게 하면 모든 문자열은 gettext 함수를 통과해서 번역된다. 이제, 이 패키지는 여러분의 모국어로 말하게 된다.

애매함을 해결하기

한 개의 도메인 이름은 대부분의 프로그램에서 잘 동작하지만, 한 개 이상의 도메인에서 번역문을 가져와야 하는 경우도 있다. 물론 textdomain을 이용해 여러 개의 도메인 사이를 왔다갔다 할 수도 있지만, 쓰기에 편하지도 않고 빠르지도 않다. 가능한 상황 한 가지를 지금 생각할 수 있다: 모든 오류 메세지들은 error라는 별도의 도메인에 넣는다. 이렇게 하면 이 오류메세지에 대한 번역문은 한 가지만 갖고 있으면 된다. 또 다른 경우는 라이브러리의 메세지의 경우이다. 이 경우는 응용 프로그램의 도메인에 관계없이 동작해야 한다.

이러한 이유로 문자열을 가져오는 함수가 두 개가 더 있다.

char *dgettext (const char *domain_name, const char *msgid);
char *dcgettext (const char *domain_name, const char *msgid,
                 int category);

이 두 함수 모두 다 새로운 첫번째 인자를 받는데, 이 인자는 textdomain의 인자에 해당한다. dcgettext의 세번째 인자는 LC_MESSAGES 이외의 로케일을 쓸 경우를 위한 것이다. 하지만 나는 이 dcgettext가 정말로 쓸모가 있는지 의문이다. domain_nameNULL이거나 category가 알려진 값 이외의 값을 가지면, 그 결과는 정의되어 있지 않다. 또 dcgettext는 솔라리스에 있는 또 다른 gettext에는 들어 있지 않다.

또 다른 애매함이 발생할 수 있는데, 한 개 이상의 도메인이 같은 이름을 가지는 경우이다. 이 경우는 필요한 메세지 목록 파일이 들어 있는 곳을 직접 지정해서 해결할 수 있다.

char *bindtextdomain (const char *domain_name,
                      const char *dir_name);

이 함수를 부르면 주어진 도메인을 지정된 디렉토리 내의 파일(이 파일이 정확히 무엇인지는 아래에 설명한다)을 사용하도록 한다. 특히 시스템의 기본 위치에 있는 파일이 지정된 파일과 다르면 쓰지 않는다 (textdomain만 사용한 경우). dir_name으로 NULL 포인터를 넘기면 domain_name과 관계있는 파일이 리턴된다. domain_nameNULL이면 아무 일도 일어나지 않고 NULL 포인터가 리턴된다. 다른 함수들과 마찬가지로 리턴값을 바꾸면 안 된다!

중요한 점 한 가지로, dir_name 인수로 상대 경로를 쓰면 문제가 발생할 수 있다. 이 경로는 현재 디렉토리에 대해 상대적으로 계산되기 때문에 프로그램이 chdir 명령을 쓰면 결과가 달라질 수 있다. 상대 경로는 이런 문제에 대한 의존성을 없애고, 동작하지 않을 가능성을 없애기 위해 절대 쓰지 말아야 한다.

메세지 목록 파일 찾기

여러 가지 패키지에 대해 여러 가지 언어로 된 번역문들이 저장되기 때문에, 메세지 목록 파일에 대한 정보를 추가할 방법이 있어야 한다. 유닉스 환경에서 보통 사용되는 파일 이름내에 저장하는 것이고, 여기서도 그렇게 한다. bindtextdomain의 두번째 인자에 주어진 디렉토리(혹은 기본 디렉토리) 다음에 로케일의 이름과 도메인의 이름이 연결된다:

dir_name/locale/LC_category/domain_name.mo

dir_name의 기본값은 시스템에 따라 다르다. GNU 라이브러리의 경우, GNU 관습에 다르는 패키지들의 경우, 이 디렉토리는:

/usr/local/share/locale

locale은 위의 LC_category의 로케일 이름이다. gettextdgettext에서 이 LC_category는 언제나 LC_MESSAGES이다. dcgettext는 세번째 인자로 이 로케일을 지정한다.(3) 로케일의 값은 setlocale (LC_category, NULL)의 결과를 통해 알아 낸다. (4)

gettext가 사용할 출력 문자의 문자셋 지정하기

gettext는 메세지 목록에 들어 있는 번역문만 찾는 것이 아니다. gettext는 번역문을 원하는 출력 문자셋으로 그 자리에서 변환해 준다. 번역자가 메세지 목록을 만들 때 쓴 문자셋과 다른 문자셋에서 사용자가 작업중일 때 이 기능이 매우 유용하게 쓰인다. 이 기능으로 문자셋만 다른 메세지 목록을 여러개 배포할 필요가 없어지게 되었다.

출력 문자셋은 기본값으로 nl_langinfo (CODESET)의 값인데, 이는 현재 로케일의 LC_CTYPE에 의존한다. 하지만 문자열을 로케일 독립적인 방법(예를 들어 UTF-8)으로 저장한 프로그램의 경우는 gettext와 관련 함수들이 특정 인코딩으로 리턴하도록 bind_textdomain_codeset 함수를 이용해 요청할 수 있다.

gettextmsgid 인자는 문자셋 변환과 관계가 없다는 점에 유의한다. 또, gettextmsgid에 대한 번역문을 찾지 못할 때 msgid를 있는 그대로 리턴한다 -- 현재 출력 문자셋이 무엇인지와 관계가 없다. 그러므로 모든 msgid는 US-ASCII 문자열로 만들기를 권한다.

Function: char * bind_textdomain_codeset (const char *domainname, const char *codeset)
bind_textdomain_codeset 함수는 domainname 도메인의 메세지 목록에 대한 출력 문자셋을 지정하는 데 쓰인다. codeset 인자는 iconv_open 함수에서 쓸 수 있는 문자셋 이름이거나, 널 포인터이어야 한다.

만약 codeset 인자가 널 포인터라면, bind_textdomain_codesetdomainname의 도메인에 선택된 문자셋을 리턴한다. 만약 어떤 문자셋도 선택되지 않은 경우에 NULL을 리턴한다.

bind_textdomain_codeset 함수는 여러번 쓰일 수 있다. 이 함수가 동일한 domainname과 함께 여러번 사용될 경우, 나중에 사용된 것이 전에 사용된 설정을 덮어쓰게 될 것이다.

bind_textdomain_codeset 함수는 선택된 코드셋의 이름이 들어 있는 문자열을 가리킬 것이다. 문자열은 함수 내부에 들어 있고 사용자에 의해 변경되어서는 안 된다. 만약 bind_textdomain_codeset 사용중에 메모리가 부족해 질 경우 리턴값은 NULL이고 전역변수 errno가 세팅될 것이다.

복수형을 위한 함수들

지금까지 설명된 gettext 계열의 함수들(그리고 catgets의 함수들)은 실제 세계에서 지금까지의 접근방식들을 모두 무색케하는 한 가지 문제가 있다. 그것은 복수형을 처리하는 것이다.

국제화를 생각하기 전에 (그리고, 안타깝게도 생각한 이후에도) 유닉스 소스 코드를 살펴본 사람들은 다음과 비슷한 코드를 본 적이 있을 것이다:

  printf ("%d file%s deleted", n, n == 1 ? "" : "s");

사람들이 이 코드를 국제화하는 과정에서 불평을 하면, 사람들은 이와 같은 수식을 완전히 없애거나 "file(s)"와 같은 문자열을 사용한다. 이 두 가지 모두 부자연스럽고 사용해서는 안 되는 것이다. 이 문제를 올바르게 해결한 첫 번째 해결 방법은 다음과 같다:

   if (n == 1)
     printf ("%d file deleted", n);
   else
     printf ("%d files deleted", n);

하지만, 이 역시 문제를 해결하지 못한다. 위 코드는 명사의 복수형이 단순히 `s'를 첨가하는 것이 아닌 언어를 제외한 모든 언어에서 적용된다. 여기에서 또 다시 사람들은 자신의 언어에 적용되는 규칙이 모든 언어에서도 마찬가지라고 믿는 오류를 범한다. 하지만 복수형을 처리하는 것은 언어에 따라 매우 다르다. 예를 들어, Rafal Maszkowski(<rzm@mat.uni.torun.pl>)은 다음과 같이 알려 주었다:

폴란드어에서는 plik(file)를 다음과 같이 쓴다:

1 plik
2,3,4 pliki
5-21 pliko'w
22-24 pliki
25-31 pliko'w

와 같이 쓴다 (o'는 8859-2의 oacute를 말하며 okreska라고 한다. aogonek와 비슷하다.)

언어들 사이에 다른 것이 두 가지가 있다 (혹은 한 어족 안에서도 다르다).

  • 복수형이 만들어 지는 형식이 다르다. 이는 예외가 많은 언어의 경우 문제가 된다. 예를 들어, 독일어는 심한 경우이다. 영어와 독일어는 같은 어족(독일계)에서 나온 것임에도 불구하고, 명사의 복수형을 만드는 보편적인 (`s'를 뒤에 첨가하는) 규칙은 독일어에서는 찾아 볼 수가 없다.
  • 복수형의 수가 다르다. 이 사실은 로마어족과 독일어족의 언어만을 접한 사람들에게는 충격적일 것이다. 그 언어들에서는 복수형이 (두가지로) 동일하기 때문이다. 하지만 다른 어족에서는 오직 한 개, 혹은 여러 개의 복수형이 존재한다. 여기에 대한 정보는 별도의 절에서 설명한다.

결론은 응용프로그램 개발자가 이 문자를 코드 내에서 해결하려고 해서는 안된다는 것이다. 복수형의 처리는 특정 언어의 환경에 대해서만 유용하기 때문에 지역화의 범주에 들어간다고 봐야 할 것이다. 코드에서 해결하는 대신에 확장된 gettext 인터페이스가 사용되어야 한다.

이 확장 함수들은 한 개의 키 대신에, 두 개의 문자열과 숫자를 키로 받는다. 여기에 들어있는 아이디어는 숫자와 첫 번째 문자열을 키로 사용해, 번역자가 지정한 규칙에 따라 올바른 복수형이 선택되도록 하는 것이다. 두 개의 문자열 인자는 메세지 목록이 없을 경우에 리턴 값으로 사용될 것이다 (보통의 gettext 함수의 동작 방식과 같다). 이 경우에 독일계 언어의 규칙이 사용될 것이고 단수형에는 첫 번재 문자열 인자가 쓰이고, 복수형의 경우에는 두 번째가 쓰인다.

결과적으로 언어 메세지 목록이 없는 프로그램의 경우 프로그램이 독일계 언어로 쓰인 경우에 올바른 문자열이 표시되게 된다. 이는 한 가지 제한점이 되지만 GNU C 라이브러리는 (GNU gettext 패키지도 마찬가지로) GNU 패키지의 일부로 개발되었고, GNU 프로젝트의 코딩 표준은 프로그램이 영어로 만들어 지도록 규정하고 있기 때문에, 이 해결방법은 그 목적에는 부합한다.

Function: char * ngettext (const char *msgid1, const char *msgid2, unsigned long int n)
ngettext 함수는 gettext 함수와 비슷하게 메세지 목록을 같은 식으로 찾는다. 하지만 ngettext는 두 개의 인자가 더 있다. msgid1 인자는 번역할 문자열의 단수형이다. 이 문자열은 목록을 찾는 키로도 쓰인다. msgid2 인자는 복수형이다. n은 복수형을 결정하는 데 쓰인다. 메세지 목록을 찾을 수 없는 경우에 n == 1이면 msgid1이 리턴되고, 그렇지 않은 경우 msgid2를 리턴한다.

이 함수를 사용하는 예는 다음과 같다:

printf (ngettext ("%d file removed", "%d files removed", n), n);

nprintf 함수에도 넘겨줘야 한다는 것에 유의한다. ngettext 함수에만 넘겨주는 것만으로는 안 된다.

Function: char * dngettext (const char *domain, const char *msgid1, const char *msgid2, unsigned long int n)
dngettext는 메세지 목록이 선택되는 방법에서 dgettext 함수와 비슷하다. 차이점은 복수형을 위한 두 개의 인자를 더 받는다는 것이다. 이 인자는 ngettext와 같은 방식으로 이용된다.

Function: char * dcngettext (const char *domain, const char *msgid1, const char *msgid2, unsigned long int n, int category)
dcngettext는 메세지 목록이 선택되는 방법에서 dcgettext 함수와 비슷하다. 차이점은 복수형을 위한 두 개의 인자를 더 받는다는 것이다. 이 인자는 ngettext와 같은 방식으로 이용된다.

그러면, 이 함수들이 어떻게 복수형 문제를 해결하는 데 쓰이는가? 언어학자들이 가르쳐 주지 않는다면 (아직 아무도 가르쳐 주지 않았다) 복수형의 형식이 몇 가지밖에 없는지, 혹은 그 복수형 숫자가 지원하는 언어가 새로 생길 때마다 늘어 날지 알 수 없었다.

그러므로 여기에서 만들어진 방법은 번역자가 복수형을 어떻게 결정할 지 규칙을 지정하는 것이다. 그 형식은 각 언어에 따라 다르므로, 이 방법은 복수형에 대한 정보를 코드에 하드코딩하는 경우를 제외하면 (이 경우에도 이 방법을 써서 새로운 언어를 지원하는 데 문제가 없도록 해야 할 것이다) 모두 적용되는 방법이다.

복수형 선택에 대한 정보는 PO 파일의 헤더 항목(msgid 문자열이 비어있는 항목)에 들어 있다. 복수형 정보는 다음과 같다:

Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;

nplurals=2 값은 이 언어에 얼마나 많은 복수형이 들어 있는지를 지정하는 십진수 수이다. plural 다음에 오는 문자열은 C 언어 문법을 쓰는 수식이다. 예외적으로 음수는 허용되지 않고, 숫자는 십진수여야 하며, 허용되는 변수는 n뿐이다. 이 수식은 ngettext, dngettext, 혹은 dcngettext가 쓰일 때마다 계산된다. 이 함수에 넘겨진 숫자 값은 이 수식에서 변수 n이 사용된 모든 곳에 쓰여진다. 결과값은 0보다 같거나 커야 하며 nplurals에 주어진 값보다 작아야 한다.

현재 다음 규칙이 알려져 있다. 각 언어는 그 어족과 함께 열거되어 있다. 하지만 (아래 표에서도 알 수 있듯이) 다음 규칙이 전체 어족에 대하여 모두 적용되는 일반적인 규칙이라는 뜻은 결코 아니다.(5)

한 가지 형식:
어떤 언어들은 오직 한 개의 단수형만 존재한다. 단수형과 복수형의 구분은 없다. 이 경우에 헤더 항목은 다음과 같이 될 것이다:
Plural-Forms: nplurals=1; plural=0;
이러한 언어는 다음과 같다:
Finno-Ugric 어족
헝가리어
아시아 어족
일본어, 한국어
터키/알타이 어족
터키어
두 가지 형식, 단수형은 1에만 쓰이는 경우
영어가 사용하는 방식이기 때문에 대부분의 현존하는 프로그램에서 사용하는 방식이다. 헤더 항목은 다음과 같다:
Plural-Forms: nplurals=2; plural=n != 1;
(주의: 불리언 식이 0 아니면 1의 값을 가진다는 C 수식의 특징을 이용했다.) 이러한 언어는 다음과 같다:
독일 어족
Danish, Dutch, 영어, 독일어, 노르웨이어, 스웨덴어
Finno-Ugric 어족
Estonian, Finnish
Latin/Greek 어족
그리스어
Semitic 어족
히브리어
Romanic 어족
이탈리아어, 포르투갈어, 스페인어
인공 언어
에스페란토
두 가지 형식, 단수형은 0과 1에 사용됨
어족에서 예외적인 경우이다. 헤더 항목은 다음과 같다:
Plural-Forms: nplurals=2; plural=n>1;
이러한 언어는 다음과 같다:
Romanic 어족
프랑스어, 브라질식 포르투갈어
세 가지 형식, 0에 특수한 경우
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;
해당 언어는 다음과 같다:
Baltic 어족
라트비아어
세 가지 형식, 1과 2에 특수한 경우
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;
이러한 언어는 다음과 같다:
Celtic
Gaeilge
세 가지 형식, 1[2-9]로 끝나는 숫자에 특수한 경우
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=3; \
    plural=n%10==1 && n%100!=11 ? 0 : \
           n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2;
이러한 언어는 다음과 같다:
Baltic 어족
Lithuanian
세가지 형식, 1, 2, 3, 4로 끝나는 수에 특수한 경우, 1[1-4]로 끝나는 경우 제외
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=3; \
    plural=n%10==1 && n%100!=11 ? 0 : \
           n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
이러한 언어는 다음과 같다:
Slavic family
크로아티아어, 체코어, 러시아어, Slovak, 우크라이나어
세 가지 형식, 1과 2, 3, 4로 끝나는 수에 특별한 경우
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=3; \
    plural=n==1 ? 0 : \
           n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
이러한 언어는 다음과 같다:
Slavic 어족
폴란드어
네 가지 형식, 1과 02, 03, 04로 끝나는 모든 수에 대해서 특수한 경우
헤더 항목은 다음과 같다:
Plural-Forms: nplurals=4; \
    plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;
이러한 언어는 다음과 같다:
Slavic 어족
슬로베니아어

GUI 프로그램에서 gettext 사용하기

gettext 함수가 큰 문제가 있는 부분이 바로 그래픽 사용자 인터페이스(GUI)에서 사용될 경우이다. 문제는 번역되야 할 문자열이 매우 짧다는 것이다. 이 문자열들은 풀다운 메뉴에 나타나야 하고 그 길이에 제한을 받는다. 하지만 전체 문장에 포함되지 않는 문자열이나 최소한 문장의 큰 부분이 프로그램에서 두 번 이상 나타나지만, 번역이 다른 경우도 있다. 이는 GUI 프로그램에서 자주 쓰이는 한 단어로 된 문자열의 경우 특히 그렇다.

결과적으로 많은 사람들이 gettext 접근 방식이 틀리고 이러한 문제가 없는catgets을 사용해야 한다고 주장한다. 하지만 gettext 함수에서 이 문제를 해결하는 아주 간단하고 강력한 방법이 있다.

예를 들어 다음의 가상적인 경우를 생각해 본다. GUI 프로그램이 다음과 같은 메뉴바를 가지고 있다고 하자:

+------------+------------+--------------------------------------+
| File       | Printer    |                                      |
+------------+------------+--------------------------------------+
| Open     | | Select   |
| New      | | Open     |
+----------+ | Connect  |
             +----------+

File, Printer, Open, New, Select, 그리고 Connect 문자열을 번역해야 할 때 gettext 함수를 부르는 부분이 코드 내에 있을 것이다. 하지만 Open을 부르는 부분은 두 군데가 있을 것이다. 번역이 다를 수도 있고, 이 경우에 위에서 설명한 딜레마에 빠진다.

한 가지 해결 방법은 인공적으로 문자열을 구분되도록 늘리는 것이다. 하지만 번역이 없다면 어떻게 할 것인가? 늘려진 문자열은 표시되어서는 안 된다. 그러므로 약간 수정된 버전의 함수를 사용해야 한다.

문자열을 늘리려면 공통된 방법을 사용해야 한다. 예를 들어 위의 예에서 문자열은 다음과 같이 만들어 질 수 있다:

Menu|File
Menu|Printer
Menu|File|Open
Menu|File|New
Menu|Printer|Select
Menu|Printer|Open
Menu|Printer|Connect

이제 모든 문자열이 달라졌고, gettext 대신에 다음의 간단한 wrapper 함수가 사용된다. 모든 것이 잘 동작한다:

  char *
  sgettext (const char *msgid)
  {
    char *msgval = gettext (msgid);
    if (msgval == msgid)
      msgval = strrchr (msgid, '|') + 1;
    return msgval;
  }

이 작은 함수가 하는 역할은 번역문이 없을 경우를 판단하는 것이다. 리턴 값이 입력한 값인지 포인터 비교를 통해서 매우 효율적으로 알아 낼 수 있다. 번역문이 없다면 입력한 문자열이 메뉴 항목에 사용한 형식인 것을 알 것이고 | 문자가 들어 있을 것이다. 우리는 단순히 이 문자가 마지막으로 나타난 곳을 찾고 그 뒤에 있는 문자들을 리턴한다. 그것뿐이다!

만약 이 늘린 문자열 형태를 자주 사용하고 gettextsgettext로 (이 함수는 GUI의 아주 일부의 경우에만 쓰일 수 있다) 대체한다면 국제화 할 수 있는 프로그램을 만드는 게 가능할 것이다.

다른 gettext 함수들 (dgettext, dcgettextngettext 계열 함수들) 역시 같은 방법으로 인자의 갯수는 다르지만 또 다른 버전의 함수를 만들어 낼 수 있을 것이다.

그러면 당연히 왜 그러한 함수가 GNU gettext 패키지에 없는 것일까라는 의문이 든다. 이 의문에 대한 답으로 두 가지 이유가 있다.

  • 이러한 함수는 매우 만들기 쉽고 이러한 함수가 사용되는 프로젝트에서 제공될 수 있다. 이 답은 다음의 두 번째 답과 함께 사용되어야 할 것이다:
  • gettext 패키지가 어디에서든 동작할 수 있는 버전의 함수를 만들어 낼 방법이 없다. 문제는 늘려진 문자열에서 원 문자열을 분리할 때 접두어를 분리하는 문자를 무엇으로 하느냐이다. 위의 예는 |를 사용했는데, 이것은 문맥에 적합한 문자이기도 하고 메세지 문자열에 잘 쓰이지 않는 문자이기도 하기 때문이다. 하지만 그 문자가 메세지 문자열에 이용되는 경우는 어떻게 되는가? 혹은 선택한 문자가 컴파일되는 기계에 존재하지 않는 문자일 경우 (예를 들어, |는 ISO C에서 꼭 필요한 것이 아니다; 이것은 ISO C 프로그래밍 환경에서 `iso646.h' 파일이 존재하기 때문이다).

한 가지 더 말할 것이 있다. 위에서 필요한 래퍼 함수에서 번역 문자열 자체는 늘려지지 말아야 한다. 늘려진 msgid는 논리적인 것이다. 문자열을 구분할 필요는 없으며 (그 문자열들은 키로 쓰여지는 것이 아니기 때문이다) 이렇게 함으로써 약간의 메모리와 디스크 공간을 절약한다.

*gettext 함수를 최적화하기

여기서, GNU gettext를 사용할때 생기는 좋은 점에 대해 얘기한다. 몇몇 독자들은 국제화된 프로그램에서 어떤 문자열이 반복문 안에서 번역될 경우, 속도가 느려질 수 있다고 지적할 수도 있다. 그 반복문 안에서 문자열이 경우에 따라 바뀔 때는 어쩔 수 없지만, 문자열이 언제나 같은 경우는 시간낭비일 뿐이다. 다음 예를 보자:

{
  while (...)
    {
      puts (gettext ("Hello world"));
    }
}

여러번 실행되는 사이에 로케일이 바뀌지 않는 한, 번역된 문자열의 값도 항상 같다. 한 가지 방법은:

{
  str = gettext ("Hello world");
  while (...)
    {
      puts (str);
    }
}

하지만 이 방법은 모든 경우에 대해 사용할 수 없고 (예를 들어 로케일 선택이 달라질 경우) 읽기 좋지도 않다.

이러한 이유로, GNU gettext는 이전의 번역 결과에 대해서 캐싱한다. 같은 번역이 두 번 요청되면서 중간에 새로운 메세지 목록을 읽어들이지 않았다면, gettext는 두 번째의 경우 결과를 한 번의 캐쉬 검색으로 찾아낼 것이다.

두가지 인터페이스의 비교

계속할 얘기는 약간 팔이 안으로 굽는 얘기다. 위에서 말한 바와 같이 우리는 GNU gettext를 유니포럼의 제안서에 맞게 구현했고 그럴만한 이유가 있었다. 하지만 어떻게 이런 결정을 내리게 되었는지 알려줘야 할 것이다.

맨 처음에 우리는 개발 과정에 대해 생각했다. 우리가 gettext에 들어 있는 고유어 지원 기능을 사용해 응용 프로그램을 작성할 때, 보통 이렇게 한다. 일단 사용자에게 출력되는, 즉 번역되야 하는 문자열을 만나면 "..." 대신에 gettext("...")를 사용한다. 우리는 각 소스 파일의 시작 부분에 (혹은 기본 헤더 파일에) 이렇게 정의한다.

#define gettext(String) (String)

이 정의는 시스템이 C 라이브러리 내에 gettext 함수를 지원하면 쓰지 않을 수 있다. 일단 이 코드를 컴파일하면 결과는 NLS 코드가 사용되지 않은 것과 동일하다. GNU gettext 코드를 살펴보면 gettext("...") 대신에 _("...")를 사용햇다는 것을 알 수 있다. 이렇게 하면 문자열을 번역하기 위해 더 써야 할 글자의 갯수를 3자로 줄일 수 있다.

이제 생산적인 프로그램이 필요하므로, 앞의 정의문을 이렇게 바꾼다

#define _(String) (String)

혹은

#include <libintl.h>
#define _(String) gettext (String)

마지막으로, 번역할 수 있는 문자열이 들어 있는 모든 소스 코드에 대해 `xgettext' 프로그램을 실행시키면 된다: 우리는 사용가능한 번역문들에 의존하지 않으면서 동작하는 프로그램이 있지만, 이 프로그램은 언제든지 사용가능한 번역문이 생기면 그 번역문을 사용할 수 있다.

같은 과정이 gettext_noop에 사용된다 (see section 번역될 수 있는 문자열의 특별한 경우). 일단 gettext_noop를 아무것도 하지 않는 매크로로 정의한다. 그 다음에 다음 코드를 사용할 수 있다:

#define gettext_noop(String) (String)
#define N_(String) gettext_noop (String)

N__와 비슷한 간략한 형태이다. GNU gettext `po/' 디렉토리의 `Makefile'은 기본적으로 이 간략한 형태들을 알고 있으므로 여기의 제안을 그대로 따르는 편이 더 쉬울 것이다.

catgets를 생각해 보자. 가장 큰 문제는 프로그래머의 일이다. 언제든지 번역될 수 있는 문자열을 만나면 어떤 숫자(혹은 정의된 상수)를 지정해야 하고, 이 숫자를 메세지 목록 파일에도 지정해야 한다. 프로그래머는 중복해서 항목을 쓰지 않도록, 중복된 메세지 ID를 쓰지 않도록 하는 것까지 신경을 써야 한다. GNU gettext 프로그램과 같은 질을 가진 메세지 목록을 만드려면 그 문자열에 대한 설명과 소스 코드의 위치를 메세지 목록에 주석으로 넣어야 한다. 이런 일은 거의 불가능한 임무(Mission: Impossible)이다.

하지만 사람들은 어떤 점에서는 catgets에 장점이 있다고도 말한다. 어떤 문자열 내의 단어가 있는데, 이 문자열이 여러 위치에서 사용되고, 그 단어가 경우에 따라 다르게 번역해야 경우이다. 예를 들어:

printf ("%s: %d", gettext ("number"), number_of_errors)

printf ("you should see %d %s", number_count,
        number_count == 1 ? gettext ("number") : gettext ("numbers"))

여기에서 우리는 "number" 라는 문자열에 대해 두번 번역해야 한다. 설령 영어 이외의 언어를 모른다고 해도 이 두 단어가 다른 의미를 가진다는 걸 알 수 있다. 독일어에서는 첫 번째는 "Anzahl"(번호)로 번역되고, 두번째는 "Zahl"(갯수)로 번역된다.

여기에서 위의 예는 정말 예외적인 경우라고 말할 수 있다. 그리고 실제로 그렇다! 이 문제에서 우리 역시 이러한 경우는 예외적이라고 느끼고 있고, 이 문제는 그렇게 중요하지 않다고 결정을 내렸다. 위의 문제에 대한 해결책은 매우 쉽다:

printf ("%s %d", gettext ("number:"), number_of_errors)

printf (number_count == 1 ? gettext ("you should see %d number")
                          : gettext ("you should see %d numbers"),
        number_count)

우리는 이런 방법으로 모든 충돌을 피해갈 수 있다고 믿는다. 위 방법이 어렵다면 충돌이 생기는 문자열을 조금 다르게 바꿀 수도 있다. 어쨌든 이 문제에 대한 해결이 불가능하지는 않다.

catgets에서는 여러 번역에 대해서 같은 항목을 사용할 수 있다. 하지만 gettext는 이 경우에 대해 또 다른, 확장 가능한 애매함을 해결하는 방법을 사용한다: See section 애매함을 해결하기.

프로그램에 libintl.a 사용하기

`libintl.h'의 버전 0.9.4는 단독으로 사용할 수 있다. 즉, 별도의 함수를 쓰지 않고도 `libintl.h'를 사용할 수 있다. `Makefile'$(prefix)를 사용해 선택된 디렉토리에 헤더 파일과 라이브러리를 설치한다.

한 가지 예외는 HP-UX 10.01 시스템이다. 여기에서는 C 라이브러리에 alloca 함수가 없다 (그리고 HP 컴파일러는 alloca를 인라인하지 못한다). 하지만 이런 바보같은 시스템때문에 전체 라이브러리를 다시 작성하라는 건 아니다. 그 대신에 libintl.a를 사용하는 모든 패키지에 alloca 함수를 포함한다.

gettext 고수가 되기

GNU gettext 라이브러리의 기능을 완전히 활용하려면, 분명 소스코드를 읽는 것이 좋다. 하지만 (복잡하기도 한) 코드를 읽을 시간이 없는 사람들이 알아두면 좋을 만한 것은:

  • 실행시에 언어 바꾸기 대화적인 프로그램의 경우 실행시에 사용할 언어를 선택하면 좋다. 어떻게 하는지 이해하려면 gettext 함수가 사용할 언어를 사용할 것인지 어떻게 판단하는지 알아야 한다. 여기 보여주는 방법들은 gettext 함수의 GNU 버전에서만 정확히 동작한다. dcgettext 함수를 쓸 때마다 가장 큰 우선순위를 갖는 환경 변수의 현재 값이 검사되고 사용된다. 그 우선순위는 다음 리스트에 우선순위가 큰 것부터 열거되어 있다.
    1. LANGUAGE
    2. LC_ALL
    3. LC_xxx, 선택된 로케일에 따라
    4. LANG
    여기서 찾은 값에 따라 경로를 만들고, 거기에 번역 파일이 있으면 그 파일을 읽는다. 이제 예를 들어 LANGUAGE의 값이 바뀌었을 때 어떻게 되는지 생각해 보자. 위에 설명된 과정에 따라 dcgettext 함수를 쓰자 마자 변수가 새로운 값이 되었다는 걸 알아낸다. 하지만 이 경우 (아마도) 또다른 메세지 목록 파일이 이미 읽혀져 있다. 다른 말로 하면: 사용할 언어가 바뀌었다. 한 가지 방법이 있다. gcc-2.7.0 이상의 코드에서는 최적화 방법이 제공된다. 이 최적화는 새로운 목록이 읽혀지지 않으면 dcgettext 함수를 부르지 않도록 한다.. 하지만 dcgettext를 부르지 않으면 프로그램은 LANGUAGE 변수가 변했다는 걸 알지도 못할 것이다 (see section *gettext 함수를 최적화하기). 해결책은 매우 쉽다. 다음 줄을 언어를 바꾸는 부분에 추가한다.
      /* Change language.  */
      setenv ("LANGUAGE", "fr", 1);
    
      /* Make change known.  */
      {
        extern int  _nl_msg_cat_cntr;
        ++_nl_msg_cat_cntr;
      }
    
    _nl_msg_cat_cntr 변수는 `loadmsgcat.c' 파일에 정의되어 있다. 프로그래머는 오랫동안 실행되면서 실행시에 사용자가 사용할 언어를 선택하는 프로그램을 개발할 때만 위와 같은 방법을 쓸 것이다. 대화적이지 않은 (모든 자그마한 Unix 도구들처럼) 프로그램들은 위와 같은 방법이 전혀 필요없다.

프로그래머 장을 위한 임시 메모

임시 - 두개의 가능한 구현

언어에 관계없는 메세지 처리를 위한 방법으로 두가지가 경쟁하고 있다: X/Open의 catgets와, Uniforum의 gettext이다. catgets 방법은 메세지를 숫자로 인덱스한다; gettext 방법은 그 메세지의 영어 문자열로 인덱스한다. catgets 방법은 더 오래되었고 더 많은 유닉스 벤더들이 지원한다. gettext 방법은 썬이 지원하고, COSE multi-vendor initiative가 지원한다는 말을 들은 적이 있다. 이 두가지 방법은 모두 POSIX 표준이 아니다; POSIX.1 위원회는 이 분야에 대해 어떠한 합의도 보지 못했다.

아무것도 표준이 아니다. POSIX.1 위원회는 gettext를 사용할 지 catgets(XPG)를 사용할지에 대해 의견이 엇갈렸다. 결국 위원회는 아무 결론을 내리지 못했고 메세지 시스템에 관해서는 표준의 일부가 되지 못했다. 나는 POSIX 표준에 XPG3 메세지 인터페이스가 "...이미 구현된 메세지 시스템의 한 가지 예로서..." 포함될 것이라고 믿는다.

POSIX 위원회는 아주 조심스럽기 때문에, 어떤 다른 인터페이스를 사용하지 말고 어떤 특정 인터페이스를 사용하라고 말하지는 않을 것이다. 이 주제에 관해서는 Programming for Internationalized FAQ를 참조하기 바란다.

임시 - catgets에 대해

catgets를 기반으로 사용하는 문제에 관해 뒤늦게 몇 가지 토의가 있었다. 나는 이 토론에 대한 두가지 반응을 알리고 악마가 조금 더 좋아하는 것을 선택할 것이다 (역자 주: play devil's advocate for a little bit).

나는 catgets가 조금 더 잘 설계되야 한다는 점에는 부인하지 않는다. catgets는 이미 지적되어 온 바와 같이 너무 제한이 많다.

하지만, 일관성과 표준에 대해 얘기할 필요가 있다. 유닉스 소프트웨어를 만들 때 흔히 발생하는 문제는 유닉스 플랫폼들 사이의 포팅 가능성 문제이다. 모든 유닉스 벤더는 자기 운영체제를 보고 향상시킬 수 있는 부분을 찾는 것처럼 보인다. 의심할 나위 없이 이런 수정은 기술 혁신을 위해 필요하고, 문제를 해결한다. 하지만, 소프트웨어 개발자는 수많은 플랫폼들 사이에 이런 변화에 대응하는 데 너무 많은 시간을 소모한다.

그리고 이러한 문제는 유닉스 벤더들이 그들 시스템에 대한 표준화를 시작하도록 만들었다. 이 문제가 Spec1170가 나오도록 자극한 것이다. 모든 주요 유닉스 벤더는 이 표준을 지원하는 데 참가했고, 모든 유닉스 소프트웨어 개발자는 기뻐하면서 이 표준을 따르는 소프트웨어를 작성하고 그냥 각 플랫폼에 따라 다시 컴파일하면 되는 (autoconf를 사용할 필요없이) 날을 기다렸다.

내가 아는 바로는, Spec1170은 대략 X/Open Portability Guidelines 버전 4(XPG4)에 기초하고 있다. catgets와 관계된 것들이 XPG4에 정의되어 있기 때문에, catgets가 Spec1170의 일부가 되고, 모든 Unix 시스템의 표준 컴포넌트가 될 것이라고 믿게 되었다.

임시 - 왜 한 가지만 구현해야 하나

메세지 목록을 사용하기 위해 두가지 종류의 시스템을 설치하는 건 낭비로 보일 수도 있다. 우리가 catgets의 약점에 대해 비판한다면 완전히 새로운 시스템을 만드는 것보다는 catgets를 (호환성이 있도록) 확장하는 편이 좋지 않는가. 또 다른 한편으로는 한개의 운영체제에 두가지 메세지 목록이 서치돈 경우를 만날 수 있다 - 한개는 GNU gettext를 국제화 도구로 사용하는 패키지에서 쓰는 메세지, 또 하나는 그 외의 소프트웨어들이 (catgets) 사용하는 메세지. 쓸데없이 부풀어 있는 걸까?

다른 메세지 목록 접근 시스템을 구현한다고 가정해 보자. 어떤 시스템을 추천해야 할까? 최소한 리눅스에서는, 가능한 한 많은 소프트웨어 개발자를 끌어들어야 한다. 즉 우리는 가능한 한 그들의 소프트웨어가 쉽게 포팅되도록 해야 한다. 그리고 그것은 catgets에 대한 지원을 뜻한다. 우리는 libintl 코드를 우리의 libc 내에 구현했지만, 또다른 메세지 접근 방법도 우리 libc내에 포함해야 한다는 뜻인가? 그리고 libintl + catgets가 아닌 방법을 쓰려고 하는 사람들의 경우는 어떠한가. 그 사람들이 소프트웨어를 또다른 플랫폼으로 포팅할 경우, front-end (libintl) 코드와 back-end (catgets가 아닌) 코드를 모두 포함해야 한다.

하지만 메세지 목록 지원은 빙산의 일각에 불과하다. 여러 가지 로케일 범주들에 대한 데이타의 경우는 어더한가. 이 로케일 데이타도 많은 약점이 있다. 우리는 이것도 버리고 똑같은 목적의 또다른 라이브러리를 만들어야 하는가 (libintl을 메세지 목록 지원 이상으로 확장해야 하는가)?

지금까지 발전되어 온 많은 유닉스의 일부와 같이, 우리는 과거의 호환성을 지키면서 미래의 혁신을 위한 쓸만한 기능향상 사이를 잘 조화해야 할 난처한 입장에 놓여 있다.

임시 - 메모

X/Open은 아주 늦게 표준화를 통과시켰기 때문에, 많은 구현물의 최종 형태를 보면 저마다 다르다. 내가 가진 두개의 시스템(옛날 리눅스 catgets와 Ultrix-4)도 이상한 차이점이 있다.

좋다. 마지막으로 고친 것을 포함시켜서 GNU/리눅스 libc gettext 함수를 만드는 데 시간을 쏟아야 한다. 이제 미래에는 gettext를 가진 시스템은 솔라리스만이 아니다.

위로 스크롤