GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 11

From 흡혈양파의 번역工房
Revision as of 19:20, 9 June 2014 by Onionmixer (talk | contribs) (표 관련된 형식수정)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
제 11 장 애플리케이션 국제화하기

애플리케이션 국제화하기

GNOME은 국제화와 관한 한 매우 훌륭한 실적을 갖고 있는데 (종종 i18n으로 줄여 부른다), 공학적 변화를 거치지 않고 다양한 언어와 지역 설정에서 조정할 수 있는 애플리케이션을 디자인하는 방법에 해당한다. GNOME은 초기 버전부터 이미 많은 언어와 지역을 지원해왔다. 이것은 세계 어디서든 활용할 준비가 되도록 애플리케이션을 만들 수 있는 훌륭한 기회를 제공한다.


이번 장은 애플리케이션을 미국 외 다른 국가로 배포하는 것을 목적으로 할 경우 매우 중요한 내용을 포함한다. 이유가 뭘까? 그것은 기본적으로 GNOME(을 비롯해 다른 하위시스템도)이 인터페이스 언어로 영어를 사용하고, 미국을 기준으로 한 일자, 시간, 숫자, 통화 포맷을 사용하기 때문이다. 미국 밖의 시장을 목표로 삼을 경우 이번 장에 소개된 단계를 따라야 한다. 본 장에서 구체적으로 다룰 주제는 아래와 같다.

  • 로케일(locale) 소개
  • i18n 기본구조 부트스트랩하기(bootstrapping)
  • UI 텍스트 번역하기
  • 지역화 과정


그럼 시작해보자!


로케일 이해하기

GNOME은 i18n을 작동하기 위해 내부적으로 "Portable Operating System Interface(POSIX)"(이식 가능 운영 체제 인터페이스) 로케일 시스템을 이용한다. 로케일은 특정 지역에서 말하는 언어의 특정 변형어(variant)로 명시된 스크립트에 적힌 문화적 매개변수들을 포함한다. 매개변수는 로케일에 사용된 문자 집합, 언어 코드, 일자와 시간의 표현, 숫자, 통화, 주소, 전화번호, 측정치(measurement)가 포함된다.


각 로케일은 로케일 코드로 식별된다. 그리고 로케일 코드는 아래의 포맷으로 정의된다.

language[_territory][.codeset][@modifier]


예를 들어 인도네시아어로 말하고 UTF-8 문자 집합에서 Jawi 스크립트에 쓰인 아체(Aceh) 언어의 로케일은 ac_ID.UTF-8@Jaw 로 코딩된다. 각괄호 안의 부분은 의미가 분명한 경우 종종 생략되기도 한다. 코드의 언어 부분은 ISO-639로 코딩되고, 지역은 ISO-3166으로 코딩되며, 스크립트는 ISO-15924 표준으로 코딩된다.


각 매개변수에는 다른 매개변수가 할당될 수 있다. 가령, 전부 네덜란더어로 번역된 데스크톱을 사용하면서 서부 아랍식 수치 포맷과 미국식 측정 및 통화 설정을 사용할 수 있는 것이다. 하지만 GNOME은 혼동을 줄이기 위해 제한된 설정만 제공한다.


설정은 보통 사용자가 데스크톱으로 로그인할 때 시스템으로 적용된다. 사용자가 설치 중에 로케일을 선택하지 않으면 시스템은 보통 C 로케일(POSIX 로케일로도 알려짐)을 사용한다. POSIX 자체는 운영체제들 간 호환성을 유지하기 위해 IEEE에서 발표한 국제 표준이다. 이 표준은 기본 로케일인 POSIX 로케일을 포함하고 있으며, 다른 문화적 선호도를 지원하도록 확장이 가능하다.

GNOME3 Chapter11 01.png


위의 그림은 애플리케이션이 로케일을 어떻게 사용하는지를 보여준다. 우측에는 데이터 집합체가 있다. "Source string"(소스 문자열)은 소스 코드로부터 온 텍스트로, 애플리케이션에 표시되는데, 라벨 텍스트, 헤더, 이미지의 캡션, 정보 텍스트 중 하나에 해당한다. "Translation"(번역)은 미국식 영어, 영국식 영어, 인도네시아어, 일본어 등 많은 인간 언어로 소스 문자열을 번역한 결과의 집합이다. "Locale data"(로케일 데이터)는 포맷팅과 (예: 숫자와 일자의 표현) 텍스트 내용에 (요일을 목표 언어로 표시) 영향을 미친다. 소스 문자열은 도중에 API로 전달되고, 최종 문자열로 변형된 후 우측에 표시된다.


번역과 로케일 데이터는 애플리케이션의 모든 목표 시장 지역에 맞춰 준비되어야 한다. 혹 누락된 경우 소스 문자열이 어떠한 변형도 거치지 않고 표시되는데, 소스 코드에 뭐가 적혀있든 번역이나 포맷팅 변경 없이 표시될 것이란 의미다.


실행하기 - 이용 가능한 로케일 얻기

