From 94e4a858bb436c296f1be5ba6bce9078ae799e6e Mon Sep 17 00:00:00 2001 From: makkarpov Date: Thu, 14 Apr 2016 14:13:08 +0300 Subject: [PATCH] Rewritten macros, added lazily resolved strings (when language is not known in advance). --- build.sbt | 3 +- .../scala/ru/makkarpov/scalingua/LValue.scala | 31 ++ .../ru/makkarpov/scalingua/Compat.scala | 1 + .../ru/makkarpov/scalingua/Compat.scala | 1 + .../scala/ru/makkarpov/scalingua/I18n.scala | 26 +- .../ru/makkarpov/scalingua/MacroUtils.scala | 112 ------- .../scala/ru/makkarpov/scalingua/Macros.scala | 314 ++++++++++++------ .../scalingua/test/IStringTest.scala | 53 +++ .../makkarpov/scalingua/test/MacroTest.scala | 5 + 9 files changed, 327 insertions(+), 219 deletions(-) create mode 100644 core/src/main/scala/ru/makkarpov/scalingua/LValue.scala delete mode 100644 scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala create mode 100644 scalingua/src/test/scala/ru/makkarpov/scalingua/test/IStringTest.scala diff --git a/build.sbt b/build.sbt index 21e4b3f..54f667b 100644 --- a/build.sbt +++ b/build.sbt @@ -23,8 +23,9 @@ val common = Seq( licenses := Seq("Apache 2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")), homepage := Some(url("https://github.com/makkarpov/scalingua")), + organizationHomepage := Some(url("https://github.com/makkarpov")), scmInfo := Some(ScmInfo( - browseUrl = new URL("https://github.com/makkarpov/scalingua"), + browseUrl = url("https://github.com/makkarpov/scalingua"), connection = "scm:git://github.com/makkarpov/scalingua.git" )), diff --git a/core/src/main/scala/ru/makkarpov/scalingua/LValue.scala b/core/src/main/scala/ru/makkarpov/scalingua/LValue.scala new file mode 100644 index 0000000..4713157 --- /dev/null +++ b/core/src/main/scala/ru/makkarpov/scalingua/LValue.scala @@ -0,0 +1,31 @@ +/****************************************************************************** + * Copyright © 2016 Maxim Karpov * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ******************************************************************************/ + +package ru.makkarpov.scalingua + +import scala.language.implicitConversions + +object LValue { + implicit def unwrapLvalue[T](lValue: LValue[T])(implicit lang: Language): T = lValue.resolve +} + +/** + * Value that can be translated lazily (e.g. for definitions whose target language is not known a priori) + */ +class LValue[T](func: Language => T) { + def resolve(implicit lang: Language) = func(lang) + override def toString: String = s"LValue(${func(Language.English)})" +} 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 e5baa2b..5656f70 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 @@ -19,4 +19,5 @@ package ru.makkarpov.scalingua object Compat { type Context = scala.reflect.macros.Context def prettyPrint(c: Context)(e: c.Tree): String = c.universe.show(e) + def termName(c: Context)(s: String): c.TermName = c.universe.newTermName(c.fresh(s)) } 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 da113d0..8404b07 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 @@ -23,4 +23,5 @@ import scala.reflect.macros.whitebox object Compat { type Context = whitebox.Context def prettyPrint(c: Context)(e: c.Tree): String = c.universe.showCode(e) + def termName(c: Context)(s: String): c.TermName = c.universe.TermName(c.freshName(s)) } diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/I18n.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/I18n.scala index becaca2..f222eb4 100644 --- a/scalingua/src/main/scala/ru/makkarpov/scalingua/I18n.scala +++ b/scalingua/src/main/scala/ru/makkarpov/scalingua/I18n.scala @@ -18,24 +18,40 @@ package ru.makkarpov.scalingua import scala.language.experimental.macros -/** - * Dynamic translation functions and the basic set of macro-based interpolators and translation functions. - */ object I18n { implicit class StringInterpolator(val sc: StringContext) extends AnyVal { def t(args: Any*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = macro Macros.interpolate[String] + + def lt(args: Any*)(implicit outputFormat: OutputFormat[String]): LValue[String] = + macro Macros.lazyInterpolate[String] } def t(msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = macro Macros.singular[String] + def lt(msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LValue[String] = + macro Macros.lazySingular[String] + def tc(ctx: String, msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = macro Macros.singularCtx[String] - def p(msg: String, msgPlural: String, n: Long, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = + def ltc(ctx: String, msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LValue[String] = + macro Macros.lazySingularCtx[String] + + def p(msg: String, msgPlural: String, n: Long, args: (String, Any)*) + (implicit lang: Language, outputFormat: OutputFormat[String]): String = macro Macros.plural[String] - def pc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = + def lp(msg: String, msgPlural: String, n: Long, args: (String, Any)*) + (implicit outputFormat: OutputFormat[String]): LValue[String] = + macro Macros.lazyPlural[String] + + def pc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) + (implicit lang: Language, outputFormat: OutputFormat[String]): String = macro Macros.pluralCtx[String] + + def lpc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) + (implicit outputFormat: OutputFormat[String]): LValue[String] = + macro Macros.lazyPluralCtx[String] } diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala deleted file mode 100644 index 6a8d97a..0000000 --- a/scalingua/src/main/scala/ru/makkarpov/scalingua/MacroUtils.scala +++ /dev/null @@ -1,112 +0,0 @@ -/****************************************************************************** - * Copyright © 2016 Maxim Karpov * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ******************************************************************************/ - -package ru.makkarpov.scalingua - -import Compat._ -import ru.makkarpov.scalingua.extract.MessageExtractor - -protected object MacroUtils { - def stringLiteral[T](c: Context)(e: c.Tree): String = { - import c.universe._ - - e match { - case Literal(Constant(s: String)) => s - - case q"scala.this.Predef.augmentString($str).stripMargin" => - str match { - case Literal(Constant(s: String)) => s.stripMargin.trim - case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") - } - - case q"scala.this.Predef.augmentString($str).stripMargin($ch)" => - (str, ch) match { - case (Literal(Constant(s: String)), Literal(Constant(c: Char))) => s.stripMargin(c).trim - case (Literal(Constant(s: String)), _) => - c.abort(c.enclosingPosition, s"Expected character literal, got instead ${prettyPrint(c)(ch)}") - case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") - } - - case _ => - c.abort(c.enclosingPosition, s"Expected string literal or multi-line string, got instead ${prettyPrint(c)(e)}") - } - } - - def tupleLiteral[T](c: Context)(e: c.Expr[(String, T)]): (String, c.Expr[T]) = { - import c.universe._ - - val (aTree, bTree) = e.tree match { - case q"scala.this.Predef.ArrowAssoc[$aType]($a).->[$bType]($b)" => (a, b) // 2.11 - case q"scala.this.Predef.any2ArrowAssoc[$aType]($a).->[$bType]($b)" => (a, b) // 2.10 - case q"($a, $b)" => (a, b) - case _ => - c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${prettyPrint(c)(e.tree)}") - } - - val keyLiteral = aTree match { - case Literal(Constant(x: String)) => x - case _ => c.abort(c.enclosingPosition, s"Expected string literal as first entry of tuple, got ${prettyPrint(c)(e.tree)} instead") - } - - (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] = - { - MessageExtractor.singular(c)(ctx, str) - - import c.universe._ - - val tr = c.Expr[String](ctx match { - case Some(s) => q"${lang.tree}.singular($s, $str)" - case None => q"${lang.tree}.singular($str)" - }) - - 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] = - { - MessageExtractor.plural(c)(ctx, str, strPlural) - - import c.universe._ - - val tr = c.Expr[String](ctx match { - case Some(s) => q"${lang.tree}.plural($s, $str, $strPlural, ${n.tree})" - case None => q"${lang.tree}.plural($str, $strPlural, ${n.tree})" - }) - - 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] = - { - import c.universe._ - - c.Expr[T](if (args.nonEmpty) { - val argList = args.map { case (k, v) => q"$k -> $v" } - q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}](${str.tree}, ..$argList)(${outputFormat.tree})" - } else q"${outputFormat.tree}.convert(${str.tree})") - } -} diff --git a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala index 98520e0..531c5d1 100644 --- a/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala +++ b/scalingua/src/main/scala/ru/makkarpov/scalingua/Macros.scala @@ -17,46 +17,114 @@ package ru.makkarpov.scalingua import Compat._ -import MacroUtils._ - -/** - * An entry point for all macros which may be useful if you want to define custom functions (like your own `Utils.t` - * referencing this macros) or custom specialized string interpolators (like `th""` for HTML). Without this class it - * would be impossible, since macros will expand at your `Utils` class and complain that string literal is required - * for I18n message. - */ +import ru.makkarpov.scalingua.extract.MessageExtractor + object Macros { - /** - * String interpolator that will infer the name of the variables passed in it and create an interpolation string - * based on them. For example, `t"Hello, \$name"` will be converted to string `"Hello, %(name)!"`. If interpolation - * 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) + // All macros variants: (lazy, eager) x (interpolation | (singular, plural) x (ctx, non ctx)), 10 total + + // Interpolators: + + 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 (msg, argsT) = interpolator(c)(args.map(_.tree)) + c.Expr[T](generate[T](c)(None, q"$msg", None, argsT)(Some(lang.tree), outputFormat.tree)) + } + + def lazyInterpolate[T: c.WeakTypeTag](c: Context) + (args: c.Expr[Any]*) + (outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] = + { + import c.universe._ + val (msg, argsT) = interpolator(c)(args.map(_.tree)) + c.Expr[LValue[T]](generate[T](c)(None, q"$msg", None, argsT)(None, outputFormat.tree)) + } + + // Singular: + + 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] = + c.Expr[T](generate[T](c)(None, msg.tree, None, args.map(_.tree))(Some(lang.tree), outputFormat.tree)) + + def lazySingular[T: c.WeakTypeTag](c: Context) + (msg: c.Expr[String], args: c.Expr[(String, Any)]*) + (outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] = + c.Expr[LValue[T]](generate[T](c)(None, msg.tree, None, args.map(_.tree))(None, outputFormat.tree)) + + 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] = + c.Expr[T](generate[T](c)(Some(ctx.tree), msg.tree, None, args.map(_.tree))(Some(lang.tree), outputFormat.tree)) + + def lazySingularCtx[T: c.WeakTypeTag](c: Context) + (ctx: c.Expr[String], msg: c.Expr[String], args: c.Expr[(String, Any)]*) + (outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] = + c.Expr[LValue[T]](generate[T](c)(Some(ctx.tree), msg.tree, None, args.map(_.tree))(None, outputFormat.tree)) + + // Plural: + + 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] = + c.Expr[T](generate[T](c)(None, msg.tree, Some(msgPlural.tree -> n.tree), args.map(_.tree)) + (Some(lang.tree), outputFormat.tree)) + + def lazyPlural[T: c.WeakTypeTag](c: Context) + (msg: c.Expr[String], msgPlural: c.Expr[String], n: c.Expr[Long], args: c.Expr[(String, Any)]*) + (outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] = + c.Expr[LValue[T]](generate[T](c)(None, msg.tree, Some(msgPlural.tree -> n.tree), args.map(_.tree)) + (None, outputFormat.tree)) + + 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] = + c.Expr[T](generate[T](c)(Some(ctx.tree), msg.tree, Some(msgPlural.tree -> n.tree), args.map(_.tree)) + (Some(lang.tree), outputFormat.tree)) + + def lazyPluralCtx[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)]*) + (outputFormat: c.Expr[OutputFormat[T]]): c.Expr[LValue[T]] = + c.Expr[LValue[T]](generate[T](c)(Some(ctx.tree), msg.tree, Some(msgPlural.tree -> n.tree), args.map(_.tree)) + (None, outputFormat.tree)) + + // Macro internals: + + /** + * A generic macro that extracts interpolation string and set of interpolation + * variables from string interpolator invocation. + * + * @param c Macro context + * @param args Arguments of interpolator + * @return (Extracted string, extracted variables) + */ + private def interpolator(c: Context)(args: Seq[c.Tree]): (String, Seq[c.Tree]) = { + import c.universe._ + // Extract raw interpolation parts val parts = c.prefix.tree match { case Apply(_, List(Apply(_, rawParts))) => rawParts.map(stringLiteral(c)(_)).map(StringContext.treatEscapes) + case _ => - c.abort(c.enclosingPosition, "Failed to detect application context") + c.abort(c.enclosingPosition, s"failed to match prefix, got ${prettyPrint(c)(c.prefix.tree)}") } assert(parts.size == args.size + 1) - val inferredNames = args.map(_.tree).map { + // Try to infer names for simple variables + val inferredNames = args.map { case Ident(name: TermName) => Some(name.decodedName.toString) case Select(This(_), name: TermName) => Some(name.decodedName.toString) case _ => None } - val filtered: Seq[(String /* part */, String /* arg name */, c.Expr[Any] /* value */)] = + // Match the %(x) explicit variable name specifications in parts and get final variable names + val filtered: Seq[(String /* part */, String /* arg name */, c.Tree /* value */)] = for { - idx <- 0 until args.size + idx <- args.indices argName = inferredNames(idx) part = parts(idx + 1) } yield { @@ -68,116 +136,160 @@ object Macros { (filtered, name, args(idx)) } else { if (argName.isEmpty) - c.abort(c.enclosingPosition, s"No name is defined for part #$idx (${Compat.prettyPrint(c)(args(idx).tree)})") + c.abort(c.enclosingPosition, s"No name is defined for part #$idx (${Compat.prettyPrint(c)(args(idx))})") (part, argName.get, args(idx)) } } - for ((name, vals) <- filtered.groupBy(_._2) if vals.exists(_ != vals.head)) - c.abort(c.enclosingPosition, s"Duplicate variable name: $name") - - val msgid = parts.head + filtered.map { + (parts.head + filtered.map { case (part, name, _) => s"%($name)$part" - }.mkString - - val tr = filtered.groupBy(_._2).mapValues(_.head._3.tree) - - generateSingular[T](c)(None, msgid, tr)(lang, outputFormat).asInstanceOf[c.Expr[T]] + }.mkString, filtered.map { + case (_, name, value) => q"($name, $value)" + }) } /** - * 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. + * A generic function to generate interpolation results. Other macros do nothing but call it. + * + * @param c Macro context + * @param ctxTree Optional tree with context argument + * @param msgTree Message argument + * @param pluralTree Optional with (plural message, n) arguments + * @param argsTree Supplied args as a trees + * @param lang Language tree that if present means instant evaluation + * @param outputFormat Tree representing `OutputFormat[T]` instance + * @return Tree representing an instance of `T` if language was present, or `LValue[T]` if + * language was absent. */ - 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] = + private def generate[T: c.WeakTypeTag](c: Context) + (ctxTree: Option[c.Tree], msgTree: c.Tree, pluralTree: Option[(c.Tree, c.Tree)], argsTree: Seq[c.Tree]) + (lang: Option[c.Tree], outputFormat: c.Tree): c.Tree = { + import c.universe._ - val (msgid, vars) = verifyVariables(c)(msg, args, None) + // Extract literals: + val ctx = ctxTree.map(stringLiteral(c)) + val msg = stringLiteral(c)(msgTree) + val plural = pluralTree.map { case (s, n) => stringLiteral(c)(s) -> n } + val args = argsTree.map(tupleLiteral(c)(_)) ++ (plural match { + case Some((_, n)) => Seq("n" -> n) + case None => Nil + }) + + // Call message extractor: + plural match { + case None => MessageExtractor.singular(c)(ctx, msg) + case Some((pl, _)) => MessageExtractor.plural(c)(ctx, msg, pl) + } - generateSingular[T](c)(None, msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] - } + // Verify variables consistency: + def verifyVariables(s: String): Unit = { + val varsArg = args.map(_._1).toSet + val varsStr = StringUtils.extractVariables(s).toSet - /** - * 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] = - { + for (v <- (varsArg diff varsStr) ++ (varsStr diff varsArg)) + if (varsArg.contains(v)) + c.abort(c.enclosingPosition, s"variable `$v` is not present in interpolation string") + else + c.abort(c.enclosingPosition, s"variable `$v` is not present at arguments section") + } - val ctxStr = stringLiteral(c)(ctx.tree) - val (msgid, vars) = verifyVariables(c)(msg, args, None) + for ((v, xs) <- args.groupBy(_._1) if xs.length > 1) + c.abort(c.enclosingPosition, s"duplicate variable `$v`") - generateSingular[T](c)(Some(ctxStr), msgid, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] - } + verifyVariables(msg) + for ((pl, _) <- plural) + verifyVariables(pl) - /** - * 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] = - { + /** + * Given a language tree `lng`, creates a tree that will translate given message. + */ + def translate(lng: c.Tree): c.Tree = { + val str = plural match { + case None => q"$lng.singular(..${ctx.toSeq}, $msg)" + case Some((pl, n)) => q"$lng.plural(..${ctx.toSeq}, $msg, $pl, $n)" + } - val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) - val (msgidPlural, _) = verifyVariables(c)(msgPlural, args, Some(n)) + if (args.isEmpty) + q"$outputFormat.convert($str)" + else { + val argsT = args.map { case (k, v) => q"$k -> $v" } + q"_root_.ru.makkarpov.scalingua.StringUtils.interpolate[${weakTypeOf[T]}]($str, ..$argsT)" + } + } - generatePlural[T](c)(None, msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] + lang match { + case Some(lng) => translate(lng) + case None => + val name = termName(c)("lng") + q""" + new _root_.ru.makkarpov.scalingua.LValue( + ($name: _root_.ru.makkarpov.scalingua.Language) => ${translate(q"$name")} + ) + """ + } } /** - * 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. + * Matches string against string literal pattern and return literal string if matched. Currently supported + * literal types: + * + * 1) Plain literals like `"123"` + * 2) Strip margin literals like `""" ... """.stripMargin` + * 3) Strip margin literals with custom margin character like `""" ... """.stripMargin('#')` + * + * @param c Macro context + * @param e Tree to match + * @return Extracted string literal */ - 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.tree) - val (msgid, vars) = verifyVariables(c)(msg, args, Some(n)) - val (msgidPlural, _) = verifyVariables(c)(msgPlural, args, Some(n)) - - generatePlural[T](c)(Some(ctxStr), msgid, msgidPlural, n, vars)(lang, outputFormat).asInstanceOf[c.Expr[T]] - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - 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.tree) - val vars = StringUtils.extractVariables(msgStr) + private def stringLiteral(c: Context)(e: c.Tree): String = { + import c.universe._ - var exprs = args.map(tupleLiteral(c)(_)) + e match { + case Literal(Constant(s: String)) => s - for (nx <- n) - exprs :+= "n" -> nx + case q"scala.this.Predef.augmentString($str).stripMargin" => + str match { + case Literal(Constant(s: String)) => s.stripMargin.trim + case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") + } - // Test uniqueness of variables: - for ((v, _) <- exprs.groupBy(_._1).filter(_._2.size > 1)) - c.abort(c.enclosingPosition, s"Duplicate variable `$v`") + case q"scala.this.Predef.augmentString($str).stripMargin($ch)" => + (str, ch) match { + case (Literal(Constant(s: String)), Literal(Constant(c: Char))) => s.stripMargin(c).trim + case (Literal(Constant(s: String)), _) => + c.abort(c.enclosingPosition, s"Expected character literal, got instead ${prettyPrint(c)(ch)}") + case _ => c.abort(c.enclosingPosition, s"Expected string literal, got instead ${prettyPrint(c)(str)}") + } - // Test difference of variables + case _ => + c.abort(c.enclosingPosition, s"Expected string literal or multi-line string, got instead ${prettyPrint(c)(e)}") + } + } - val argVars = exprs.map(_._1).toSet + /** + * Matches string against tuple `(String, T)` pattern and returns extracted string literal and tuple value. + * Currently supported literal types: + * + * 1) Plain literals like `("1", x)` + * 2) ArrowAssoc literals like `"1" -> x` + * + * @param c Macro context + * @param e Tree to match + * @return Extracted tuple literal parts + */ + private def tupleLiteral(c: Context)(e: c.Tree): (String, c.Tree) = { + import c.universe._ - for (n <- (vars diff argVars) ++ (argVars diff vars)) - if (vars.contains(n)) - c.abort(c.enclosingPosition, s"Variable `$n` is not present at argument list") - else - c.abort(c.enclosingPosition, s"Variable `$n` is not present at interpolation string") + val (a, b) = e match { + case q"scala.this.Predef.ArrowAssoc[$aType]($ax).->[$bType]($bx)" => (ax, bx) // 2.11 + case q"scala.this.Predef.any2ArrowAssoc[$aType]($ax).->[$bType]($bx)" => (ax, bx) // 2.10 + case q"($ax, $bx)" => (ax, bx) + case _ => + c.abort(c.enclosingPosition, s"Expected tuple definition `x -> y` or `(x, y)`, got instead ${prettyPrint(c)(e)}") + } - (msgStr, Map(exprs:_*).mapValues(_.tree).asInstanceOf[Map[String, c.Tree]]) + (stringLiteral(c)(a), b) } } diff --git a/scalingua/src/test/scala/ru/makkarpov/scalingua/test/IStringTest.scala b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/IStringTest.scala new file mode 100644 index 0000000..1dd2f4d --- /dev/null +++ b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/IStringTest.scala @@ -0,0 +1,53 @@ +/****************************************************************************** + * Copyright © 2016 Maxim Karpov * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ******************************************************************************/ + +package ru.makkarpov.scalingua.test + +import org.scalatest.{FlatSpec, Matchers} +import ru.makkarpov.scalingua.I18n._ +import ru.makkarpov.scalingua.{Language, LanguageId} + +class IStringTest extends FlatSpec with Matchers { + class MockLang(s: String) extends Language { + override def id: LanguageId = LanguageId("mock", "p" + s) + override def singular(msgid: String): String = s"$s:$msgid" + override def singular(msgctx: String, msgid: String): String = s"$s:$msgctx:$msgid" + override def plural(msgid: String, msgidPlural: String, n: Long): String = s"$s:$msgid:$msgidPlural:$n" + override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = + s"$s:$msgctx:$msgid:$msgidPlural:$n" + } + + val mockLang1 = new MockLang("1") + val mockLang2 = new MockLang("2") + val mockLang3 = new MockLang("3") + + it should "handle internationalized strings when surrounding implicit lang is not present" in { + val t = lt"Hello, world!" + + t.resolve(mockLang1) shouldBe "1:Hello, world!" + t.resolve(mockLang2) shouldBe "2:Hello, world!" + } + + it should "handle internationalized strings when implicit lang is present" in { + implicit val lang = mockLang3 + + val t = lt"12345" + + t.resolve(mockLang1) shouldBe "1:12345" + t.resolve(mockLang2) shouldBe "2:12345" + t.resolve shouldBe "3:12345" + } +} diff --git a/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala index a814da0..539dee1 100644 --- a/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala +++ b/scalingua/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala @@ -141,5 +141,10 @@ class MacroTest extends FlatSpec with Matchers { t("""1 |2 """.stripMargin('#')) shouldBe "{s:1\n |2}" + + t("1".stripMargin('1')) shouldBe "{s:}" + + """ val c = '1'; t("1".stripMargin(c)) """ shouldNot compile + """ val s = "1"; t(s.stripMargin('1')) """ shouldNot compile } }