Wonny Log,

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

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

Gettext를 사용해보자

Wonny (워니)
Wonny (워니)·2022년 10월 13일 22:54

What is Gettext?

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

설치하기

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

# 생성
mix new [프로젝트명]

# 실행
iex -S mix

참고: Introduction to Mix | elixir

gettext 설치하기

  1. 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
  2. 다음 명령어를 실행하여 설치
    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"를 기본 로케일로 설정했다.

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

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

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

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

  5. PO 파일에 번역 작성

    다음과 같이 각 PO 파일의 msgstr에 번역을 작성한다.

    # priv/gettext/ko/LC_MESSAGES/default.po
    msgid "Confirm"
    msgstr "확인"
    
    # priv/gettext/ko/LC_MESSAGES/errors.po
    msgid "Wrong Message"
    msgstr "잘못된 메시지"
  6. 실행

    위에서 생성한 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가 붙은 함수 -> 추출을 위한 번역을 표시하기 위해 사용하는 것으로 번역을 하지 않음

컴파일 타임에 문자열로 확장되지 않는 domainmsgctxtmsgidmsgid_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")

백엔드가 gettextngettextpgettext를 사용할 때 백엔드의 기본 도메인이 사용된다. 백엔드의 기본 도메인을 따로 설정하지 않은 경우 기본값은 "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"가 붙은 pgettextdpgettextpngettextdpngettext를 사용하고, "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 함수와 매크로들은 msgidmsgid_plural과 엘레멘트 수 인수를 가진다. 복수형 예시
msgid "%{n} Apples"
msgstr "%{n}개의 사과들"
gettext("%{n} Apples", n: 4) # result: "4 Apples"

 

로케일이 "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
  2. 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

 

gettextput_localeget_locale 외의 함수

  • known_locales(backend)
    • 백엔드에 존재하는 PO 파일들의 모든 로케일 반환
  • with_locale(locale, fun) / with_locale(backend, locale, fun)
    • 해당 로케일을 적용한 상태에서 함수 실행
    • 함수 실행 전에 글로벌 Gettext 로케일을 설정하고, 함수 실행 후에 이전 값으로 다시 설정한다. 로케일 설정 시 put_locale/2를 사용한다.
    • 함수의 반환 값을 반환한다.

 

References

engineering
elixir
translation
hex

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