이용 가능한 로케일 리스트는 시스템에서 쉽게 확인할 수 있다. 애플리케이션을 번역할 수 없는 이유를 디버깅해야 할 경우 우리의 삶을 매우 수월하게 만들기 위해 꼭 숙달해야 하는 첫 번째 명령이다. 이를 어떻게 실행하는지 살펴보자.

  1. Terminal을 열어라.
  2. 아래의 명령을 실행하라.
    locale -a.
    
  3. 시스템에서 이용 가능한 로케일 리스트가 표시될 것이다 (자신의 컴퓨터에 표시된 실제 출력은 다를 수 있다). 리스트 예를 들어보겠다.
    C
    C.UTF-8
    en_US.utf8
    POSIX
    


무슨 일이 일어났는가?

앞의 리스트에서는 시스템에 4개의 로케일이 설치되어 있음을 확인할 수 있다. 국제 부호화 문자 집합(Universal Character Set)과 UTF-8로 되어 있는 C와 UTF-8 로케일로 된 미국식 영어가 있다. 즉, 애플리케이션의 번역이 네 개밖에 없다는 의미다. C와 POSIX는 번역에 전혀 사용되지 않는 것이 보통이며, 소스 코드에서 사용되는 언어인 소스 언어에 의해서만 사용된다 (프로그래밍 언어가 아니라 텍스트 표현에서 사용된 인간 언어). 이러한 사실은 하나의 번역, 즉 영어로 된 번역만 가질 수 있다는 의미가 된다.


문자 집합은 데이터가 화면에, 저장공간에서, 그리고 데이터 전송 시 어떻게 표시되는지에 영향을 미친다. 이는 "code point"(코드 포인트)라고 불리는 정수 숫자와 문자, 숫자, 부호, 그 외 타입의 문자 표현에 해당하는 문자(character) 간의 매핑에 해당한다. GNOME은 내부적으로 UTF-8을 사용하므로 이 문자 집합에 집중하도록 하겠다.


시도해보기 - 로케일 매개변수 탐구하기

모든 로케일 매개변수를 확인하려면 매개변수 없이 locale 을 실행해보라. 매개변수는 로케일 함수 변수(이름 앞에 LC_가 붙는다)를 설정함으로써 영향을 받는다. 어떤 매개변수가 어떤 변수에 의해 표현되는지 예측할 수 있는가?


실행하기 - 로케일 추가하기

이용 가능한 로케일 리스트에 영어만 표시된 경우 로케일을 시스템으로 추가해보자. 영어 이외의 로케일이 보인다면 이 단계를 건너뛰어도 좋다. 인도네시아 지역으로 된 인도네시아 로케일을 추가할 것인데, 그에 대한 코드는 id_ID가 된다. 물론 자신의 선호도에 따라 다른 로케일을 이용해도 좋다.

  1. /etc/locale.gen 파일을 superuser로 편집하라.
  2. id_ID.UTF-8을 표시하는 행을 찾아라.
  3. 행 앞에 해시(hash)로 표시된 주석 표시를 제거하라.
  4. 파일을 저장하라.
  5. Terminal을 열고 sudo locale-gen 명령을 실행하라.
  6. /etc/locale.gen 파일의 내용에 따라 출력이 다를 수 있다. 하지만 인도네시아어와 미국식 영어의 주석을 해제하면 아래와 같은 출력이 보일 것이다.
    Generating locales (this might take a while)...
    en_US.UTF-8... done
    id_ID.UTF-8... done
    Generation complete.
    
  7. 명령이 성공했는지 확인하기 위해서는 다시 terminal에 locale -a를 입력하라.
  8. 리스트에 아래와 같이 id_ID.utf-8 로케일을 확인할 수 있을 것이다.
    C
    C.UTF-8
    en_US.utf8
    id_ID.utf8
    POSIX
    


무슨 일이 일어났는가?

많은 문화에 대한 로케일 정보는 시스템에 이미 존재한다. 하지만 여전히 텍스트 형태로 되어 있어 이용할 수가 없다. 따라서 locale-gen 명령을 이용해 이진 표현을 생성하고, /etc/locale.gen 파일에 주석 없이 열거된 로케일을 이용할 수 있도록 해보았다. 이 명령을 이용하면 시스템 전체적으로 이용 가능해진다. 따라서 루트 권한이 필요하며 이를 성취하기 위해 sudo를 이용했다 (locale -a는 이러한 권한을 필요로 하지 않음을 주목한다).


실행하기 - 다른 로케일로 된 다른 출력 얻기

이제 미국식 영어 외에 로케일이 있으니 다음은 무엇을 할 차례일까? 매우 간단한 프로그램, 즉 일자(date) 프로그램으로 시작해보자.

  1. Terminal을 열어라.
  2. 아래를 입력하라.
    LC_ALL=C date
    
  3. 아래와 같은 출력을 확인하라.
    Sun Oct 28 15:41:38 EET 2012
    
  4. 아래의 명령을 입력하라 (id_ID.utf8을 앞서 활성화한 로케일로 변경할 것을 잊지 말라).
    LC_ALL=id_ID.utf8 date
    
  5. 출력을 확인하라. 아래와 비슷한 결과가 나왔을 것이다.
    Min Okt 28 15:41:45 EET 2012
    


무슨 일이 일어났는가?

