Skip to content

Commit 827db44

Browse files
committed
initial
0 parents  commit 827db44

14 files changed

+336
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pom.xml
2+
*~
3+
*.iml
4+
/target
5+
/project/target/
6+
/project/project/target/
7+
/.ensime
8+
.DS_Store
9+
/.idea/

LICENSE.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright (C) 2013 Alexander Azarov <[email protected]>
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Introduction
2+
3+
Small library to support i18n messages in Scala "just like in Play Framework".
4+
5+
* properties files
6+
* in UTF-8 (vs. Java properties files in ISO 8859-1)
7+
* formatted with `MessageFormat`
8+
9+
It provides a trivial library to "localize" application entities as well.
10+
11+
# Messages
12+
13+
Mostly just like in [Play
14+
Framework](https://www.playframework.com/documentation/2.3.x/ScalaI18N) (the
15+
differences are outlined below). Messages are in `messages_XXX.txt` (mind the
16+
file extension, Play does not use any) files in UTF-8 encoding in the
17+
application's resources (Play's messages reside in `/conf` ). There is a default
18+
file `messages.txt` where the key is looked up when it cannot be found in the
19+
language-specific file, e.g. `messages_en.txt` or `messages_ru.txt`.
20+
21+
The messages are formatted using Java's `java.util.MessageFormat`. Mind the
22+
apostrophes (see the details in
23+
[Javadoc](http://docs.oracle.com/javase/7/docs/api/java/text/MessageFormat.html))!
24+
25+
`Messages` call depend on the implicit parameter of type `Lang` which represents
26+
the language. It's being used in `Localized` as well, see below.
27+
28+
You would use `Messages` so:
29+
30+
```scala
31+
implicit val userLang = Lang("en")
32+
33+
val msg = Messages("greet", userLang)
34+
```
35+
36+
# Localized
37+
38+
You may want to localize for some application's entity, e.g. a user or a
39+
session. In this case, you may implicitly determine this entity's preferred
40+
language:
41+
42+
```scala
43+
case class User(id: Int, lang: Lang)
44+
45+
object User {
46+
implicit object userLocale extends Localized {
47+
override def locale(user: User) = user.lang
48+
}
49+
}
50+
```
51+
52+
and format the whole page or email for any user later:
53+
54+
```
55+
val email =
56+
Localized(user) { implicit lang =>
57+
val greet = Messages("email.greet", user.fullName)
58+
val text = Messages("email.text")
59+
s"$greet $text"
60+
}
61+
```
62+
63+
# Credits
64+
65+
* https://gist.github.com/alaz/1388917
66+
* http://stackoverflow.com/questions/4659929/how-to-use-utf-8-in-resource-properties-with-resourcebundle
67+
68+
# License
69+
70+
Apache 2.0

build.sbt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
organization := "com.osinka.i18n"
2+
3+
name := "scala-i18n"
4+
5+
homepage := Some(url("https://github.com/osinka/scala-i18n"))
6+
7+
startYear := Some(2014)
8+
9+
scalaVersion := "2.11.2"
10+
11+
crossScalaVersions := Seq("2.11.2", "2.10.4")
12+
13+
licenses += "Apache License, Version 2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")
14+
15+
organizationName := "Osinka"
16+
17+
description := """MongoDB Document parser combinators and builders"""
18+
19+
scalacOptions ++= List("-deprecation", "-unchecked", "-feature")
20+
21+
libraryDependencies ++= Seq(
22+
"org.scalatest" %% "scalatest" % "2.2.2" % "test"
23+
)
24+
25+
credentials <+= (version) map { version: String =>
26+
val file =
27+
if (version.trim endsWith "SNAPSHOT") "credentials_osinka"
28+
else "credentials_sonatype"
29+
Credentials(Path.userHome / ".ivy2" / file)
30+
}
31+
32+
pomIncludeRepository := { x => false }
33+
34+
publishTo <<= (version) { version: String =>
35+
Some(
36+
if (version.trim endsWith "SNAPSHOT")
37+
"Osinka Internal Repo" at "http://repo.osinka.int/content/repositories/snapshots/"
38+
else
39+
"Sonatype OSS Staging" at "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
40+
)
41+
}
42+
43+
useGpg := true
44+
45+
pomExtra := <xml:group>
46+
<developers>
47+
<developer>
48+
<id>alaz</id>
49+
<email>azarov@osinka.com</email>
50+
<name>Alexander Azarov</name>
51+
<timezone>+3</timezone>
52+
</developer>
53+
<developer>
54+
<id>lavrov</id>
55+
<email>lavrov@osinka.com</email>
56+
<name>Vitaly Lavrov</name>
57+
<timezone>+4</timezone>
58+
</developer>
59+
</developers>
60+
<scm>
61+
<connection>scm:git:git://github.com/osinka/scala-i18n.git</connection>
62+
<developerConnection>scm:git:git@github.com:osinka/scala-i18n.git</developerConnection>
63+
<url>http://github.com/osinka/scala-i18n</url>
64+
</scm>
65+
<issueManagement>
66+
<system>github</system>
67+
<url>http://github.com/osinka/scala-i18n/issues</url>
68+
</issueManagement>
69+
</xml:group>

project/build.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=0.13.5

project/pgp.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.1")

src/main/scala/Lang.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.osinka.i18n
2+
3+
import java.util.Locale
4+
5+
case class Lang(locale: Locale) {
6+
def language = locale.getLanguage
7+
def country = locale.getCountry
8+
}
9+
10+
object Lang {
11+
val Default = Lang(Locale.getDefault)
12+
13+
def apply(language: String): Lang = Lang(new Locale(language))
14+
15+
def apply(maybeLang: Option[String], default: Lang = Default): Lang = maybeLang.map(apply) getOrElse default
16+
}

src/main/scala/Localized.scala

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.osinka.i18n
2+
3+
/** An entity's preferred language.
4+
*
5+
* A convenient type class to represent a preferred language of a user or session or whatever.
6+
*
7+
* Use it in the companion object:
8+
*
9+
* {{{
10+
* case class User(id: Int, lang: Lang)
11+
*
12+
* object User {
13+
* implicit object localized extends Localized[User] {
14+
* override def locale(user: User) = user.lang
15+
* }
16+
* }
17+
* }}}
18+
*
19+
* @see [[Lang]]
20+
*/
21+
trait Localized[T] {
22+
def locale(a: T): Lang
23+
}
24+
25+
/** Provides a helper for "localized" objects.
26+
*
27+
* For example, for [[Messages]]:
28+
*
29+
* {{{
30+
* Localized(user) { implicit lang =>
31+
* Messages("error")
32+
* }
33+
* }}}
34+
*
35+
* @see [[Lang]]
36+
*/
37+
object Localized {
38+
def apply[T: Localized, R](a: T)(fn: Lang => R) = {
39+
val localized = implicitly[Localized[T]]
40+
fn( localized.locale(a) )
41+
}
42+
43+
implicit def optionLocalized[T](implicit localized: Localized[T]) =
44+
new Localized[Option[T]] {
45+
override def locale(o: Option[T]) =
46+
o map(localized.locale) getOrElse Lang.Default
47+
}
48+
}

src/main/scala/Messages.scala

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.osinka.i18n
2+
3+
import java.text.MessageFormat
4+
import java.util.{ResourceBundle, Locale}
5+
6+
/** Messages externalization
7+
*
8+
* == Overview ==
9+
* You would use it like so:
10+
*
11+
* {{{
12+
* Localized(user) { implicit lang =>
13+
* val error = Messages("error")
14+
* }
15+
* }}}
16+
*
17+
* Messages are stored in `messages_XXX.txt` files in UTF-8 encoding in resources.
18+
* The lookup will fallback to default file `messages.txt` if the string is not found in
19+
* the language-specific file.
20+
*
21+
* Messages are formatted with `java.text.MessageFormat`.
22+
*/
23+
object Messages {
24+
val FileName = "messages"
25+
val FileExt = "txt"
26+
27+
/** get the message w/o formatting */
28+
def raw(msg: String)(implicit lang: Lang): String = {
29+
val bundle = ResourceBundle.getBundle(FileName, lang.locale, UTF8BundleControl)
30+
bundle.getString(msg)
31+
}
32+
33+
def apply(msg: String, args: Any*)(implicit lang: Lang): String = {
34+
new MessageFormat(raw(msg), lang.locale).format(args.map(_.asInstanceOf[java.lang.Object]).toArray)
35+
}
36+
}
37+
38+
// @see https://gist.github.com/alaz/1388917
39+
// @see http://stackoverflow.com/questions/4659929/how-to-use-utf-8-in-resource-properties-with-resourcebundle
40+
private[i18n] object UTF8BundleControl extends ResourceBundle.Control {
41+
val Format = "properties.utf8"
42+
val FallbackLocale = new Locale("")
43+
44+
override def getFormats(baseName: String): java.util.List[String] = {
45+
import collection.convert.decorateAsJava._
46+
47+
Seq(Format).asJava
48+
}
49+
50+
override def getFallbackLocale(baseName: String, locale: Locale) = FallbackLocale
51+
52+
override def newBundle(baseName: String, locale: Locale, fmt: String, loader: ClassLoader, reload: Boolean): ResourceBundle = {
53+
import java.util.PropertyResourceBundle
54+
import java.io.InputStreamReader
55+
56+
// The below is an approximate copy of the default Java implementation
57+
def resourceName = toResourceName(toBundleName(baseName, locale), Messages.FileExt)
58+
59+
def stream =
60+
if (reload) {
61+
for {url <- Option(loader getResource resourceName)
62+
connection <- Option(url.openConnection)}
63+
yield {
64+
connection.setUseCaches(false)
65+
connection.getInputStream
66+
}
67+
} else
68+
Option(loader getResourceAsStream resourceName)
69+
70+
(for {format <- Option(fmt) if format == Format
71+
is <- stream}
72+
yield new PropertyResourceBundle(new InputStreamReader(is, "UTF-8"))).orNull
73+
}
74+
}

src/test/resources/messages.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
hello=Hello
2+
world=World

src/test/resources/messages_en.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
greet=Hello, {0}

src/test/resources/messages_ru.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
hello=Привет
2+
greet=Привет, {0}

src/test/scala/messagesSpec.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.osinka.i18n
2+
3+
import org.scalatest.{Matchers, FunSpec}
4+
5+
class messagesSpec extends FunSpec with Matchers {
6+
val EN = Lang("en")
7+
val RU = Lang("ru")
8+
9+
describe("Messages") {
10+
it("should return localized message") {
11+
Messages("hello")(EN) should equal("Hello")
12+
Messages("hello")(RU) should equal("Привет")
13+
}
14+
it("should fallback to default") {
15+
Messages("world")(EN) should equal("World")
16+
Messages("world")(RU) should equal("World")
17+
}
18+
it("should format") {
19+
Messages("greet", "world")(EN) should equal("Hello, world")
20+
Messages("greet", "world")(RU) should equal("Привет, world")
21+
}
22+
it("should throw when no default") {
23+
intercept[java.util.MissingResourceException] {
24+
Messages("nokey")(RU)
25+
}
26+
}
27+
}
28+
}

version.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
version in ThisBuild := "1.0.0-SNAPSHOT"

0 commit comments

Comments
 (0)