From a8700f076d40188742a73e89bcdb3db197c3e834 Mon Sep 17 00:00:00 2001 From: makkarpov Date: Wed, 13 Apr 2016 16:27:53 +0300 Subject: [PATCH] Added Scaladoc for most of the `core` classes, style improvements --- build.sbt | 2 +- .../ru/makkarpov/scalingua/Language.scala | 50 +++++++++++++++- .../ru/makkarpov/scalingua/LanguageId.scala | 39 +++++++++++- .../ru/makkarpov/scalingua/Messages.scala | 36 +++++++++++ .../ru/makkarpov/scalingua/OutputFormat.scala | 21 +++++++ .../makkarpov/scalingua/PluralFunction.scala | 10 ++++ .../ru/makkarpov/scalingua/StringUtils.scala | 50 ++++++++++++++-- .../scalingua/plugin/Scalingua.scala | 2 + .../ru/makkarpov/scalingua/Compat.scala | 20 +++++-- .../ru/makkarpov/scalingua/Compat.scala | 20 +++++-- .../scala/ru/makkarpov/scalingua/Macros.scala | 59 ++++++++++++++----- 11 files changed, 271 insertions(+), 38 deletions(-) diff --git a/build.sbt b/build.sbt index 432f7eb..44f99c7 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ enablePlugins(CrossPerProjectPlugin) val common = Seq( organization := "ru.makkarpov", - version := "0.1", + version := "0.2", crossPaths := true, scalaVersion := "2.11.7", diff --git a/core/src/main/scala/ru/makkarpov/scalingua/Language.scala b/core/src/main/scala/ru/makkarpov/scalingua/Language.scala index fa0614f..068e39d 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/Language.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/Language.scala @@ -16,12 +16,16 @@ package ru.makkarpov.scalingua -/** - * Base trait for objects reprensenting language files. Can be implicitly summoned from `Messages` and `LanguageId`. - */ object Language { + /** + * Implicit conversion to derive language from available `LanguageId` and `Messages` + */ + @inline implicit def $providedLanguage(implicit msg: Messages, lang: LanguageId): Language = msg.apply(lang) + /** + * A fallback English language that always returns the same message strings. + */ val English: Language = new Language { override def id = LanguageId("en", "US") @@ -33,10 +37,50 @@ object Language { } } +/** + * Base trait for objects reprensenting languages. + */ trait Language { + /** + * A exact (with country part) ID of this language. + */ def id: LanguageId + + /** + * Resolve singular form of message without a context. + * + * @param msgid A message to resolve + * @return Resolved message or `msgid` itself. + */ def singular(msgid: String): String + + /** + * Resolve singular form of message with a context. + * + * @param msgctx A context of message + * @param msgid A message to resolve + * @return Resolved message or `msgid` itself. + */ def singular(msgctx: String, msgid: String): String + + /** + * Resolve plural form of message without a context + * + * @param msgid A singular form of message + * @param msgidPlural A plural form of message + * @param n Numeral representing which form to choose + * @return Resolved plural form of message + */ def plural(msgid: String, msgidPlural: String, n: Long): String + + /** + * Resolve plural form of message with a context. + * + * @param msgctx A context of message + * @param msgid A singular form of message + * @param msgidPlural A plural form of message + * @param n Numeral representing which form to choose + * @return Resolved plural form of message + */ def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String } diff --git a/core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala b/core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala index 7c6dccf..f746d40 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala @@ -16,6 +16,41 @@ package ru.makkarpov.scalingua -case class LanguageId(language: String, country: String) { - override def toString: String = s"${language}_$country" +object LanguageId { + private val languagePattern = "([a-zA-Z]{2,3})(?:[_-]([a-zA-Z]{2,3}))?".r + + /** + * Creates `LanguageId` instance from language codes like `en` or `en-US` + * + * @param s Language code + * @return `Some` with `LanguageId` instance if language code was parsed successfully, or `None` otherwise. + */ + def get(s: String): Option[LanguageId] = s match { + case languagePattern(lang, country) => + Some(LanguageId(lang.toLowerCase, if (country eq null) "" else country.toUpperCase)) + case _ => None + } + + /** + * Creates `LanguageId` instance from language codes like `en` or `en-US` + * + * @param s Language code + * @return `LanguageId` instance + */ + def apply(s: String): LanguageId = get(s).getOrElse(throw new IllegalArgumentException(s"Unrecognized language '$s'")) +} + +/** + * Class representing a pair of language and country (e.g. `en_US`) + * + * @param language ISO code of language + * @param country ISO code of country, may be empty for generic languages. + */ +final case class LanguageId(language: String, country: String) { + /** + * @return Whether the country part is present + */ + @inline def hasCountry = country.nonEmpty + + override def toString: String = language + (if (hasCountry) "-" else "") + country } diff --git a/core/src/main/scala/ru/makkarpov/scalingua/Messages.scala b/core/src/main/scala/ru/makkarpov/scalingua/Messages.scala index 30f9d09..b78a478 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/Messages.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/Messages.scala @@ -17,6 +17,12 @@ package ru.makkarpov.scalingua object Messages { + /** + * Load all available languages that are compiled by SBT plugin. + * + * @param pkg Package to seek in. Must be equal to `localePackage` SBT setting. + * @return A loaded `Messages` + */ def compiled(pkg: String = "locales"): Messages = try { val cls = Class.forName(pkg + (if (pkg.nonEmpty) "." else "") + "Languages$") @@ -27,6 +33,11 @@ object Messages { } } +/** + * Class representing a collection of language. + * + * @param langs Available languages + */ class Messages(langs: Language*) { private val (byLang, byCountry) = { val lng = Map.newBuilder[String, Language] @@ -45,5 +56,30 @@ class Messages(langs: Language*) { (lng.result(), cntr.result()) } + /** + * Retrieves a language from message set by it's ID. The languages are tried in this order: + * 1) Exact language, e.g. `ru_RU` + * 2) Language matched only by language id, e.g. `ru_**` + * 3) Fallback English language + * + * @param lang Language ID to fetch + * @return Fetched language if available, or `Language.English` otherwise. + */ def apply(lang: LanguageId): Language = byCountry.getOrElse(lang, byLang.getOrElse(lang.language, Language.English)) + + /** + * Test whether this messages contains specified language, either exact (`ru_RU`) or fuzzy (`ru_**`). + * + * @param lang Language ID to test + * @return Boolean indicating whether specified language is available + */ + def contains(lang: LanguageId): Boolean = byCountry.contains(lang) || byLang.contains(lang.language) + + /** + * Test whether this messages contains specified language exactly. + * + * @param lang + * @return + */ + def containsExact(lang: LanguageId): Boolean = byCountry.contains(lang) } diff --git a/core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala b/core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala index ec49e94..716974b 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala @@ -17,13 +17,34 @@ package ru.makkarpov.scalingua object OutputFormat { + /** + * String interpolation format that does nothing at all it's already strings. + */ implicit val StringFormat = new OutputFormat[String] { override def convert(s: String): String = s override def escape(s: String): String = s } } +/** + * An implicit evidence that strings could be interpolated into type `R`. + * + * @tparam R Result type of interpolation + */ trait OutputFormat[R] { + /** + * Convert resulting string into type `R` + * + * @param s Complete interpolated string + * @return An instance of `R` + */ def convert(s: String): R + + /** + * Escape interpolation variable. + * + * @param s A string contents of interpolation variable + * @return A escaped string that will be inserted into interpolation output + */ def escape(s: String): String } diff --git a/core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala b/core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala index b5bf4a8..9221d81 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala @@ -16,7 +16,17 @@ package ru.makkarpov.scalingua +/** + * Trait representing `Plural-Forms` *.po header, either statically compiled by SBT plugin or dynamically parsed. + */ trait PluralFunction { + /** + * Number of plural forms in language + */ def numPlurals: Int + + /** + * A plural form for number `n` + */ def plural(n: Long): Int } diff --git a/core/src/main/scala/ru/makkarpov/scalingua/StringUtils.scala b/core/src/main/scala/ru/makkarpov/scalingua/StringUtils.scala index 7c2d588..830e19c 100644 --- a/core/src/main/scala/ru/makkarpov/scalingua/StringUtils.scala +++ b/core/src/main/scala/ru/makkarpov/scalingua/StringUtils.scala @@ -21,11 +21,25 @@ package ru.makkarpov.scalingua * escaping and unescaping. */ object StringUtils { - val VariableCharacter = '%' - val VariableParens = ('(', ')') + /** + * Interpolation placeholder character + */ + val VariableCharacter = '%' + + /** + * Opening and closing interpolation parentheses + */ + val VariableParentheses = '(' -> ')' class InvalidInterpolationException(msg: String) extends IllegalArgumentException(msg) + /** + * Convert escape sequences like `\\n` to their meanings. Differs from `StringContext.treatEscapes` because latter + * does not handle `\\uXXXX` escape codes. + * + * @param s String with literal escape codes + * @return `s` having escape codes replaced with their meanings. + */ def unescape(s: String): String = { val ret = new StringBuilder ret.sizeHint(s.length) @@ -71,6 +85,12 @@ object StringUtils { ret.result() } + /** + * Converts all non-letter and non-printable characters in `s` to their escape codes. + * + * @param s Raw string to escape + * @return Escaped version of `s` + */ def escape(s: String): String = { val ret = new StringBuilder ret.sizeHint(s.length) @@ -93,6 +113,18 @@ object StringUtils { ret.result() } + /** + * Replaces all occurences of placeholders like `%(var)` to corresponding variables in `args` with respect to + * specified `OutputFormat` (all placeholders will be escaped). `%` can be escaped as `%%`. Note: for performance + * reasons this function will not use any `Map`s to index variables, it will use linear search every time it + * encounters a variable. + * + * @param msg Interpolation string + * @param args Interpolation variables + * @param format Desired `OutputFormat` summoned implicitly + * @tparam R Result type + * @return Interpolation result wrapped by `OutputFormat` + */ def interpolate[R](msg: String, args: (String, Any)*)(implicit format: OutputFormat[R]): R = { val result = new StringBuilder @@ -114,8 +146,8 @@ object StringUtils { result += VariableCharacter cursor = pos + 2 - case VariableParens._1 => - val end = msg.indexOf(VariableParens._2, pos + 2) + case VariableParentheses._1 => + val end = msg.indexOf(VariableParentheses._2, pos + 2) if (end == -1) throw new IllegalArgumentException(s"Unterminated variable at $pos") @@ -151,6 +183,12 @@ object StringUtils { format.convert(result.result()) } + /** + * Extracts all referred variables from string and returns a `Set` with names. + * + * @param msg Interpolation string + * @return Set of variable names referred in `msg` + */ def extractVariables(msg: String): Set[String] = { val result = Set.newBuilder[String] @@ -167,8 +205,8 @@ object StringUtils { case VariableCharacter => cursor = pos + 2 - case VariableParens._1 => - val end = msg.indexOf(VariableParens._2, pos + 2) + case VariableParentheses._1 => + val end = msg.indexOf(VariableParentheses._2, pos + 2) if (end == -1) throw new IllegalArgumentException(s"Unterminated variable at $pos") diff --git a/sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/Scalingua.scala b/sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/Scalingua.scala index dbc6820..fac0de5 100644 --- a/sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/Scalingua.scala +++ b/sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/Scalingua.scala @@ -144,7 +144,9 @@ object Scalingua extends AutoPlugin { val idx = { val langs = collectLangs(compileLocales).value val pkg = (localePackage in compileLocales).value + val tgt = filePkg((target in compileLocales).value, pkg) / "Languages.scala" + createParent(tgt) PoCompiler.generateIndex(pkg, tgt, langs) diff --git a/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala b/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala index 48d380a..d0212df 100644 --- a/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala +++ b/scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala @@ -58,8 +58,11 @@ object Compat { }, c.Expr[T](bTree)) } - def generateSingular[T: c.WeakTypeTag](c: Context)(ctx: Option[String], str: String, args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def generateSingular[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { MessageExtractor.singular(c)(ctx, str) @@ -76,8 +79,11 @@ object Compat { generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] } - def generatePlural[T: c.WeakTypeTag](c: Context)(ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], - args: Map[String, c.Tree])(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def generatePlural[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { MessageExtractor.plural(c)(ctx, str, strPlural) @@ -97,8 +103,10 @@ object Compat { generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] } - private def generateInterpolation[T: c.WeakTypeTag](c: Context)(str: c.Expr[String], args: Map[String, c.Tree], - outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + private def generateInterpolation[T: c.WeakTypeTag] + (c: Context) + (str: c.Expr[String], args: Map[String, c.Tree], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { import c.universe._ diff --git a/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala b/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala index 6839544..d38b012 100644 --- a/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala +++ b/scalingua/src/main/scala-2.11/ru/makkarpov/scalingua/Compat.scala @@ -45,8 +45,11 @@ object Compat { (keyLiteral, c.Expr[T](bTree)) } - def generateSingular[T: c.WeakTypeTag](c: Context)(ctx: Option[String], str: String, args: Map[String, c.Tree]) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def generateSingular[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { MessageExtractor.singular(c)(ctx, str) @@ -60,8 +63,11 @@ object Compat { generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] } - def generatePlural[T: c.WeakTypeTag](c: Context)(ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], - args: Map[String, c.Tree])(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def generatePlural[T: c.WeakTypeTag] + (c: Context) + (ctx: Option[String], str: String, strPlural: String, n: c.Expr[Long], args: Map[String, c.Tree]) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { MessageExtractor.plural(c)(ctx, str, strPlural) @@ -75,8 +81,10 @@ object Compat { generateInterpolation[T](c)(tr, args, outputFormat).asInstanceOf[c.Expr[T]] } - private def generateInterpolation[T: c.WeakTypeTag](c: Context)(str: c.Expr[String], args: Map[String, c.Tree], - outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + private def generateInterpolation[T: c.WeakTypeTag] + (c: Context) + (str: c.Expr[String], args: Map[String, c.Tree], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { import c.universe._ diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala index 4e82691..995d2e8 100644 --- a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala +++ b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala @@ -31,7 +31,11 @@ object Macros { * variable is a complex expression, you can pass the name after it, like `t"2 + 2 is \${2 + 2}%(result)"`, so the * interpolator will use `"result"` as name of expression `2 + 2`. */ - def interpolate[T: c.WeakTypeTag](c: Context)(args: c.Expr[Any]*)(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def interpolate[T: c.WeakTypeTag] + (c: Context) + (args: c.Expr[Any]*) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { import c.universe._ val parts = c.prefix.tree match { @@ -58,8 +62,8 @@ object Macros { argName = inferredNames(idx) part = parts(idx + 1) } yield { - if (part.startsWith(StringUtils.VariableCharacter.toString + StringUtils.VariableParens._1)) { - val pos = part.indexOf(StringUtils.VariableParens._2) + if (part.startsWith(StringUtils.VariableCharacter.toString + StringUtils.VariableParentheses._1)) { + val pos = part.indexOf(StringUtils.VariableParentheses._2) val name = part.substring(2, pos) val filtered = part.substring(pos + 1) @@ -85,18 +89,29 @@ object Macros { } /** - * Macro that will check the supplied string that all interpolation variables are present at `args`. + * The whole purpose of this macro, beside of extraction of strings, is to verify that all string interpolation + * variables are present and to omit insertion of `StringUtils.interpolate` if nothing is dynamic. */ - def singular[T: c.WeakTypeTag](c: Context)(msg: c.Expr[String], args: c.Expr[(String, Any)]*) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + def singular[T: c.WeakTypeTag] + (c: Context) + (msg: c.Expr[String], args: c.Expr[(String, Any)]*) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { val (msgid, vars) = verifyVariables(c)(msg, args, None) Compat.generateSingular[T](c)(None, msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } - def singularCtx[T: c.WeakTypeTag](c: Context)(ctx: c.Expr[String], msg: c.Expr[String], args: c.Expr[(String, Any)]*) - (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + /** + * The whole purpose of this macro, beside of extraction of strings, is to verify that all string interpolation + * variables are present and to omit insertion of `StringUtils.interpolate` if nothing is dynamic. + */ + def singularCtx[T: c.WeakTypeTag] + (c: Context) + (ctx: c.Expr[String], msg: c.Expr[String], args: c.Expr[(String, Any)]*) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { val ctxStr = stringLiteral(c)(ctx) val (msgid, vars) = verifyVariables(c)(msg, args, None) @@ -104,8 +119,15 @@ object Macros { Compat.generateSingular[T](c)(Some(ctxStr), msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } - def plural[T: c.WeakTypeTag](c: Context)(msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], - args: c.Expr[(String, Any)]*)(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + /** + * The whole purpose of this macro, beside of extraction of strings, is to verify that all string interpolation + * variables are present and to omit insertion of `StringUtils.interpolate` if nothing is dynamic. + */ + def plural[T: c.WeakTypeTag] + (c: Context) + (msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) val (msgidPlural, _) = verifyVariables(c)(msgPlural, args, Some(n)) @@ -113,8 +135,15 @@ object Macros { Compat.generatePlural[T](c)(None, msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] } - def pluralCtx[T: c.WeakTypeTag](c: Context)(ctx: c.Expr[String], msg: c.Expr[String], msgPlural: c.Expr[String], - n: c.Expr[Long], args: c.Expr[(String, Any)]*)(lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = { + /** + * The whole purpose of this macro, beside of extraction of strings, is to verify that all string interpolation + * variables are present and to omit insertion of `StringUtils.interpolate` if nothing is dynamic. + */ + def pluralCtx[T: c.WeakTypeTag] + (c: Context) + (ctx: c.Expr[String], msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*) + (lang: c.Expr[Language], outputFormat: c.Expr[OutputFormat[T]]): c.Expr[T] = + { val ctxStr = stringLiteral(c)(ctx) val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) @@ -125,8 +154,10 @@ object Macros { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private def verifyVariables(c: Context)(msg: c.Expr[String], args: Seq[c.Expr[(String, Any)]], - n: Option[c.Expr[Long]]): (String, Map[String, c.Tree]) = { + private def verifyVariables + (c: Context) + (msg: c.Expr[String], args: Seq[c.Expr[(String, Any)]], n: Option[c.Expr[Long]]): (String, Map[String, c.Tree]) = + { val msgStr = stringLiteral(c)(msg) val vars = StringUtils.extractVariables(msgStr)