여기서는 LC_ALL 환경 변수를 이용해 설정을 수동으로 구성하였다. LC_ALL 변수를 어떤 로케일 식별자로 설정함으로써 POSIX 시스템은 즉시 애플리케이션의 출력을 변경하기 위해 명시된 로케일을 사용한다. 앞서 언급했듯이 시스템에는 우리가 설정할 수 있는 매개변수가 많이 포함되어 있다. LC_ALL 변수는 값이 하나인 매개변수를 모두 설정하는 마법의 매개변수다.


사실상 일자 프로그램에 출력되는 내용은 순수하게 일자와 시간 데이터에 해당한다. 따라서 영향을 받는 매개변수는 LC_TIME이 된다.


시도해보기 - 잘못된(bogus) 로케일 식별자 사용하기

확인할 수 있듯이 출력은 우리가 로케일을 설정하는 즉시 인도네시아어로 번역된다. 잘못된 로케일 식별자를 제공해보면 (또는 오타를 만들어보면!) 더 이상 작동하지 않음을 확인할 수 있을 것이다. 또 다른 로케일이 설치되어 있다면 모두 실험해보길 바란다!


Vala 프로젝트에서 i18n 사용하기

i18n을 Vala에서 작용하도록 만들기 위해서는 몇 가지 단계를 실행할 필요가 있다. Vala 프로젝트를 생성하고 i18n 단계를 하나씩 실행해보자.


실행하기 - 기본구조 부트스트랩하기

처음으로 해야 할 일은 빌드 기본구조를 준비하는 일이다. 이 단계는 꽤 까다로울 수 있는데, 아무리 작은 내용이라도 빠뜨리게 되면 무엇이 잘못되었는지 찾는 데에 수 시간의 작업 내용을 날려버릴 수 있기 때문이다. 하지만 두려워하지 말고 얼음물에 점프해 물고기를 잡아보도록 하자!

  1. GtkBuilder를 지원하는 Vala 프로젝트를 생성하고 hello-i18n 이라고 부르자.
  2. configure.ac 파일을 편집하고, 아래의 코드를 참조함으로써 파일을 수정하라. 강조되지 않은 행들 간에 강조된 행을 삽입해야 한다. 올바른 실행을 확신할 수 없다면 전체 파일을 복사해서 붙이면 된다.
    AC_INIT(hello_i18n, 0.1)
    AC_CONFIG_HEADERS([config.h])
    AM_INIT_AUTOMAKE([1.11])
    AM_SILENT_RULES([yes])
    AC_PROG_CC
    
    LT_INIT
    IT_PROG_INTLTOOL()
    
    AH_TEMPLATE([GETTEXT_PACKAGE], [Package name for gettext])
    GETTEXT_PACKAGE=hello-i18n
    
    AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["$GETTEXT_PACKAGE"],
                [The domain to use with gettext])
    AC_SUBST(GETTEXT_PACKAGE)
    AM_GLIB_GNU_GETTEXT
    
    PACKAGE_LOCALE_DIR=[${datadir}/locale]
    AC_SUBST(PACKAGE_LOCALE_DIR)
    
    dnl Check for vala
    AM_PROG_VALAC([0.10.0])
    
    dnl Development mode
    AC_ARG_ENABLE(development,
        AS_HELP_STRING([--enable-development],[enable development mode]),
            enable_development="$enableval",
                enable_development=no)
    if test "x$enable_development" = "xyes"; then
        DEVELOPMENT_MODE="yes"
    fi
    
    AC_SUBST(DEVELOPMENT_MODE)
    AH_TEMPLATE([DEVELOPMENT_MODE], [Whether in development mode or
    not])
    AC_DEFINE_UNQUOTED([DEVELOPMENT_MODE], ["$DEVELOPMENT_MODE"],
                [Development mode])
    
    PKG_CHECK_MODULES(HELLO_I18N, [gtk+-3.0 ])
    
    AC_OUTPUT([
    Makefile
    src/Makefile
    po/Makefile.in
    ])
    
  3. 프로젝트 최상위 폴더에서 po라는 폴더를 생성하라.
  4. po 폴더에 POTFILES.in 이라는 새 파일을 생성하고, 파일을 아래의 코드로 채워라.
    [type: gettext/glade]src/hello_i18n.ui
    
  5. po 폴더에 LINGUAS라는 새로운 폴더도 생성하고, 지원하고자 하는 언어 코드로 내용을 채워라. 앞의 예제를 계속하자면 아래의 코드를 추가함으로써 파일에 인도네시아어를 추가할 수 있겠다.
    id
    
  6. 다른 로케일을 사용 중이라면 로케일 식별자 전체가 아니라 언어 코드를 넣으면 된다.
  7. 이 새로운 폴더에서 초기 번역 템플릿을 설정할 필요가 있다. Terminal을 열고 po 폴더 안에서 아래의 명령을 실행하라.
    intltool-update --pot
    
  8. 위의 명령의 결과로 hello-i18n.pot가 생긴다. 이는 빈 번역 파일로, 번역의 기반으로 사용할 것이다.
  9. hello-i18n.pot 파일을 복사하고 id.po로 명명한다. 다시 말하지만, 다른 언어를 사용 중이라면 파일명이 언어의 코드를 반영하도록 조정하라. 본래 .po 확장자는 그것을 이식 가능한 객체 파일로 표시하는데, 이는 오픈 소스 세계에서는 매우 자주 사용되는 번역 파일 포맷이다. 지금은 id.po 파일을 그대로 두고 후에 사용하도록 하겠다.
  10. 최상위 폴더에서 Makefile.am을 편집하되, 아래의 코드를 참조하여 내용을 수정하라. 원래 생성된 파일에서 강조된 부분만 추가 또는 수정된다.
        SUBDIRS = src po
    
    hello_i18ndocdir = ${prefix}/doc/hello_i18n
    hello_i18ndoc_DATA = \
        README\
        COPYING\
        AUTHORS\
        ChangeLog\
        INSTALL\
        NEWS
    
    EXTRA_DIST = \
        $(hello_i18ndoc_DATA) \
        intltool-extract.in \
        intltool-merge.in \
        intltool-update.in
    
    DISTCLEANFILES = \
        intltool-extract \
        intltool-merge \
        intltool-update \
        po/.intltool-merge-cache \
        $(NULL)
    
    # Remove doc directory on uninstall
    uninstall-local:
        -rm -r $(hello_i18ndocdir)
    
  11. src/config.vapi 파일을 편집하여 네임스페이스에 아래의 행을 추가하라.
    public const string DEVELOPMENT_MODE;
    
  12. src/Makefile.am을 편집하고 hello_i18n_VALAFLAGS 선언 뒤에 --Xcc='--include config.h' 를 추가하여 전체 코드가 아래와 같은 모양이 되도록 한다.
    hello_i18n_VALAFLAGS = \
        --pkg gtk+-3.0 --Xcc='--include config.h'
    
  13. 위의 파일에서 AM_CPPFLAGS 선언에 -include config.h를 삽입하여 전체 코드 선언이 아래와 같은 모습이 되도록 한다.
    AM_CPPFLAGS = \
        -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \
        -include config.h \
        -DPACKAGE_SRC_DIR=\""$(srcdir)"\" \
        -DPACKAGE_DATA_DIR=\""$(pkgdatadir)"\" \
        $(HELLO_I18N_CFLAGS)
    
  14. src/hello_i18n.vala를 편집하고 main 함수에 아래 코드를 추가하라.
        if (Config.DEVELOPMENT_MODE == "yes") {
            Intl.bindtextdomain(Config.GETTEXT_PACKAGE, "src/po");
    } else {
            Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.PACKAGE_LOCALE_DIR);
    }
    
    Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
    Intl.textdomain(Config.GETTEXT_PACKAGE);
    
  15. Built 메뉴에서 Configure Project... 를 클릭하여 프로젝트를 설정하고 대화창이 나타나면 Configure Options 에 --enable-develpment를 추가하라.
  16. 프로젝트를 빌드해보자.
  17. 첫 번째 단계에서 특별한 내용을 확인하지는 못하겠지만 빌드는 성공적으로 이루어질 것이다.


