Skip to content

Commit

Permalink
Added Scaladoc for most of the core classes, style improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
makkarpov committed Apr 13, 2016
1 parent dad3fb2 commit a8700f0
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 38 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enablePlugins(CrossPerProjectPlugin)

val common = Seq(
organization := "ru.makkarpov",
version := "0.1",
version := "0.2",

crossPaths := true,
scalaVersion := "2.11.7",
Expand Down
50 changes: 47 additions & 3 deletions core/src/main/scala/ru/makkarpov/scalingua/Language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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
}
39 changes: 37 additions & 2 deletions core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
36 changes: 36 additions & 0 deletions core/src/main/scala/ru/makkarpov/scalingua/Messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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$")
Expand All @@ -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]
Expand All @@ -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)
}
21 changes: 21 additions & 0 deletions core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
50 changes: 44 additions & 6 deletions core/src/main/scala/ru/makkarpov/scalingua/StringUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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")
Expand Down Expand Up @@ -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]

Expand All @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 14 additions & 6 deletions scalingua/src/main/scala-2.10/ru/makkarpov/scalingua/Compat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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._

Expand Down
Loading

0 comments on commit a8700f0

Please sign in to comment.