Skip to content

Commit

Permalink
1. Remove unused library
Browse files Browse the repository at this point in the history
2. Restructuring for splitting the API and the XML implementation according to the `OCP`
  • Loading branch information
sauntor committed Apr 12, 2017
1 parent 274bc01 commit 492fcf5
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 106 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ A most lightweight library for translating you application to the local language
</translator>
```
### Usage
1. Add to your project
1. Add `Stranslator` to your project
```sbtshell
libraryDependencies += "com.lingcreative" %% "stranslator" % "1.1.0"
```

2. Code sample in your project
2. Code sample
```scala
import translator._

val translator = Translator()
val locale = new Locale("zh", "CN")
implicit val context = SimpleTranslatorContext(translator, Seq(locale))
Expand All @@ -46,7 +48,7 @@ The `welcome` would be:
`适然,你好!欢迎来到中国!`
###### Notice
1. the line feed(`\n`) after the beginning and before the ending tag of `from`, `locale`s (in `to` tag, i.e. `locale` stands for `<zh_CN>` and `</zh_CN>`), will be ignored.
2. you can break one line into multiple lines by puting an `\ ` to the end of the line(and the leading space is ignored and the space after `\ ` will be preserved).
2. you can break one line into multiple lines by putting an `\ ` to the end of the line(and the leading space is ignored and the space after `\ ` will be preserved).
For example:
```xml
<translator>
Expand Down Expand Up @@ -92,7 +94,7 @@ val translator = Translator("cp://l10n/translator.xml")
> The `<include>` tag **does not** support **relative path**, i.e. you can't include a resource like `../some/other/module.xml`.
### About the `stranslator.Translator`
It's the core API for translating. You can initialize it with an URL, a class path resource which is start with "cp://" (no prefix is identical to it too),
or an external resource on a **Server**(i.e. http://localhost:9090/l10n/my_app.xml).
It's the core API for translating. You can initialize it with an URL. An class path resource will start with "cp://" (no prefix is identical to it too),
or an external resource on a **Web Server** with the corresponding uri pattern(i.e. http://example.com/l10n/demo-app.xml).

### Enjoy it! :tea:
5 changes: 2 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name := "stranslator"
organization := "com.lingcreative"
organizationName := "LingCreative Studio"
version := "1.1.0"
version := "1.2.0-SNAPSHOT"

scalaVersion := "2.12.1"
crossScalaVersions := Seq("2.11.0", "2.12.1")
crossScalaVersions := Seq("2.11.8", "2.12.1")

description := "A most lightweight library for translating you application to the local languages."
homepage := Some(url(s"https://github.com/sauntor/stranslator"))
Expand All @@ -23,4 +23,3 @@ useGpg := true

libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.0.6"
libraryDependencies += "org.specs2" %% "specs2-core" % "3.8.8" % Test
libraryDependencies += "org.specs2" %% "specs2-junit" % "3.8.8" % Test
259 changes: 162 additions & 97 deletions src/main/scala/stranslator/Translator.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,116 @@
package stranslator

import java.net.URI
import java.io.{File, FileDescriptor, InputStream, Reader}
import java.net.URL
import java.util.Locale

import scala.collection.mutable
import scala.util.control.Breaks._
import scala.xml.Elem
import scala.xml.factory.XMLLoader
import scala.xml.{Elem, InputSource}

/** The core API for `Stranslator`, which declares how an translator implementation should behave.
*/
trait Translator {

/** translate the original message `from` to the target dialect `to`.
* @param from the original message
* @param to the target dialect, such as `Seq("zh", "CN")`
* @return the translated message
*/
def tr(from: String, to: Seq[String]): Option[String]

/** translate the original message `from` to the target [[java.util.Locale Locale]] `to`.
*
* @param from the original message
* @param to the target dialect, and it will be turned to a [[scala.collection.Seq Seq&#91;String&#93;]] such as `Seq("zh", "CN")`. But, it's not forced
* to follow this rule, you can implement your own.
* @return
*/
def tr(from: String, to: Locale): Option[String]
}

object Translator {
/** Build a translator from a class path xml file: `l10n/translator.xml`.
*
* @return the translator
*/
def apply() = new XmlTranslator()

/** Build a translator from a xml file who's url is `source`. The url of the resource which contains translations,
* a resource from class path should start with `cp://` or no prefix, and a url start with other scheme(e.g. file://,
* http://, and etc.) will be loaded with [[java.net.URL]].
*
* @return the translator
*/
def apply(source: String) = new XmlTranslator(source)
def apply(source: Elem) = new XmlTranslator(() => source)
def apply(source: InputStream) = new XmlTranslator(() => Sources.load(source))
def apply(source: InputSource) = new XmlTranslator(() => Sources.load(source))
def apply(source: Reader) = new XmlTranslator(() => Sources.load(source))
def apply(source: URL) = new XmlTranslator(() => Sources.load(source))
def apply(source: File) = new XmlTranslator(() => Sources.loadFile(source))
def apply(source: FileDescriptor) = new XmlTranslator(() => Sources.loadFile(source))