무슨 일이 일어났는가?

Anjuta가 기본구조 셋업을 알아서 제공해주지 않아 우리가 스스로 실행해보았다. 기본적으로 gettext와 intltool을 빌드 파이프라인으로 포함시키도록 autotools를 설정하는 일을 해보았다. Gettext 는 애플리케이션을 목표 지역 언어로 번역하도록 도와주는 라이브러리이고, intltool 은 번역 시스템에 필요한 데이터를 준비하도록 도와주는 라이브러리다.


번역 파일을 위한 기본 폴더도 제공하였다. 이러한 po 폴더는 UI 텍스트의 번역을 포함할 것이다. 하지만 지금으로선 아직 유용하지 않다.


또 한 가지 실행한 중요한 단계는 코드 설정 파일시스템을 추가한 일이다. configure.ac에서 GETTEXT_PACKAGE와 DEVELOPMENT_MODE가 코드에 의해 설정되도록 명시하였다. 이는 아래와 같은 행을 추가하여 실행되었다.

AH_TEMPLATE([DEVELOPMENT_MODE], [Whether in development mode or not])
AC_DEFINE_UNQUOTED([DEVELOPMENT_MODE], ["$DEVELOPMENT_MODE"], [Development mode])


그 결과 애플리케이션을 생성하면 config.h 라는 파일이 생성되고, 파일에서는 두 개의 변수를 이용할 수 있을 것이다. 이후 소스 코드(config.vapi, 그리고 생성된 C 언어 코드)는 값으로 접근할 수 있을 것이다. 이와 관련된 내용은 다음 활동에서 더 자세히 논하겠다.


마지막이지만 무엇보다 중요한 단계는 바로 메인 함수에 번역 초기화 단계를 넣는 일이다.

if (Config.DEVELOPMENT_MODE == "yes") {
    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, "src/po");
} else {
    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.PACKAGE_LOCALE_DIR);
}

Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
Intl.textdomain(Config.GETTEXT_PACKAGE);


