dev

Hello Gettext! - hex package | 사용법, 공식 문서 요약

Wonny (워니)
Wonny (워니)·2022년 10월 14일 07:54

What is Gettext?

국제화와 지역화를 위해 gettext 기반의 API를 제공하는 모듈

설치하기

엘릭서 프로젝트 생성 및 실행하기

# 생성 mix new [프로젝트명] # 실행 iex -S mix

참고: Introduction to Mix | elixir

gettext 설치하기

  1. mix.exsdeps 함수 안에 {:gettext, "~> 0.20.0"} 추가 (참고: gettext | hex package)
# mix.exs defmodule GettextPractice.MixProject do .. defp deps do [{:gettext, "~> 0.20.0"}] end ... end
  1. 다음 명령어를 실행하여 설치
mix deps.get

Gettext 사용해보기

  1. use Gettext를 호출하는 모듈 정의
# lib/gettext_practice/gettext.ex defmodule GettextPractice.Gettext do use Gettext, otp_app: :gettext_practice, default_locale: "ko" end

예제에서는 :default_locale 옵션을 통해 "ko"를 기본 로케일로 설정했다.

  1. 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
  1. 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.poterrors.pot 파일이 생성된다.
  1. PO 파일 생성
mix gettext.merge [POT 파일이 담긴 디렉터리. (default: priv/gettext)] --locale [로케일]

위 명령어 실행 시 POT 파일을 기반으로 디렉터리에 입력한 로케일과 동일한 디렉터리가 다음과 같은 구조로 생성된다.

priv/gettext └─ ko └─ LC_MESSAGES ├─ default.po └─ errors.po

각 PO 파일의 이름이므로, 매크로에 입력한 도메인에 대한 모든 파일이 생성된다.

  1. PO 파일에 번역 작성
    • 다음과 같이 각 PO 파일의 msgstr에 번역을 작성한다.
# priv/gettext/ko/LC_MESSAGES/default.po msgid "Confirm" msgstr "확인" # priv/gettext/ko/LC_MESSAGES/errors.po msgid "Wrong Message" msgstr "잘못된 메시지"
  1. 실행

위에서 생성한 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 파일이 담긴 디렉토리 이름과 일치하는 임의의 문자열로 설정할 수 있다.

사용할 로케일을 결정하는 단계

  1. 현재 프로세스에서 주어진 백엔드에 대한 로컬 로케일이 있는 경우 해당 로케일을 사용 (put_locale/2로 설정한 로케일)
  2. 현재 프로세스에서 대해 지정된 백엔드에 대한 글로벌 로케일이 있는 경우 해당 로케일을 사용 (put_locale/1로 설정한 로케일)
  3. 백엔드의 :opt_app에 대한 설정에 백엔드별 디폴트 로케일이 있는 경우 해당 로케일 사용 (Gettext 모듈에서 :default_locale 옵션으로 설정한 로케일)
  4. 디폴트 글로벌 로케일 사용 (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" 대신 사용할 디폴트 글로벌 도메인 지정

백엔드 설정

설정하는 방법 두 가지

  1. use Gettext (컴파일 타임에 설정)
defmodule GettextPractice.Gettext do use Gettext, otp_app: :gettext_practice, default_locale: "ja" end
  1. 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을 설정할 수 없다.
    • 번역을 검색할 앱의 디렉터리를 결정하는 데도 사용된다.
  • :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를 사용한다.
    • 함수의 반환 값을 반환한다.

References

dev
hex
erlang
elixir
gettext

광고를 붙이기 싫어서 후원 버튼을 추가해보았습니다. 😉 여러분의 작은 후원이 워니에게 큰 힘이 됩니다! 🥰
© 2020 Wonny.