protected trait TranslatorSupport {
def apply(message: =>String)(implicit context: TranslatorContext): String = {
var translated: Option[String] = None
breakable {
for (locale <- context.locales) {
translated = context.translator.tr(message, localeToSeq(locale))
if (translated.isDefined) break
}
}
translated match {
case Some(str) => str
case None => ""
}
}
}

implicit def localeToSeq(locale: Locale) = {
var seq = Seq[String]()
if (locale.getVariant != "") seq +:= locale.getVariant
if (locale.getCountry != "") seq +:= locale.getCountry
if (locale.getLanguage != "") seq +:= locale.getLanguage
seq
}

/** An helper make translator easy to use.
*
* For example: {{{
* val translator = Translator()
* val locale = new Locale("zh", "CN")
* implicit val context = SimpleTranslatorContext(translator, locale)
*
* val welcome = ${"Hello, Sauntor! Welcome to China!"}
* }}}
* If you create a translation file which located in `l10n/translator.xml`(within class path) with the flowing content: {{{
* <translator>
* <message>
* <from>Hello, Sauntor! Welcome to China!</from>
* <to>
* <zh_CN>适然,你好!欢迎来到中国!</zh_CN>
* </to>
* </translator>
* }}}
* The `welcome` would be: {{{
* 适然,你好!欢迎来到中国!
* }}}
*
*/
object $ extends TranslatorSupport

object ! extends TranslatorSupport

implicit class StringInterpolator(private val sc: StringContext) {
def $(args: Any*)(implicit context: TranslatorContext): String = {
sc.checkLengths(args)
if (args.size > 1) throw new IllegalArgumentException("The message to be translated can't take any place holder!")
Translator.$(sc.parts(0))
}
}
}

/** A holder for a specific message and it's translations.
*
Expand All @@ -16,68 +120,71 @@ import scala.xml.factory.XMLLoader
*/
case class Message(from: String, to: Map[Seq[String], String])