bindtextdomain 함수는 번역 카탈로그의 검색 폴더를 (Gettext 용어로는 domain 이라 부른다) 명시된 폴더를 가리키도록 설정한다. 카탈로그명과 폴더는 인자에 명시된다. 카탈로그는 소스 언어(주로 영어로 쓰임)와 목표 언어 간 매핑을 포함한다. 함수는 기본적으로 번역을 폴더에서 이용할 수 있으며, 살펴볼 파일의 이름은 카탈로그명에서 명시된 이름과 동일함을 알려준다.


이 부분에는 DEVELOPMENT_MODE 값에 따라 두 개의 branch가 있다. DEVELOPMENT_MODE 모드에 있다면 번역은 src/po 폴더로 설정되고, 그 외의 모드는 시스템 폴더로 (주로 /usr/share/locale) 설정된다. DEVELOPMENT_MODE 값 자체는 프로젝트 설정에 -enable-development를 전달할 때 설정된다.


bind_textdomain_codeset 부분은 우리가 UTF-8 문자 집합을 사용하고 있음을 알려준다. 마지막으로 textdomain은 카탈로그를 현재 프로세스로 부착하므로 초기화 다음에 오는 gettext 함수는 카탈로그가 사용 중이라고 가정한다.


위는 모두 애플리케이션에서 다중 언어를 지원할 때 따라야 하는 단계들이다. 하지만 개발이 시작되는 동안에 한 번만 실행하면 끝나기 때문에 염려하지 말라.


한 가지 중요한 것은 프로젝트의 설정 대화상자에서 --enable-development를 제거해야 한다는 사실인데, 이 플래그 집합을 이용하면 코드는 시스템이 아니라 소스 코드 트리에서 번역을 찾을 것이기 때문이다.


실행하기 - UI 생성하기

기본구조가 준비되면 다음으로 넘어가 UI를 생성해보자.

  1. hello_i18n.ui 파일을 열어라.
  2. Box 위젯을 넣고 5개의 항목으로 나눠라.
  3. 처음 4개의 항목에는 각 항목마다 5개의 Label 컨트롤을 넣어라.
  4. 마지막 항목에는 Button 컨트롤을 넣어라.
  5. 라벨을 편집하라. 각 라벨마다 아래의 리스트에서 텍스트를 할당하라.
    This is a label
    Today's date is %x
    The price is %^=#6n
    The weight is %.3f %s
    
  6. 버튼에 대해서는 See you later를 텍스트로 설정하라.
  7. 라벨을 편집할 때는 텍스트박스의 오른쪽에 있는 타원형 버튼을 눌러라. 아래와 같은 대화상자가 나타날 것이다.
    GNOME3 Chapter11 02.png
  8. 모든 라벨에 (버튼의 라벨도 포함) Translatable 체크박스를 체크하도록 한다. 체크하지 않으면 애플리케이션을 전혀 번역할 수 없기 때문에 꼭 체크하도록 한다.
  9. hello_i18n_vala 파일이 아래의 코드 조각과 같은 모습이 되도록 수정하라.
    using GLib;
    using Gtk;
    
    public class Main : Object
    {
            /*
            * Uncomment this line when you are done testing and building a tarball
            * or installing
        */
    //const string UI_FILE = Config.PACKAGE_DATA_DIR + "/" + "hello_i18n.ui";
    const string UI_FILE = "src/hello_i18n.ui";
            /* ANJUTA: Widgets declaration for hello_i18n.ui - DO NOT REMOVE
    */
            Builder builder = null;
    
            public Main ()
            {
                try
                {
                    builder = new Builder ();
                    builder.add_from_file (UI_FILE);
                    builder.connect_signals (this);
    
                    var window = builder.get_object ("window") as Window;
                    /* ANJUTA: Widgets initialization for hello_i18n.ui - DO NOT
    REMOVE */
                    window.show_all ();
                }
                catch (Error e) {
                    stderr.printf ("Could not load UI: %s\n", e.message);
                }
    
                var b = builder.get_object ("button1") as Button;
                b.clicked.connect(()=>{
                    Gtk.main_quit();
                });
    
                var dateData = new Date();
                char buffer[100];
                    Time t = Time.local (time_t ());
    
                    var date = builder.get_object ("label_date") as Label;
                t.strftime (buffer, date.label);
                date.label = (string) buffer;
    
                var priceData = 9.99;
                var price = builder.get_object ("label_price") as Label;
                Monetary.strfmon(buffer, price.label, priceData);
                price.label = (string) buffer;
    
                weak string measurement = LangInfo.get_info(LangInfo.Item.
    MEASUREMENT);
                bool metric = (measurement[0] == 1);
    
                var weightData = 11.24;
                var weight = builder.get_object ("label_weight") as Label;
                weight.label = weight.label.printf(metric ? weightData :
    weightData * 2.20462, metric ? "Kg" : "Lb");
            }
    
            [CCode (instance_pos = -1)]
            public void on_destroy (Widget window)
            {
                Gtk.main_quit();
            }
    
            static int main (string[] args)
            {
                Gtk.init (ref args);
                if (Config.DEVELOPMENT_MODE == "yes") {
                    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, "src/po");
                } else {
                    Intl.bindtextdomain(Config.GETTEXT_PACKAGE, Config.PACKAGE_LOCALE_DIR);
                }
                Intl.bind_textdomain_codeset(Config.GETTEXT_PACKAGE, "UTF-8");
                Intl.textdomain(Config.GETTEXT_PACKAGE);
    
                var app = new Main ();
    
                Gtk.main ();
    
                return 0;
            }
        }
    
  10. 앞의 코드 블록에서 LangInfo.get_info와 Monetary.strfmon 함수를 시스템 내 어디에서도 이용할 수 없음을 눈치챌 것이다. 사실 glibc로부터 얻은 nl_langinfo와 strfmon 값은 우리가 추가해야 하는 두 개의 파일, langinfo.vapi와 monetary.vapi에 래핑한다. 따라서 처음에는 빈 파일을 새로 생성하고 langinfo.vapi로 명명한 후 아래 코드로 채운다.
    [CCode (cprefix = "", lower_case_cprefix = "", cheader_filename =
    "langinfo.h")]
    public class LangInfo {
                public enum Item
                {
            [CCode (cname="_NL_MEASUREMENT_MEASUREMENT")]
            MEASUREMENT
        }
        [CCode (cname="nl_langinfo")]
        public static weak string get_info (Item type);
    }
    
  11. 그 다음 빈 monetary.vapi 파일을 생성하고 아래 코드로 채워라.
    [CCode (cprefix = "", lower_case_cprefix = "", cheader_filename =
    "monetary.h")]
    public class Monetary {
        public static ssize_t strfmon(char[] s, string format, double
    data, ...);
    }
    
  12. src/Makefile.am을 편집하여 monetary.vapi와 langinfo.vapi를 hello_i18n_SOURCES 선언으로 추가하여 코드가 아래와 같이 되도록 하라.
    hello_i18n_SOURCES = \
        hello_i18n.vala config.vapi monetary.vapi langinfo.vapi
    
  13. 애플리케이션을 재빌드하여 실행하라. 아래와 같은 창이 확인될 것이다.
