From 492fcf525448f8c61d07dddad235d295d0bfeea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=80=82=E7=84=B6?= Date: Wed, 12 Apr 2017 21:26:08 +0800 Subject: [PATCH] 1. Remove unused library 2. Restructuring for splitting the API and the XML implementation according to the `OCP` --- README.md | 12 +- build.sbt | 5 +- src/main/scala/stranslator/Translator.scala | 259 +++++++++++------- .../scala/stranslator/TranslatorSpec.scala | 2 +- 4 files changed, 172 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 59ca0cc..83d9fbd 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,15 @@ A most lightweight library for translating you application to the local language ``` ### 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)) @@ -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 `` and ``), 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 @@ -92,7 +94,7 @@ val translator = Translator("cp://l10n/translator.xml") > The `` 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: diff --git a/build.sbt b/build.sbt index d61f18b..9a96d95 100644 --- a/build.sbt +++ b/build.sbt @@ -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")) @@ -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 diff --git a/src/main/scala/stranslator/Translator.scala b/src/main/scala/stranslator/Translator.scala index e9366c7..12b9a5c 100644 --- a/src/main/scala/stranslator/Translator.scala +++ b/src/main/scala/stranslator/Translator.scala @@ -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[String]]] 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: {{{ + * + * + * Hello, Sauntor! Welcome to China! + * + * 适然,你好!欢迎来到中国! + * + * + * }}} + * 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. * @@ -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: {{{ + * + * + * + * This is the original message! + * + * + * 这是原始文本! + * + * 這是原始文本! + * + * + * + * }}} * - * @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://(.*)|(? 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]] @@ -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 @@ -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 { @@ -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://(.*)|(?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: {{{ - * - * - * Hello, Sauntor! Welcome to China! - * - * 适然,你好!欢迎来到中国! - * - * - * }}} - * 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"""") + }) } } - diff --git a/src/test/scala/stranslator/TranslatorSpec.scala b/src/test/scala/stranslator/TranslatorSpec.scala index 316c4e7..dcc505a 100644 --- a/src/test/scala/stranslator/TranslatorSpec.scala +++ b/src/test/scala/stranslator/TranslatorSpec.scala @@ -2,7 +2,7 @@ package stranslator import java.util.Locale -import stranslator.Translator.$ +import stranslator.Translator._ /** * Translator test spec.