/** Create a translator and load translations from `source`.
/** A translator loading it's translations from an xml file. The format of the translation xml file is: {{{
* <?xml version="1.0" encoding="UTF-8" ?>
* <translator>
* <message>
* <from>This is the original message!</from>
* <to>
* <!-- the translation for chinese -->
* <zh>这是原始文本!</zh>
* <!-- the translation for traditional chinese(Hong Kong) -->
* <zh_HK>這是原始文本!</zh_HK>
* </to>
* </message>
* </translator>
* }}}
*
* @constructor Create a new translator which can translate messages using a resource who's url is `source`.
* @param source the url of the resource which contains translations, a resource from class path should start with `cp://`
* or no prefix, and a url start with other scheme(e.g. file://, http://, and etc.) will be loaded
* with [[java.net.URI]].
* @constructor Create a new translator which can translate messages using a [[scala.xml.Elem]] as the `source`.
* @param source the factory to build an [[scala.xml.Elem]]
*/
case class Translator(val source:String) {
case class XmlTranslator(val source: () => Elem) extends Translator {

import Translator._

private val rSplit = "(_|-)".r
private val rClasspath = "cp://(.*)|(?<![a-z]+://)(.*)".r
private val rURL = "([a-zA-Z0-9]+://)(.*)".r
import XmlTranslator._

@volatile
private var caches = Map[String, Option[String]]()

lazy val translations = {
val messages = mutable.Buffer[Message]()
read(load(source), messages)
read(source(), messages)
messages.toIndexedSeq
}

/** Create a new translator which can translate messages using a resource who's url is `source`.
* @param source the url of the resource which contains translations, a resource from class path should start with `cp://`
* or no prefix, and a url start with other scheme(e.g. file://, http://, and etc.) will be loaded
* with [[java.net.URL]].
*/
def this(source: String) = {
this(() => Sources.loadSource(source))
}

/** Create a translator using classpath resource `l10n/translator.xml`.
*/
def this() = {
this("cp://l10n/translator.xml")
}

protected def load(source: String) = {
loader.load(source match {
case rClasspath(s1, s2) =>
val s = if (s1 == null) s2 else s1
fromClasspath(s) match {
case Some(resource) => resource
case None => throw new IllegalArgumentException(s"""Can't load translations "$s" from class path, please check if it is exists!""")
}
case rURL(s) =>
new URI(s).toURL()
case other =>
throw new IllegalArgumentException(s"""Bad source path: "$other"""")
})
}

protected def read(root: Elem, messages: mutable.Buffer[Message]): Unit = {
for (node <- root \ "_") {
node.label match {
case "include" =>
val subSource = node.text.trim
if (subSource != "") read(load(subSource), messages)
if (subSource != "") read(Sources.loadSource(subSource), messages)
case "message" =>
var to = mutable.Map[Seq[String], String]()
val from = cleanup((node \\ "from").text)
val from = filter((node \\ "from").text)
for (toNode <- (node \\ "to" \ "_")) {
to += (rSplit.split(toNode.label).toSeq -> cleanup(toNode.text))
to += (rSplit.split(toNode.label).toSeq -> filter(toNode.text))
}
messages += Message(from, to.toMap)
}
}
}

def tr(from: String, to: Locale): Option[String] = tr(from, Translator.localeToSeq(to))

/** Translate the message `from` to the target dialect `to`, and the `to` is in general form.
* @param from the original message to translate
* @param to the target dialect, may build from a [[java.util.Locale]]
Expand Down Expand Up @@ -112,7 +219,7 @@ case class Translator(val source:String) {
messageMatched
}

protected def cleanup(s: String) = {
protected def filter(s: String): String = {
val builder = StringBuilder.newBuilder
var bs = -2
var begin = true
Expand Down Expand Up @@ -150,6 +257,11 @@ case class Translator(val source:String) {
}
}


object XmlTranslator {
private val rSplit = "(_|-)".r
}

/** The context which provides a translator and the current candidate locales.
*/
trait TranslatorContext {
Expand All @@ -162,76 +274,29 @@ case class SimpleTranslatorContext(private val _translator: Translator, private
override def locales: Seq[Locale] = _locale
}

object Translator {
private object loader extends XMLLoader[Elem] {}
private[stranslator] object Sources extends XMLLoader[Elem] {

private val defaultCL = classOf[Translator].getClassLoader
private val rClasspath = "cp://(.*)|(?<![a-z]+://)(.*)".r
private val rURL = "([a-zA-Z0-9]+://)(.*)".r

private def fromClasspath(name: String) = {
private val defaultCL = classOf[XmlTranslator].getClassLoader
def fromClasspath(name: String) = {
var cl = Thread.currentThread().getContextClassLoader
cl = if (cl == null) defaultCL else cl
Option(cl.getResource(name))
}

private def localeToSeq(locale: Locale) = {
var seq = Seq[String]()
if (locale.getVariant != "") seq +:= locale.getVariant
if (locale.getCountry != "") seq +:= locale.getCountry
if (locale.getLanguage != "") seq +:= locale.getLanguage
seq
}

def apply() = new Translator()

protected trait TranslatorSupport {
def apply(message: =>String)(implicit context: TranslatorContext): String = {
var translated: Option[String] = None
breakable {
for (locale <- context.locales) {
translated = context.translator.tr(message, localeToSeq(locale))
if (translated.isDefined) break
def loadSource(source: String) = {
Sources.load(source match {
case rClasspath(s1, s2) =>
val s = if (s1 == null) s2 else s1
fromClasspath(s) match {
case Some(resource) => resource
case None => throw new IllegalArgumentException(s"""Can't load translations "$s" from class path, please check if it is exists!""")
}
}
translated match {
case Some(str) => str
case None => ""
}
}
}

/** An helper make translator easy to use.
*
* For example: {{{
* val translator = Translator()
* val locale = new Locale("zh", "CN")
* implicit val context = SimpleTranslatorContext(translator, locale)
*
* val welcome = ${"Hello, Sauntor! Welcome to China!"}
* }}}
* If you create a translation file which located in `l10n/translator.xml`(within class path) with the flowing content: {{{
* <translator>
* <message>
* <from>Hello, Sauntor! Welcome to China!</from>
* <to>
* <zh_CN>适然,你好!欢迎来到中国!</zh_CN>
* </to>
* </translator>
* }}}
* The `welcome` would be: {{{
* 适然,你好!欢迎来到中国!
* }}}
*
*/
object $ extends TranslatorSupport

object ! extends TranslatorSupport

implicit class StringInterpolator(private val sc: StringContext) {
def $(args: Any*)(implicit context: TranslatorContext): String = {
sc.checkLengths(args)
if (args.size > 1) throw new IllegalArgumentException("The message to be translated can't take any place holder!")
Translator.$(sc.parts(0))
}
case rURL(s) =>
new URL(s)
case other =>
throw new IllegalArgumentException(s"""Bad source path: "$other"""")
})
}
}

2 changes: 1 addition & 1 deletion src/test/scala/stranslator/TranslatorSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package stranslator

import java.util.Locale

import stranslator.Translator.$
import stranslator.Translator._

/**
* Translator test spec.
Expand Down

0 comments on commit 492fcf5

Please sign in to comment.