GNOME3 Chapter11 03.png


무슨 일이 일어났는가?

여기서 중요한 부분은 라벨을 번역 가능하게 만드는 것이다. 이는 intltool이 hello_i18n.ui 파일에서 텍스트를 알아낼 수 있도록 해준다.


다음으로 중요한 사항은 Monetary.strfmon과 LangInfo.get_info 함수의 (재)소개다. Monetary.strfmon은 strfmon 함수의 래퍼이고, 통화(monetary) 데이터를 문자열로 포맷팅하는 데에 사용된다. 통화 데이터는 로케일에 따라 완전히 통화 기호로 표시되어야 한다. 이 함수는 우리가 래핑하는 해당 C 헤더 파일명과 함수명을 나타내는 .vapi 파일을 제공함으로써 래핑된다. 이러한 경우 래핑이 간단한데, 이는 원래 함수명(strfmon)이 래퍼 함수의 이름(이 또한 strfmon이라 부름)과 동일하다는 의미다.


다음으로 nl_langinfo 함수의 래퍼인 LangInfo.get_info 함수가 있다. 이 함수는 로케일 설정 함수를 취하여 포인터나 문자열로 변환한다. 로케일 항목은 로케일 파일에서 특정 로케일 설정을 설명하는 항목이다. 이런 경우 두 가지 부분을 래핑한다. 첫 번째 로케일 항목은 우리가 열거로서 래핑하여 Item이라고 명명한다. 또 LC_MEASUREMENT 항목만 필요하므로 열거 안에서 래핑한다. 또 다른 항목이 필요하다면 래핑하여 열거에 넣어도 좋다. 열거 순서는 더 이상 중요하지 않은데, enum member를 아래 코드를 이용해 실제 C 언어 enum member로 직접 매핑하기 때문이다.

[CCode (cname="_NL_MEASUREMENT_MEASUREMENT")]
MEASUREMENT


즉, Vala에서 LangInfo.MEASUREMENT는 C의 _NL_MEASUREMENT_MEASUREMENT와 정확히 동일하다.


그 다음으로 wrap nl_langinfo 함수를 LangInfo.get_info로 래핑한다.

[CCode (cname="nl_langinfo")]
public static weak string get_info (Item type);


이름 앞에 nl_가 붙은 래퍼는 Vala에서는 의미가 통하지 않기 때문에 nl_langinfo 대신 get_info를 사용하는데, get_info는 함수가 하는 일을 반영한다. 리턴된 문자열을 Vala가 관리해선 안 된다는 점을 나타내기 위해선 함수에 weak 키워드를 이용한다.


실행하기 - UI 텍스트 번역하기

다음은 실제로 UI 텍스트를 번역할 단계다. UI 파일로부터 텍스트를 뽑아 번역하는 방법을 살펴보자.

