msgcat — Tcl 메시지 카탈로그
msgcat — Tcl 메시지 카탈로그
다국어 UI를 만든다면, 코드 안에 "Hello" 같은 문자열을 그대로 박아 두는 대신 번역 가능한 메시지로 관리하고 싶을 거예요. msgcat 패키지는 애플리케이션과 분리된 메시지 카탈로그(message catalog)를 두고, 로케일(locale)에 따라 문자열을 번역해서 보여주는 다국어 인터페이스 관리 기능을 제공합니다.
본문
package require Tcl 8.5
package require msgcat 1.6
::msgcat::mc src-string ?arg arg ...?
::msgcat::mcmax ?src-string src-string ...?
::msgcat::mcexists ?-exactnamespace? ?-exactlocale? src-string
::msgcat::mclocale ?newLocale?
::msgcat::mcpreferences
::msgcat::mcloadedlocales subcommand ?locale?
::msgcat::mcload dirname
::msgcat::mcset locale src-string ?translate-string?
::msgcat::mcmset locale src-trans-list
::msgcat::mcflset src-string ?translate-string?
::msgcat::mcflmset src-trans-list
::msgcat::mcunknown locale src-string ?arg arg ...?
::msgcat::mcpackagelocale subcommand ?locale?
::msgcat::mcpackageconfig subcommand option ?value?
::msgcat::mcforgetpackage
설명
msgcat 패키지는 다국어 사용자 인터페이스를 관리하는 데 쓸 수 있는 함수 모음을 제공해요. 텍스트 문자열은 애플리케이션과 독립적인 "메시지 카탈로그"에 정의되고, 이 카탈로그는 애플리케이션 소스 코드를 수정하지 않고도 편집하거나 지역화할 수 있어요. 메시지 카탈로그에 새 파일을 하나 추가하면 새 언어나 로케일을 제공할 수 있답니다.
msgcat은 네임스페이스로 패키지를 구분해요. 각 패키지는 msgcat 안에 고유한 메시지 카탈로그와 설정을 갖습니다.
로케일(locale)은 스위스 독일어를 뜻하는 de_ch처럼 사용자 언어를 설명하는 사양 문자열이에요. msgcat에서 현재 시스템의 시스템 로케일로 초기화되는 전역 로케일이 있어요. 각 패키지는 전역 로케일을 쓰거나 패키지 전용 로케일을 쓰기로 결정할 수 있어요.
전역 로케일은 필요에 따라 바꿀 수 있는데, 예를 들면 사용자가 시작한 언어 변경이나 웹 서버 같은 다중 사용자 애플리케이션 안에서요.
명령
::msgcat::mc src-string ?arg arg ...?
현재 로케일에 따라 src-string의 번역을 돌려줘요. src-string 뒤에 추가 인수가 주어지면, format 명령으로 src-string의 번역에 추가 인수들을 치환해요.
::msgcat::mc는 현재 네임스페이스에서 정의된 메시지 중 src-string의 번역을 찾아요. 없으면 현재 네임스페이스의 부모, 그다음엔 그 부모로 전역 네임스페이스에 도달할 때까지 계속 찾아요. 번역 문자열이 없으면 ::msgcat::mcunknown이 호출되고, 그 반환 문자열이 반환돼요.
::msgcat::mc는 애플리케이션을 지역화하는 데 쓰는 주요 함수예요. 영어 문자열을 직접 쓰는 대신, 애플리케이션은 영어 문자열을 ::msgcat::mc에 통과시켜 그 결과를 사용해요. 한 언어만 지원하도록 이런 식으로 작성된 애플리케이션은, 나중에 새 메시지 카탈로그 항목을 정의하는 것만으로 추가 언어 지원을 쉽게 덧붙일 수 있어요.
::msgcat::mcmax ?src-string src-string ...?
여러 소스 문자열이 주어지면, ::msgcat::mcmax는 가장 긴 번역 문자열의 길이를 돌려줘요. 지역화된 GUI를 설계할 때 유용한데, 예를 들어 모든 버튼을 고정 폭으로 만들어야 할 때(가장 넓은 버튼의 폭이 될) 필요한 값이에요.
::msgcat::mcexists ?-exactnamespace? ?-exactlocale? src-string
주어진 src-string에 대한 번역이 있으면 참을 돌려줘요.
-exactnamespace 옵션으로 검색을 현재 네임스페이스만 확인하고 부모 네임스페이스는 확인하지 않도록 제한할 수 있어요. -exactlocale 옵션으로 첫 번째 선호 로케일만 확인하도록 제한할 수도 있어요(전역 로케일을 쓰면 ::msgcat::mcpreferences가 돌려주는 첫 번째 요소).
::msgcat::mclocale ?newLocale?
로케일을 newLocale로 설정해요. newLocale을 생략하면 현재 로케일을 돌려주고, 그렇지 않으면 현재 로케일을 newLocale로 설정해요. msgcat은 로케일을 대소문자를 구분하지 않고 저장·비교하며, 로케일을 소문자로 돌려줘요. 초기 로케일은 사용자 환경에 지정된 로케일로 결정돼요. 로케일 문자열 형식에 대한 설명은 아래 로케일 사양 섹션을 참고해요.
로케일이 설정되면 로케일의 선호 목록이 평가돼요. 이 목록의 로케일 중 아직 로드되지 않은 것들은 지금 로드돼요.
::msgcat::mcpreferences
사용자의 언어 사양을 바탕으로 사용자가 선호하는 로케일의 정렬된 목록을 돌려줘요. 목록은 가장 구체적인 것부터 선호가 낮은 순서로 정렬돼요. 이 목록은 ::msgcat::mclocale이 msgcat에 설정한 현재 로케일에서 파생되며, 독립적으로 설정할 수 없어요. 예를 들어 현재 로케일이 en_US_funky라면, ::msgcat::mcpreferences는 {en_us_funky en_us en {}}를 돌려줘요.
::msgcat::mcloadedlocales subcommand ?locale?
패키지 로케일을 설정하지 않은 패키지에 대해 로드된 로케일 목록을 관리하는 명령 그룹이에요. get은 현재 로드된 로케일 목록을 돌려주고, present는 인수 locale을 요구하며 그 로케일이 로드되어 있으면 참을 돌려줘요. clear는 현재 선호 목록에 없는 모든 로케일과 그 데이터를 제거해요.
::msgcat::mcload dirname
지정된 디렉토리에서 ::msgcat::mcloadedlocales get이 돌려주는 언어 사양(패키지 로케일이 설정되어 있으면 msgcat::mcpackagelocale preferences)과 일치하는 파일을 찾아요. 모두 소문자이며 .msg 확장자가 붙어요. 일치하는 각 파일은 UTF-8 인코딩을 가정하고 순서대로 읽어요. 파일 내용은 Tcl 스크립트로 평가돼요. 즉 메시지 파일에 유니코드 문자가 UTF-8 인코딩 형태로 직접 있거나, Tcl 평가에서 인식하는 백슬래시-u 따옴표로 있어도 돼요. 사양과 일치해 로드된 메시지 파일의 개수가 반환돼요.
또한 주어진 폴더는 로케일 변경 시 필요한 메시지 카탈로그 파일을 로드하기 위해 msgcat 패키지 설정 옵션 mcfolder에 저장돼요.
::msgcat::mcset locale src-string ?translate-string?
지정된 로케일과 현재 네임스페이스에서 src-string의 번역을 translate-string으로 설정해요. translate-string을 지정하지 않으면 src-string이 둘 다로 사용돼요. 함수는 translate-string을 돌려줘요.
::msgcat::mcmset locale src-trans-list
지정된 로케일과 현재 네임스페이스에서 src-trans-list의 여러 소스 문자열에 대한 번역을 설정해요. src-trans-list는 짝수 개의 요소를 가져야 하며 {src-string translate-string ?src-string translate-string ...?} 형태예요. ::msgcat::mcmset은 ::msgcat::mcset을 여러 번 호출하는 것보다 크게 빠를 수 있어요. 함수는 설정된 번역 수를 돌려줘요.
::msgcat::mcflset src-string ?translate-string?
::msgcat::mcload를 통해 로드되는 메시지 카탈로그 이름이 암시하는 로케일을 위해, 현재 네임스페이스에서 src-string의 번역을 translate-string으로 설정해요. translate-string을 지정하지 않으면 src-string이 둘 다로 사용돼요. 함수는 translate-string을 돌려줘요.
::msgcat::mcflmset src-trans-list
::msgcat::mcload를 통해 로드되는 메시지 카탈로그 이름이 암시하는 로케일을 위해, 현재 네임스페이스에서 src-trans-list의 여러 소스 문자열에 대한 번역을 설정해요. src-trans-list는 짝수 개의 요소를 가져야 하며 {src-string translate-string ?src-string translate-string ...?} 형태예요. ::msgcat::mcflmset은 ::msgcat::mcflset을 여러 번 호출하는 것보다 크게 빠를 수 있어요. 함수는 설정된 번역 수를 돌려줘요.
::msgcat::mcunknown locale src-string ?arg arg ...?
::msgcat::mc가 현재 로케일에 src-string의 번역이 정의되어 있지 않을 때 이 루틴을 호출해요. 기본 동작은 인수가 있으면 format으로 처리한 src-string을 돌려주는 거예요. 이 프로시저는 애플리케이션이 재정의할 수 있는데, 예를 들어 알 수 없는 각 문자열에 대한 오류 메시지를 로그로 남기도록요. ::msgcat::mcunknown 프로시저는 ::msgcat::mc 호출과 같은 스택 컨텍스트에서 호출돼요. ::msgcat::mcunknown의 반환값이 ::msgcat::mc 호출의 반환값으로 사용돼요.
이 루틴은 관련 패키지가 패키지 로케일 unknown 명령 이름을 설정하지 않은 경우에만 호출된다는 점을 기억하세요.
::msgcat::mcforgetpackage
호출하는 패키지가 msgcat 패키지 안의 모든 설정·번역을 포함한 모든 상태를 비워요.
로케일 사양
로케일은 ::msgcat::mclocale에 전달되는 로케일 문자열로 msgcat에 지정돼요. 로케일 문자열은 언어 코드, 선택적 국가 코드, 선택적 시스템별 코드로 구성되며 각각 _로 구분돼요. 국가·언어 코드는 ISO-639와 ISO-3166 표준으로 지정돼요. 예를 들어 로케일 "en"은 영어를, "en_US"는 미국식 영어를 지정해요.
msgcat 패키지가 처음 로드될 때 로케일은 사용자 환경에 따라 초기화돼요. env(LC_ALL), env(LC_MESSAGES), env(LANG) 변수를 순서대로 검사해요. 이 중 처음으로 비어 있지 않은 값을 가진 것이 초기 로케일을 결정하는 데 사용돼요. 그 값은 XPG4 패턴 language[_country][.codeset][@modifier]로 파싱돼요. 그런 다음 초기 로케일은 language[_country][_modifier] 인수로 ::msgcat::mclocale을 호출해 설정돼요.
Windows와 Cygwin에서 그런 환경 변수가 하나도 없으면, msgcat은 레지스트리에서 로케일 정보를 추출하려 시도해요. Windows Vista부터는 RFC4747 로케일 이름 "lang-script-country-options"이 "lang_country_script"으로 변환돼요(예: sr-Latn-CS → sr_cs_latin). Windows XP에서는 언어 ID가 비슷하게 변환돼요(예: 0c1a → sr_yu_cyrillic). 사용자 환경에서 초기 로케일을 알아내려는 시도가 모두 실패하면, msgcat은 초기 로케일 "C"를 기본값으로 사용해요.
사용자가 로케일을 지정하면 문자열 번역 중 "최적 일치(best match)" 검색이 수행돼요. 예를 들어 사용자가 en_GB_Funky를 지정하면, 로케일 "en_gb_funky", "en_gb", "en", ""(빈 문자열)이 일치하는 번역 문자열을 찾을 때까지 순서대로 검색돼요. 번역 문자열이 없으면 unknown 핸들러가 호출돼요.
네임스페이스와 메시지 카탈로그
메시지 카탈로그에 저장된 문자열은 추가된 네임스페이스 기준으로 저장돼요. 이렇게 하면 여러 패키지가 서로 충돌할 걱정 없이 같은 문자열을 쓸 수 있어요. 또 소스 문자열을 더 짧게 만들고 오타 위험도 줄여줘요.
예를 들어 다음 코드를 실행하면:
::msgcat::mcset en hello "hello from ::"
namespace eval foo {
::msgcat::mcset en hello "hello from ::foo"
}
puts [::msgcat::mc hello]
namespace eval foo {puts [::msgcat::mc hello]}
이렇게 출력돼요:
hello from ::
hello from ::foo
메시지의 번역을 찾을 때, 메시지 카탈로그는 먼저 현재 네임스페이스, 그다음 현재 네임스페이스의 부모, 전역 네임스페이스에 도달할 때까지 계속 검색해요. 이렇게 하면 자식 네임스페이스가 부모 네임스페이스의 메시지를 "상속"할 수 있어요.
예를 들어("en" 로케일에서) 다음 코드를 실행하면:
::msgcat::mcset en m1 ":: message1"
::msgcat::mcset en m2 ":: message2"
::msgcat::mcset en m3 ":: message3"
namespace eval ::foo {
::msgcat::mcset en m2 "::foo message2"
::msgcat::mcset en m3 "::foo message3"
}
namespace eval ::foo::bar {
::msgcat::mcset en m3 "::foo::bar message3"
}
namespace import ::msgcat::mc
puts "[mc m1]; [mc m2]; [mc m3]"
namespace eval ::foo {puts "[mc m1]; [mc m2]; [mc m3]"}
namespace eval ::foo::bar {puts "[mc m1]; [mc m2]; [mc m3]"}
이렇게 출력돼요:
:: message1; :: message2; :: message3
:: message1; ::foo message2; ::foo message3
:: message1; ::foo message2; ::foo::bar message3
메시지 파일의 위치와 형식
메시지 파일은 다음 조건을 만족한다면 어느 디렉토리에든 둘 수 있어요.
- 패키지의 모든 메시지 파일이 같은 디렉토리에 있다.
- 메시지 파일 이름은 msgcat 로케일 지정자(모두 소문자) 뒤에
.msg가 붙는다. 예:es.msg(스페인어),en_gb.msg(영국 영어). - 예외: 루트 로케일
""의 메시지 파일은"ROOT.msg"라고 한다. 이 예외는 Unix 파일 시스템에서 메시지 파일이 "숨김"으로 표시되는 등의 특이한 동작을 막기 위한 것이다.
파일에는 그 언어에 필요한 번역 문자열을 설정하는 mcflset와 mcflmset 호출 시리즈가 들어 있고, 보통 namespace eval로 감싸서 모든 소스 문자열이 패키지의 네임스페이스에 묶이게 해요. 예를 들어 짧은 es.msg는 이렇게 담길 수 있어요:
namespace eval ::mypackage {
::msgcat::mcflset "Free Beer" "Cerveza Gratis"
}
패키지 권장 메시지 설정
패키지가 tcl_pkgPath의 하위 디렉토리에 설치되고 package require로 로드된다면, 다음 절차를 권장해요.
- 패키지 설치 중, 패키지 디렉토리 아래에
msgs하위 디렉토리를 만든다. *.msg파일을 그 디렉토리로 복사한다.- 패키지 초기화 스크립트에 다음 명령을 추가한다:
# load language files, stored in msgs subdirectory
::msgcat::mcload [file join [file dirname [info script]] msgs]
format·scan 명령의 위치 코드
format의 인수로 쓰이는 메시지 문자열이 위치에 따라 달라지는 매개변수를 가질 수 있어요. 예를 들어 번역하면서 문장 구조를 재배열하는 것이 구문상 바람직할 수 있어요.
format "We produced %d units in location %s" $num $city
format "In location %s we produced %d units" $city $num
이것은 위치 매개변수로 처리할 수 있어요:
format "We produced %1\$d units in location %2\$s" $num $city
format "In location %2\$s we produced %1\$d units" $num $city
마찬가지로 scan과도 위치 매개변수를 사용해 국제화된 문자열에서 값을 추출할 수 있어요. ::msgcat::mc의 출력을 format에 직접 전달할 필요는 없다는 점을 기억하세요. 치환할 값을 인수로 전달하면 포맷 치환이 바로 수행돼요.
msgcat::mc {Produced %1$d at %2$s} $num $city
# ... where that key is mapped to one of the
# human-oriented versions by msgcat::mcset
패키지 전용 로케일
msgcat을 사용하는 패키지는 ::msgcat::mclocale이 설정한 전역 로케일과 독립적으로, 자신만의 패키지 전용 로케일과 자신만의 로드된 로케일 집합을 사용하기로 선택할 수 있어요.
이렇게 하면 패키지가 다른 패키지의 로케일 로드·제거를 일으키지 않고, 전역 로케일 변경 콜백(아래 참고)도 호출하지 않으면서 로케일을 바꿀 수 있어요.
이 동작은 다음 앙상블로 제어돼요:
::msgcat::mcpackagelocale set ?locale?
패키지 전용 로케일을 설정하거나 변경해요. locale이 주어지면 패키지 전용 로케일을 그 값으로 설정해요. locale 옵션을 주지 않으면 패키지가 패키지 전용 로케일 모드로 설정되지만 로케일은 바뀌지 않아요(예: 그 전에 전역 로케일이 유효했다면 그것이 패키지 전용 로케일로 복사돼요). 이 명령은 로케일 로드를 일으킬 수 있어요.
::msgcat::mcpackagelocale get
패키지 전용 로케일을 돌려주고, 패키지 전용 로케일이 설정되어 있지 않으면 전역 로케일을 돌려줘요.
::msgcat::mcpackagelocale preferences
패키지 전용 선호 목록을 돌려주고, 패키지 전용 로케일이 설정되어 있지 않으면 전역 선호 목록을 돌려줘요.
::msgcat::mcpackagelocale loaded
이 패키지에 로드된 로케일 목록을 돌려줘요.
::msgcat::mcpackagelocale isset
패키지 전용 로케일이 설정되어 있으면 참을 돌려줘요.
::msgcat::mcpackagelocale unset
패키지 전용 로케일을 해제하고 전역 로케일을 사용해요. 패키지의 로드된 로케일 목록이 전역 로드된 로케일 목록에 맞도록 로케일을 로드·제거해요.
::msgcat::mcpackagelocale present locale
주어진 로케일이 패키지에 로드되어 있으면 참을 돌려줘요.
::msgcat::mcpackagelocale clear
패키지 선호 목록에 없는 패키지의 로드된 로케일을 모두 지워요.
패키지 옵션 변경
msgcat을 사용하는 각 패키지는 msgcat 안에 옵션 집합을 가져요. 패키지 옵션은 다음 섹션(패키지 옵션)에 설명돼요. 각 패키지 옵션은 다음 앙상블로 개별 설정·해제할 수 있어요:
::msgcat::mcpackageconfig get option
주어진 옵션의 현재 값을 돌려줘요. 패키지에 옵션이 설정되어 있지 않으면 오류를 돌려줘요.
::msgcat::mcpackageconfig isset option
패키지에 옵션이 설정되어 있으면 1, 아니면 0을 돌려줘요.
::msgcat::mcpackageconfig set option value
주어진 옵션을 주어진 값으로 설정해요. 옵션에 따라 추가 동작을 호출할 수 있어요. 반환값은 0 또는 mcfolder 옵션의 경우 로드된 패키지 수예요.
::msgcat::mcpackageconfig unset option
패키지의 옵션을 해제해요. 옵션이 설정되어 있지 않으면 아무 동작도 하지 않아요. 빈 문자열을 돌려줘요.
패키지 옵션
각 패키지에 대해 다음 패키지 옵션을 사용할 수 있어요.
mcfolder
패키지의 메시지 폴더예요. 이 옵션은 mcload와 set 하위 명령으로 설정돼요. 둘 다 동일하며 로드된 메시지 카탈로그 파일 수를 돌려줘요. 이 값을 설정하거나 변경하면 패키지에 유효한 선호 목록에 포함된 모든 로케일을 로드해요. 이는 설정된 loadcmd(아래 참고)를 호출하는 것도 의미해요. 이 값을 해제하면 패키지의 메시지 파일 로드가 비활성화돼요.
loadcmd
이 콜백은 이 속성이 설정된 패키지에 대해 메시지 카탈로그 파일 집합이 로드되기 전에 호출돼요. 이 콜백은 메시지 파일 로드 준비 작업을 하거나, 데이터베이스 같은 다른 소스에서 메시지 데이터를 가져오는 데 사용할 수 있어요. 이 경우 메시지 파일은 사용되지 않아요(mcfolder가 해제됨). 콜백 호출 섹션을 참고해요. 이 콜백에 추가되는 매개변수 목록은 로드할 로케일 목록이에요. 이 콜백이 변경되면 패키지에 유효한 선호 목록과 함께 호출돼요.
changecmd
이 콜백은 기본 로케일 변경이 수행되었을 때 호출돼요. 그 목적은 패키지가 GUI를 다른 언어로 표시하는 것 같은 기본 로케일 의존성을 갱신하게 하는 거예요. 콜백 호출 섹션을 참고해요. 이 콜백에 추가되는 매개변수 목록은 mcpreferences예요. 등록된 콜백은 특정 순서 없이 호출돼요.
unknowncmd
표준 버전(msgcat 패키지가 제공하는 msgcat::mcunknown) 대신 패키지 로케일 mcunknown 프로시저를 사용해요. 호출되는 프로시저는 결국 msgcat::mc가 반환할 포맷된 메시지를 돌려줘야 해요. 빈 문자열로 설정하면 일반 unknown 핸들러가 사용되는데, 인수가 없으면 키를 돌려주는 것으로 구성돼요. 인수가 주어지면 format 명령으로 인수를 처리해요. 콜백 호출 섹션을 참고해요. 추가되는 인수는 msgcat::mcunknown과 동일해요.
콜백 호출
패키지는 위에서 설명한 대로 콜백을 하나 이상 등록할 수 있어요. 콜백은 다음 조건에서 호출돼요:
- 콜백 명령이 설정되어 있고,
- 명령이 빈 문자열이 아니며,
- 등록 네임스페이스가 존재한다.
호출된 루틴이 오류로 실패하면, 명령 완료 후 인터프리터의 bgerror 루틴이 호출돼요. 유일한 예외는 unknowncmd 콜백인데, 여기서 오류가 나면 호출한 mc-명령이 그 오류로 실패해요.
예제
GUI를 표시하는 패키지는 전역 로케일이 바뀔 때 위젯을 갱신할 수 있어요. 콜백을 등록하려면:
namespace eval gui {
msgcat::mcpackageconfig changecmd updateGUI
proc updateGUI args {
puts "New locale is '[lindex $args 0]'."
}
}
% msgcat::mclocale fr
fr
% New locale is 'fr'.
로케일(또는 추가 로케일)이 데이터베이스 같은 다른 소스에 있다면, 패키지는 mcload 대신 load 콜백을 사용할 수 있어요:
namespace eval db {
msgcat::mcpackageconfig loadcmd loadMessages
proc loadMessages args {
foreach locale $args {
if {[LocaleInDB $locale]} {
msgcat::mcmset $locale [GetLocaleList $locale]
}
}
}
}
clock 명령 구현은 패키지 로케일과 함께 msgcat을 사용해 명령줄 매개변수 -locale을 구현해요. 구현 스케치 몇 개를 보여드릴게요.
먼저 패키지 로케일을 초기화하고 일반 unknown 함수를 비활성화해요:
msgcat::mcpackagelocale set
msgcat::mcpackageconfig unknowncmd ""
예를 들어 사용자가 특정 로케일로 요일을 요구하면:
clock format [clock seconds] -format %A -locale fr
clock은 패키지 로케일을 fr로 설정하고 요일 이름을 다음과 같이 찾아요:
msgcat::mcpackagelocale set $locale
return [lindex [msgcat::mc DAYS_OF_WEEK_FULL] $day]
### Returns "mercredi"
clock 안에서 일부 메시지 카탈로그 항목은 계산이 무거워서 다음과 같이 동적으로 캐시돼요:
proc ::tcl::clock::LocalizeFormat { locale format } {
set key FORMAT_$format
if { [::msgcat::mcexists -exactlocale -exactnamespace $key] } {
return [mc $key]
}
#...expensive computation of format clipped...
mcset $locale $key $format
return $format
}
더 알아보기
format,scan— 문자열 포맷·파싱namespace,package— 네임스페이스·패키지 관리