Hello Gettext! - hex package | 사용법, 공식 문서 요약
What is Gettext?
국제화와 지역화를 위해 gettext 기반의 API를 제공하는 모듈
설치하기
엘릭서 프로젝트 생성 및 실행하기
# 생성
mix new [프로젝트명]
# 실행
iex -S mix
참고: Introduction to Mix | elixir
gettext 설치하기
mix.exs
의deps
함수 안에{:gettext, "~> 0.20.0"}
추가 (참고: gettext | hex package)
# mix.exs
defmodule GettextPractice.MixProject do
..
defp deps do
[{:gettext, "~> 0.20.0"}]
end
...
end
- 다음 명령어를 실행하여 설치
mix deps.get
Gettext 사용해보기
use Gettext
를 호출하는 모듈 정의
# lib/gettext_practice/gettext.ex
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice, default_locale: "ko"
end
예제에서는 :default_locale
옵션을 통해 "ko"를 기본 로케일로 설정했다.
gettext
매크로를 사용하는 함수 추가
# lib/gettext_practice.ex
defmodule GettextPractice do
import GettextPractice.Gettext
def localize_confirm_words do
# "default" 도메인에서 번역 검색
gettext("Confirm") |> IO.inspect()
# "errors" 도메인에서 번역 검색
dgettext("errors", "Wrong Message") |> IO.inspect()
end
end
- POT 파일 추출
mix gettext.extract
- POT 파일은 PO 파일의 템플릿 파일이고, PO 파일은 번역이 담긴 파일이다. (참고: What is the difference between the .po .mo and .pot localization files? | StackExchange)
- 위 명령어 실행 시 소스 코드에서 Gettext 관련 매크로를 찾아서 자동으로 POT 파일을 생성해준다.
- 현재 예제에서 위 명령어 실행 시
priv/gettext
디렉터리에default.pot
과errors.pot
파일이 생성된다.
- PO 파일 생성
mix gettext.merge [POT 파일이 담긴 디렉터리. (default: priv/gettext)] --locale [로케일]
위 명령어 실행 시 POT 파일을 기반으로 디렉터리에 입력한 로케일과 동일한 디렉터리가 다음과 같은 구조로 생성된다.
priv/gettext
└─ ko
└─ LC_MESSAGES
├─ default.po
└─ errors.po
각 PO 파일의 이름이므로, 매크로에 입력한 도메인에 대한 모든 파일이 생성된다.
- PO 파일에 번역 작성
- 다음과 같이 각 PO 파일의
msgstr
에 번역을 작성한다.
- 다음과 같이 각 PO 파일의
# priv/gettext/ko/LC_MESSAGES/default.po
msgid "Confirm"
msgstr "확인"
# priv/gettext/ko/LC_MESSAGES/errors.po
msgid "Wrong Message"
msgstr "잘못된 메시지"
- 실행
위에서 생성한 localize_confirm_words/0
함수 실행 시 PO 파일에 입력한 번역이 출력되는 걸 확인할 수 있다.
iex -S mix
iex> GettextPractice.localize_confirm_words
"확인"
"잘못된 메시지"
Deep dive
번역
PO(Portable Object) 파일
번역들은 확장자가 *.po
인 PO(Portable Object) 파일에 작성한다.
PO 파일은 다음과 같이 작성한다.
msgid "Confirm"
msgstr "확인"
msgid "Confirm"
msgstr "확인하기"
msgctxt "Verb"
msgid "Error"
msgid_plural "%n Errors"
msgstr[0] "Un Error"
msgstr[1] "%n Errors"
PO 파일들은 다음 구조를 가진 디렉토리에 저장되어야 하고, 기본적으로 이 디렉토리의 위치는 priv/gettext
이다.
# 디렉터리 구조
[gettext directory (default: priv/gettext)]
└─ [locale]
└─ LC_MESSAGES
├─ [domain_1].po
├─ [domain_2].po
└─ [domain_3].po
# 실제 디렉터리 예시
priv/gettext
└─ en_US
| └─ LC_MESSAGES
| ├─ default.po
| └─ errors.po
└─ it
└─ LC_MESSAGES
├─ default.po
└─ errors.po
locale
에는en_US
이나ko
와 같은 로케일 정보가 들어가고LC_MESSAGES
는 고정된 디렉터리이다.domain_1.po
과 같이 각 PO 파일들은 각 도메인 범위의 번역을 포함한다. PO 파일의 이름이 도메인 이름이다.
번역 파일 디렉터리 변경하는 방법
# gettext.ex
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice, priv: [디렉터리 경로]
end
:priv
옵션에서 지정한 디렉터리는 반드시 priv
디렉터리 안에 있어야 한다. 그렇지 않으면 mix compile.gettext
와 같은 몇 가지 명령들이 제대로 동작하지 않을 수 있다.
Locale
- 런타임에서 로케일 정보를 명시적으로 설정하지 않은 모든 Gettext 관련 함수와 매크로는 백엔드 로케일과 기본 로케일 값을 읽어서 Gettext의 로케일로 사용한다.
- 로케일은 사전에 프로세스별로 저장된다. 해당 프로세스에서 사용할 수 있는 올바른 로케일을 가지려면 모든 새 프로세스에서 로케일을 설정해야 한다.
- 로케일은
en
와 같이 문자열로 표현하고, PO 파일이 담긴 디렉토리 이름과 일치하는 임의의 문자열로 설정할 수 있다.
사용할 로케일을 결정하는 단계
- 현재 프로세스에서 주어진 백엔드에 대한 로컬 로케일이 있는 경우 해당 로케일을 사용 (
put_locale/2
로 설정한 로케일) - 현재 프로세스에서 대해 지정된 백엔드에 대한 글로벌 로케일이 있는 경우 해당 로케일을 사용 (
put_locale/1
로 설정한 로케일) - 백엔드의
:opt_app
에 대한 설정에 백엔드별 디폴트 로케일이 있는 경우 해당 로케일 사용 (Gettext
모듈에서:default_locale
옵션으로 설정한 로케일) - 디폴트 글로벌 로케일 사용 (
config.exs
에서:default_locale
옵션으로 설정한 로케일)
Default locale
- 글로벌 디폴트 로케일은
en
- 글로벌 디폴트 로케일은
:gettext
의:default_locale
키를 통해 설정
# config/config.exs
config :gettext, :default_locale, "ko"
- 백엔드별 디폴트 로케일 설정 방법
- config를 통한 설정 (각 백엔드가 사용하는 로케일을 추적하기 힘드므로 권장되지 않음)
config :gettext_practice, GettextPractice.Gettext, default_locale: "ko"
- Gettext 모듈에서 설정
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice, default_locale: "ko"
end
Functions
로케일 설정
put_locale/1
: 현재 엘릭서 프로세스의 모든 백엔드 로케일을 변경 (런타임에서 로케일 설정할 때 선호되는 방법)put_locale/2
: 다른 Gettext 백엔드에 영향 없이 특정 Gettext 백엔드 로케일을 변경
로케일 조회
get_locale/0
: 현재 프로세스의 모든 백엔드에 대한 로케일 조회get_locale/1
: 현재 프로세스의 특정 백엔드 로케일 조회
Gettext API를 사용하는 방법
use Gettext
를 호출하는 모듈을 통해 매크로를 사용하는 방법 (권장)
- 매크로를 사용하면 소스 코드를 통해 PO 파일을 동기화할 수 있어서 매크로 사용이 권장된다.
- 제약사항: 매크로에 전달되는 인수가 컴파일 시 문자열이어야 하므로 문자열 리터럴 또는 문자열 리터럴로 확장되어야 한다. e.g.
@my_string "foo"
와 같은 모듈 속성
@module_attr "Confirm"
def use_module_attribute do
gettext(@module_attr)
|> IO.inspect() # result: "Confirm"
end
매크로 종류
use Gettext
를 호출하는 각 모듈은 Gettext.Backend 동작을 구현하기 때문에 일반적으로 Gettext 백엔드라고 한다.use Gettext
를 호출할 때, 다음 매크로들이 모듈 안에 자동으로 정의되고 다음과 같이 동작한다.gettext(msgid, bindings \\ %{})
->Gettext.gettext(MyApp.Gettext, msgid, bindings)
dgettext(domain, msgid, bindings \\ %{})
->Gettext.dgettext(MyApp.Gettext, domain, msgid, bindings)
pgettext(msgctxt, msgid, bindings \\ %{})
-> Gettext.pgettext(MyApp.Gettext, msgctxt, msgid, bindings)dpgettext(domain, msgctxt, msgid, bindings \\ %{})
->Gettext.dpgettext(MyApp.Gettext, domain, msgctxt, msgid, bindings)
ngettext(msgid, msgid_plural, n, bindings \\ %{})
->Gettext.ngettext(MyApp.Gettext, msgid, msgid_plural, n, bindings)
dngettext(domain, msgid, msgid_plural, n, bindings \\ %{})
->Gettext.dngettext(MyApp.Gettext, domain, msgid, msgid_plural, n, bindings)
pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ %{})
->Gettext.pngettext(MyApp.Gettext, msgctxt, msgid, msgid_plural, n, bindings)
dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ %{})
->Gettext.dpngettext(MyApp.Gettext, domain, msgctxt, msgid, msgid_plural, n, bindings)
gettext_noop
과 같이 위의 모든 매크로의 접미사로_noop
가 붙은 함수 -> 추출을 위한 번역을 표시하기 위해 사용하는 것으로 번역을 하지 않음
컴파일 타임에 문자열로 확장되지 않는 domain
, msgctxt
, msgid
, msgid_plural
이 있는 경우 ArgumentError
가 발생한다. 이런 경우에는 아래에 나올 함수를 사용해야 한다.
msgid = "Hello world"
MyApp.Gettext.gettext(msgid)
#=> \*\* (ArgumentError) msgid must be a string literal
Gettext
모듈의 함수 사용
컴파일 타임에서 문자열을 사용할 수 없는 경우 매크로 대신 Gettext
모듈에 있는 함수를 사용할 수 있다.
모든 함수들이 첫 번째 인수로 모듈 이름을 필요로 한다. 이 모듈은 반드시 use Gettext
를 호출해야 한다.
매크로를 사용할 때와 똑같은 번역 결과가 나오지만, 뒤에 나올 컴파일 타임 기능들을 활용할 수 없다.
함수를 사용하는 예시
# gettext.ex
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice
end
Gettext.gettext(GettextPractice.Gettext, "Confirm")
도메인
도메인은 PO 파일의 이름으로 결정된다. ko/LC_MESSAGES/errors.po
와 같은 파일이 있다면 dgettext
함수나 dngettext
함수를 사용할 때 "errors"
를 도메인 값으로 넣어주면 된다.
dgettext("errors", "Wrong Message")
백엔드가 gettext
, ngettext
, pgettext
를 사용할 때 백엔드의 기본 도메인이 사용된다. 백엔드의 기본 도메인을 따로 설정하지 않은 경우 기본값은 "default"
이다.
백엔드 기본 도메인 변경하는 방법 두 가지
백엔드에서 설정
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice, default_domain: "messages"
end
config를 통해 설정
config :gettext_practice, GettextPractice.Gettext, default_domain: "translations"
컨텍스트
GNU Gettext 구현체는 컨텍스트를 지원하고, 이는 문맥적인 번역을 가능하게 해준다. 가령 영어에서 "file"이란 단어는 동사와 명사 모두로 사용할 수 있다. 이 중 어떤 품사로 사용할지 모호하다. 이러한 모호성 문제를 컨텍스트를 통해 해결할 수 있다.
컨텍스트를 사용할 때는 "p"가 붙은 pgettext
, dpgettext
, pngettext
, dpngettext
를 사용하고, "p"는 "particular"을 의미한다.
컨텍스트 사용 예시
msgid "Confirm"
msgstr "확인"
msgctxt "imperative"
msgid "Confirm"
msgstr "확인하기"
gettext("Confirm") # result: "확인"
pgettext("imperative", "Confirm") # result: "확인하기"
보간법
Gettext가 제공하는 모든 *gettext
함수와 매크로는 보간법을 지원한다.
보간 키는 다음과 같이 %{
및 }
에 묶어서 넣을 수 있다.
msgid "%{n} Apples"
msgstr "%{n}개의 사과들"
gettext("%{n} Apples", n: 4) # result: "4 Apples"
문자열에 보간 키가 있는데 바인딩을 제공하지 않는 경우 Gettext.Error
예외가 발생한다.
바인딩에 있으나 문자열에 보간 키가 없는 경우에는 바인딩이 무시된다.
Gettext의 보간은 종종 컴파일 타임에 확장되어 런타임 시 성능적 이점이 있다.
복수형
*ngettext
함수와 매크로들은 msgid
, msgid_plural
과 엘레멘트 수 인수를 가진다.
복수형 예시
msgid "One error"
msgid_plural "%{count} errors"
msgstr[0] "Un error"
msgstr[1] "%{count} errors"
ngettext("One error", "%{count} errors", 1) # result: "Un error"
ngettext("One error", "%{count} errors", 3) # result: "3 errors"
로케일이 "ko"인 경우 복수형이 지원되지 않는다.
Gettext.put_locale("ko")
ngettext("One error", "%{count} errors", 3) # result: "One error"
한글과 같은 언어는 복수형 지원이 안되기 때문이다. 복수형 지원 여부는 이 코드에서 확인할 수 있다.
백엔드 설정에 :plural_forms
옵션을 사용하여 pluralizer 모듈을 설정할 수 있다.
defmodule MyApp.Gettext do
use Gettext, otp_app: :my_app, plural_forms: MyApp.PluralForms
end
PO 파일에 번역이 없을 때
gettext
/dgettext
/pgettext
/dpgettext
들을 호출할 때 번역을 찾지 못하면msgid
인수를 그대로 반환함ngettext
/dngettext
/pngettext
/dpngettext
들을 호출할 때 번역을 찾지 못하면, 단수인 경우에는msgid
, 복수인 경우에는msg_plural
값을 그대로 반환함msgstr
가 비었을 때(""
) 번역이 없는 것으로 처리되어 위와 같은 동작을 수행한다.
Compile-time features
소스 코드에서 POT 파일 추출
Gettext 매크로는 함수와 다르게 컴파일 타임에 번역을 수행한다. 그러므로 소스 코드에서 POT 파일을 자동으로 추출할 수 있다. 소스 코드에 새 번역이 있을 때마다 gettext.extract을 실행하면 존재하는 POT 파일을 동기화한다. POT 파일은 기본으로 priv/gettext
에 생성된다.
mix gettext.extract
PO 파일에 POT 내용 반영
POT 파일은 PO 파일에 대한 템플릿 파일이고, 모든 번역(msgstr
)이 빈 문자열인 PO 파일과 동일하다.
POT 파일이 변경 될 때마다 개발자 (또는 번역자)가 각 로케일 PO 파일들을 업데이트 해야 한다. 이때 각 PO 파일에 수동으로 번역을 추가하지 않고 다음 명령어를 통해 POT 파일과 동기화할 수 있다.
mix gettext.merge priv/gettext
Configuration
:gettext
설정 (config.exs에서 설정)
# config/config.exs
import Config
config :gettext, :default_locale, "ko"
옵션들
:default_locale
: 모든 백엔드에 사용할 디폴트 글로벌 로케일 지정:default_domain
: 모든 백엔드에 "default" 대신 사용할 디폴트 글로벌 도메인 지정
백엔드 설정
설정하는 방법 두 가지
use Gettext
(컴파일 타임에 설정)
defmodule GettextPractice.Gettext do
use Gettext, otp_app: :gettext_practice, default_locale: "ja"
end
- Mix 설정 사용
# config/config.exs
import Config
config(:gettext_practice, GettextPractice.Gettext, default_locale: "ko")
옵션들
:otp_app
- OTP(Open Telecom Platform) 앱을 나타내는 아톰 이 옵션은 항상 설정되어야 하고,
use Gettext
에 전달되어야 한다. - 컴파일 타임에서 매크로를 만들 때 이 설정을 보고 만들어준다. 그러므로 Mix 설정을 통해서는
:opt_app
을 설정할 수 없다. - 번역을 검색할 앱의 디렉터리를 결정하는 데도 사용된다.
- OTP(Open Telecom Platform) 앱을 나타내는 아톰 이 옵션은 항상 설정되어야 하고,
:priv
(default:priv/gettext
)- 번역을 검색할 디렉터리의 상대 경로 (
:otp_app
에서 지정한 앱을 기준으로 상대 경로임) - PO 파일들은
priv
디렉터리 안에 두는 걸 추천한다. 그렇지 않으면mix compile.gettext
같은 명령어가 잘 동작하지 않을 수 있다.
- 번역을 검색할 디렉터리의 상대 경로 (
:plural_forms
- pluralizer으로 사용할 모듈
:default_locale
- 지정한 백엔드에 대한 디폴트 로케일
:split_module_by
- 모든 로케일을 한 모듈안에 번들링하는 대신 Gettext가 로케일당 혹은 도메인당, 로케일당X도메인당 인터널 모듈로 빌드하게 해준다.
- 컴파일 타임이랑 큰 프로젝트의 빔 파일 사이즈를 줄여준다.
:split_module_compilation
(default::parallel
)- 분리된 모듈의 편집이
:parallel
이어야 하는지:serial
이어야 하는지 설정한다.
- 분리된 모듈의 편집이
:allowed_locales
- 백엔드에 번틀될 로케일 리스트.
- 기본으로는 priv에 있는 모든 로케일이 지정된다.
- 로케일만 컴파일하므로 컴파일 타임이 줄어 개발 시 유용하게 쓰인다.
:interpolation
(default: Gettext.Interpolation.Default)- Gettext.Interpolation 동작을 구현하는 모듈
Mix task 설정
mix.exs
에 있는 project/0
가 반환하는 설정 :gettext
키 아래에 설정할 수 있다.
def project() do
[app: :my_app,
# ...
gettext: [...]]
end
옵션들
:fuzzy_threshold
:excluded_refs_from_purging
:write_reference_comments
:sort_by_msgid
*gettext
, put_locale
, get_locale
외의 함수
known_locales(backend)
- 백엔드에 존재하는 PO 파일들의 모든 로케일 반환
with_locale(locale, fun)
/with_locale(backend, locale, fun)
- 해당 로케일을 적용한 상태에서 함수 실행
- 함수 실행 전에 글로벌 Gettext 로케일을 설정하고, 함수 실행 후에 이전 값으로 다시 설정한다. 로케일 설정 시
put_locale/2
를 사용한다. - 함수의 반환 값을 반환한다.