1. Terminal을 이용해 po 폴더로 가자.
2. 다음 명령을 입력하여 첫 활동에서 복사한 id.po 파일을 업데이트하라.
intltool-update id
3. GNOME Activities 메뉴에서 Gtranslator 프로그램을 시작하라.
4. 처음으로 시작할 때 프로파일을 채울 필요가 있다. 프로파일에 이름을 제공하고 본인의 이름과 이메일 주소를 채워라. 다음 창에서는 번역하고자 하는 언어 프로퍼티에 대해 질문할 것이다. 언어 코드가 올바른지 확인하라 (ISO-639 참고). 문자 집합으로 UTF-88bit 를 입력하고 인코딩 값을 각각 전달하라. Team email 은 이 언어의 번역가가 사용하는 이메일 주소다. Plural forms 옵션은 단어가 복수 형태를 어떻게 사용할 것인지 결정한다. 인도네시아어에서 복수의 개수는 2이고, 단어가 참조하는 객체의 계수가 1보다 크면 단어는 복수 형태를 사용할 것이다. 따라서 nplurals=2; plural=(n>1) 옵션을 선택한다. 인도네시아어의 경우 아래와 같은 모습이 될 것이다.
GNOME3 Chapter11 04.png
5. Continue 를 클릭하고 GTranslator 의 셋업을 완료한 후 이 메뉴를 이용해 id.po 파일을 열어라.
6. 파일이 열리고 나면 이제 번역을 할 준비가 되었다. 리스트의 각 텍스트를 클릭하고 Translated 텍스트박스에 번역을 입력하라. %x, %s 등의 모든 포맷팅 표시는 요청된 경우를 (예: 데이터 포맷을 조정 시) 제외하고는 번역가에 의해 수정되거나 변경되어선 안 된다. 이러한 포맷이 표시되는 순서 또한 변경되어선 안 된다.
7. 아래의 번역 표를 참조하여 텍스트를 번역한다.
원본 텍스트 번역된 텍스트
This is a label Ini adalah label
Today's date is %x Tanggal hari ini adalah %x
The price is %^=#6n Harganya %^=#6n
The weight is %.3f %s Beratnya %.3f %s
See you later Sampai jumpa
8. 파일을 저장하라.
9. 애플리케이션을 재빌드하라.
10. Terminal에서 src 폴더 안에 po 폴더를 생성하라. po 폴더 안에서 id 폴더를 생성하라. 그리고 id 안에 LC_MESSAGES를 생성하라. 여기까지 완료되면 src/po/id/LC_MESSAGES라는 새 폴더가 생겼을 것이다.
11. 재빌드 후에 생성된 id.gmo 파일을 hello-i18n.mo로 재명명하고 앞의 단계에서 생성한 폴더로 넣는다. hello-i18n 이름은 앞에서 설정한 GETTEXT_PACKAGE 변수의 값과 정확히 동일해야 함을 주목한다.
12. Terminal을 통해 프로젝트의 최상위 폴더로 가서 아래와 같이 인도네시아어로 애플리케이션을 실행하라.
LANGUAGE=id LC_ALL=id_ID.utf8 src/hello-i18n
13. 아래 스크린샷과 같은 애플리케이션 창이 나타날 것이다. 놀랍지 않은가!
GNOME3 Chapter11 05.png


무슨 일이 일어났는가?

여기서는 UI 텍스트가 생성되고 번역되는 곳이다. 이후 결과가 되는 .po 파일이 번역기의 손에 들어가고 번역 큐로 들어간다. 이는 소스 코드에서 어떤 텍스트든 업데이트할 때마다 실행되어야 한다. 아래의 행을 입력하면 된다.

intltool-update id


위의 코드는 id.po 파일만 업데이트를 실행하며, 나머지 파일이 존재한다면 개별적으로 업데이트되어야 한다. 이 명령이 실제로 하는 일은 POTFILES 파일에 명시된 파일로부터 새로운 텍스트를 뽑아내어 기존의 id.po 파일로 합치는 것이다. 모호한 사항이 있으면 GTranslator 에서 텍스트가 흐릿한 상태로 표시되어 번역가에게 알릴 것이다. 이에 따라 번역가는 흐릿한 텍스트에 대해 새로운 번역을 제공하여 문제를 수정할 수 있다.


.po 파일 내 모든 문자열이 번역되고 나면 번역된 .po 파일들은 모두 po 폴더로 돌아가야 한다. 그리고 그곳에서 빌드를 실행한다. 사실 빌드가 하는 일은 아래의 명령을 실행시키는 것뿐이다.

msgfmt -cv id.po -o id.gmo


이 단계를 실행하면 텍스트 형태의 id.po를 이진 형태의 id.gmo로 변환한다. 애플리케이션은 하나의 이진 형태만 취할 수 있으므로 이 단계는 꼭 필요하다. 애플리케이션에서 bindtextdomain 함수는 우리가 개발 모드에 있을 때마다 폴더를 src/po 폴더로 설정한다. 사실상 이 함수는 src/po/<language-code>/LC_MESSAGES 폴더에서 이진 .gmo 파일을 검색할 것이다. 언어 코드는 LANGUAGE 환경 변수에 명시된 코드다. 이 변수가 명시되어 있지 않으면 LANG, LC_MESSAGES에서 언어 코드를 검색하고 마지막으로 LC_ALL 변수를 연속하여 검색할 것이다.


애플리케이션을 실행하기 위해서는 아래의 명령을 실행해야 한다.

LANGUAGE=id LC_ALL=id_ID.utf8 src/hello-i18n


