I18n

GNU gettext 가 사용됩니다. gettext는 다른 국제화 방법과는 다르게 복수형을 지원합니다.

http://poedit.net/images/home_image2-big.png

소스코드에 국제화 메세지 작성

xitrum.Actionxitrum.I18n 확장했으며 다음의 메소드가 있습니다:

t("Message")
tc("Context", "Message")

t("Hello %s").format("World")

// 1$ and 2$ are placeholders
t("%1$s says hello to %2$s, then %2$s says hello back to %1$s").format("Bill", "Hillary")

// {0} and {1} are placeholders
java.text.MessageFormat.format(t("{0} says hello to {1}, then {1} says hello back to {0}"), "Bill", "Hillary")

t("%,.3f").format(1234.5678)                                // => 1,234.568
t("%,.3f").formatLocal(java.util.Locale.FRENCH, 1234.5678)  // => 1 234,568
// Above, you explicitly specify locale.
// If you want to implicitly use locale of the current action:
// when English => 1,234.568, when French => 1 234,568
t("%,.3f", 1234.5678)

action 안에서나 혹은 액션에서 호출할 수 있습니다. model과 같은 곳에서의 사용은 현재의 액션에서 ttc 를 호출하여 넘겨줘야 합니다:

// In an action
respondText(MyModel.hello(this))

// In the model
import xitrum.I18n
object MyModel {
  def hello(i18n: I18n) = i18n.t("Hello World")
}

pot에 메세지 추출하기

프로젝트 루트에 빈 i18n.pot 파일을 생성하여 전체 프로젝트를 다시 컴파일 하면 됩니다.

sbt/sbt clean
rm i18n.pot
touch i18n.pot
sbt/sbt compile

sbt/sbt clean 는 모든 .class파일을 삭제하고 전체를 다시 컴파일 합니다. sbt/sbt clean 명령은 SBT가 모든 의존된 라이브러리를 다운받기 때문에 find target -name *.class -delete 명령이 조금 더 빠르지만 target 내부의 .class 파일을 삭제하는것은 동일합니다.

재컴파일 후에 i18n.pot는 소스코드로 부터 추출된 gettext 메세지를 채웁니다. 마법같은 이 동작은 Scala compiler plugin technique 에 기술되어 있습니다.

한 가지 이 메소드의 주의점은 gettext는 Scala 소스 코드로부터 메세지를 추출합니다. 만약 자바 파일을 사용한다면 다음과 같이 xgettext 커맨드 라인을 사용하여야 합니다.

xgettext -kt -ktc:1c,2 -ktn:1,2 -ktcn:1c,2,3 -o i18n_java.pot --from-code=UTF-8 $(find src/main/java -name "*.java")

그리고 나서 수동으로 i18n_java.pot 파일과 i18n.pot파일을 병합해야 합니다.

po 파일 저장위치

i18n.pot은 임시 파일입니다. 파일들을 <language>.po 로 복사하여 번역해야 합니다.

Xitrum은 클래스 패스상의 i18n 디렉토리를 모니터링합니다. 만약 런타임시 디렉토리 상의 <language>.po 파일이 변경되거나 추가된다면 Xitrum은 자동으로 <language>.po 파일들을 다시 로드합니다.

src
  main
    scala
    view
    resources
      i18n
        ja.po
        vi.po
        ...

po 파일을 수정하기 위해서는 Poedit 와 같은 툴을 사용하면 됩니다. 툴을 사용하여 새로운 pot 파일을 기존의 po 파일에 추가 할수 있습니다.

po 파일들은 여러 JAR파일들에 패키징할 수 있고 Xitrum은 실행시에 자동으로 병합합니다.

mylib.jar
  i18n
    ja.po
    vi.po
        ...

another.jar
  i18n
    ja.po
    vi.po
        ...

언어설정

  • 브라우저의 요청 헤더의 Accept-Language 에서 언어 셋을 가져오려면 browserLanguages 를 호출하면 됩니다. 결과는 브라우저의 우선순위에 따라 정렬됩니다.

  • 기본 언어는 “en” 입니다. 현재의 언어로 세팅하기 위해서는, 예를 들어, 일어일 경우 language = "ja" 로 하면 됩니다.

  • 가장 적절한 언어를 리소스에서 찾아 자동세팅을 위해서는 autosetLanguage(availableLanguages) 을 호출하면 됩니다. availableLanguagesresources/i18n 디렉토리와 JAR파일들의 가능한 언어들의 리스트를 가지고 있습니다. 만약 적절한 언어가 없을 경우에는 기본언어인 “en”으로 설정합니다.

  • 현재의 언어셋을 확인하려면 language 를 사용하면 됩니다.

일반적으로 액션의 before 필터에서 언어를 세팅합니다:

beforeFilter {
  val lango: Option[String] = yourMethodToGetUserPreferenceLanguageInSession()
  lango match {
    case None       => autosetLanguage(Locale.forLanguageTag("ja"), Locale.forLanguageTag("vi"))
    case Some(lang) => language = lang
  }
  true
}

검증 메세지

jQuery Validation 플러그인은 i18n error messages 제공합니다. Xitrum은 자동으로 현재의 언어에 상응하는 메세지를 가져옵니다.

서버의 기본 검증인 xitrum.validator 패키지, 또한 Xitrum은 제공하고 있습니다.

복수형

tn("Message", "Plural form", n)
tcn("Context", "Message", "Plural form", n)

Xitrum은 다음에 정의된 복수형만을 사용합니다:

복수형은 다음 중 하나를 사용해야 합니다:

nplurals=1; plural=0
nplurals=2; plural=n != 1
nplurals=2; plural=n>1
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2
nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2
nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2
nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2
nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3

날짜와 시간 포멧

만약 Scalate 템플릿 엔진을 사용한다면 날짜와 시간 포멧은 현재 액션의 언어 포멧을 따르게 됩니다.

다른 포멧을 사용하고자 하는 경우:

import java.text.{DateFormat, NumberFormat}

val myDateFormat   = ...
val myNumberFormat = ...
val options        = Map("date" -> myDateFormat, "number" -> myNumberFormat)
respondView(options)