이는 src/po/id/LC_MESSAGES에서 번역을 검색하기 위해 bindtextdomain 함수를 설정하고 (LANGAUGE 변수는 id로 설정되었기 때문에), 그 외의 경우 id_ID.utf8를 로케일 설정으로 사용한다. 그 결과 UI 텍스트가 인도네시아어로 번역되고, 통화, 숫자, 측정치 매개변수 또한 인도네시아어로 설정된다.


시도해보기 - 다른 로케일 설치하기

시도해 볼만한 또 다른 로케일이 설치되어 있다면 실험하기 좋은 기회가 된다. 예를 들어, LC_MEASUREMENT를 en_US.utf8(미국)으로 설정하고 LC_MONETARY를 de_DE.utf8(독일어)으로, 그리고 LC_TIME을 ar_SA.utf8(사우디 아라비아)으로 설정해보라. 결과가 아래 스크린샷과 비슷하게 나타나는가?

GNOME3 Chapter11 06.png


지역화 과정

앞의 과정을 통해 우리는 지역화를 위한 과정이 필요함을 깨달았을 것이다. "Localization"(지역화)은 (종종 L10n으로 줄여 씀) 국제화에서 더 나아간 단계에 해당한다. 여기서는 목표 시장 영역에 한정적인 실제 작업이 이루어진다. 이 과정에는 보통 애플리케이션을 빌드할 수 없는 번역가가 수반되기 때문에 꼭 필요하다.

GNOME3 Chapter11 07.png


위 그림은 매우 간소화된 L10n 프로세스를 표시한다. 소스 코드는 UI 디자인에 따라 개발자에 의해 생성되었다. 디자인은 형식적 문서나 목업이 가능하다. 개발자는 개발을 하면서 텍스트를 소스 코드와 UI 파일로 추가한다. 이러한 텍스트는 후에 목표 시장의 각 언어마다 .po 파일로 추출된다.


여기서 번역가들은 완전히 번역될 때까지 고유의 파일을 소유한다. 번역된 파일은 이후 개발자에게 넘겨진다. 개발자와 검사자는 테스트를 위한 빌드를 만들 수 있다. 중요한 단계가 다가올 때마다 번역과 함께 제품 빌드가 생성되어 소비자에게 전달된다. 삶에 도움이 되려면 훌륭한 기본구조를 마련할 필요가 있다. 앞서 설명한 전체적인 시나리오는 자동화되어야 하므로 각 단계들 사이에 어떠한 지연도 발생해선 안 된다.


실제로 빌드가 소비자에게 전달되기 전에는 각 로케일에 대한 일련의 테스트가 철저하게 실행되어야 한다. 대부분의 경우 번역가가 UI 텍스트를 번역하는 동안 갑자기 길을 잃곤 한다. 이러한 오류는 번역 파일에 주석을 사용함으로써 최소화할 수 있지만, 그 외에도 테스트가 중요한 이유가 있다. 바로 텍스트가 표시되는 방식에 관한 문제다. 텍스트가 번역에서 잘렸는지(truncated) 여부를 확인하는 검사가 있어야 한다. 만일 잘렸다면 공학적 과정을 실행해야 하는지(예: 텍스트 크기 축소) 아니면 번역가가 대안적 번역을 마련해야 하는지 확인하라. 검사자 역시 텍스트가 올바로 렌더링되었는지 여부를 확인해야 한다. 목표 시장에 만일 아시아국의 언어 또는 왼쪽에서 오른쪽으로, 오른쪽에서 왼쪽으로 쓰는 언어의 국가가 모두 포함되어 있다면 테스트는 더욱 더 심혈을 기울여 진행해야 할 것이다.


요약

본 장에서는 애플리케이션을 국제화에 더불어 지역화가 가능하도록 준비시키는 방법을 논했다. 국제화와 지역화의 차이는, i18n 부분에서는 소프트웨어가 다중 언어 및 다문화 정보를 표시할 수 있도록 확보하는 반면 L10n은 언어와 문화 설정을 애플리케이션으로 전달하기 위한 실제적 노력이라는 점이다. 다시 말해, i18n은 소프트웨어 L10n을 준비시킨다고 볼 수 있겠다.


본 장의 가장 앞 부분에서는 로케일의 소개로 시작했다. 이제 시스템에서 이용할 수 있는 로케일의 리스트를 얻을 수 있을 뿐만 아니라 로케일을 이용하도록 만드는 방법도 알게 되었다.


다음으로 Vala 프로젝트를 이용해 i18 기본구조를 부트스트랩하는 방법을 학습했다. 이 과정은 프로젝트의 초기 단계에서 실행되어야 한다. 그 다음은, UI 텍스트를 번역하도록 소스 코드를 준비시키는 방법을 학습하고, i18n 함수 중 일부를 이용해 시간, 통화, 측정치 데이터의 지역화를 실행해보았다. 그에 더해 번역가가 번역 파일을 어떻게 작업하는지도 살펴보았다.


마지막으로 L10n 프로세스의 예제를 논했다. 이 예제는 조직이 공학 프로세스를 어떻게 처리하는지에 따라 조정할 수 있다.


다음 장에서는 애플리케이션에서의 품질 제어에 관해 논하도록 하겠다. 따라서 소프트웨어 공학에서 최고의 실제를 GNOME 방식으로 적용할 것이다.


Notes