├── .gitignore ├── .travis.yml ├── LICENSE ├── build.sbt ├── core └── src │ └── main │ └── scala │ └── ru │ └── makkarpov │ └── scalingua │ ├── CompiledLanguage.scala │ ├── LValue.scala │ ├── Language.scala │ ├── LanguageId.scala │ ├── MergedLanguage.scala │ ├── Messages.scala │ ├── OutputFormat.scala │ ├── PluralFunction.scala │ ├── StringUtils.scala │ └── TaggedLanguage.scala ├── play └── src │ ├── main │ └── scala │ │ └── ru │ │ └── makkarpov │ │ └── scalingua │ │ └── play │ │ ├── I18n.scala │ │ ├── MessagesProvider.scala │ │ ├── PlayUtils.scala │ │ ├── ScalinguaConfig.scala │ │ ├── ScalinguaConfigProvider.scala │ │ └── ScalinguaPlugin.scala │ └── test │ └── scala │ └── ru │ └── makkarpov │ └── scalingua │ └── play │ └── test │ ├── MockEnglishLang.scala │ └── PlayTest.scala ├── project ├── ParserGenerator.scala ├── build.properties ├── plugins.sbt ├── scripted.sbt └── skeleton.jflex ├── readme.md ├── sbt-plugin └── src │ ├── main │ └── scala │ │ └── ru │ │ └── makkarpov │ │ └── scalingua │ │ └── plugin │ │ ├── GenerationContext.scala │ │ ├── ParseFailedException.scala │ │ ├── PoCompiler.scala │ │ ├── PoCompilerStrategy.scala │ │ └── Scalingua.scala │ └── sbt-test │ └── main │ ├── fail-on-invalid-locales │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ └── locales │ │ │ └── invalid.po │ └── test │ ├── implicit-context │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ ├── locales │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── Test.scala │ └── test │ ├── inline-scala-js │ ├── build.sbt │ ├── messages.pot │ ├── project │ │ ├── build.properties │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ ├── locales │ │ │ ├── pl_PL.po │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── Test.scala │ ├── test │ └── verify-pot.sh │ ├── load-in-runtime │ ├── build.sbt │ ├── messages.pot │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ ├── locales │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── Test.scala │ ├── test │ └── verify-pot.sh │ ├── merging │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ └── scala │ │ │ └── Test.scala │ ├── subA │ │ └── src │ │ │ └── main │ │ │ ├── locales │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── SubA.scala │ ├── subB │ │ └── src │ │ │ └── main │ │ │ ├── locales │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── SubB.scala │ └── test │ ├── multilang │ ├── build.sbt │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ ├── locales │ │ │ ├── de_DE.po │ │ │ ├── ru_RU.po │ │ │ └── zh_ZH.po │ │ │ └── scala │ │ │ └── Test.scala │ └── test │ ├── no-escape-unicode │ ├── build.sbt │ ├── messages.pot │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ └── scala │ │ │ └── Test.scala │ ├── test │ └── verify-pot.sh │ ├── simple-lang │ ├── build.sbt │ ├── messages.pot │ ├── project │ │ └── plugins.sbt │ ├── src │ │ └── test │ │ │ ├── locales │ │ │ └── ru_RU.po │ │ │ └── scala │ │ │ └── Test.scala │ ├── test │ └── verify-pot.sh │ └── tagged-strings │ ├── build.sbt │ ├── messages.pot │ ├── project │ └── plugins.sbt │ ├── src │ └── test │ │ ├── locales │ │ └── ru_RU.po │ │ └── scala │ │ └── Test.scala │ ├── tagged.json │ ├── test │ └── verify-pot.sh ├── scalingua ├── jvm │ └── src │ │ └── test │ │ └── scala │ │ └── ru │ │ └── makkarpov │ │ └── scalingua │ │ └── test │ │ └── PoFileTest.scala └── shared │ └── src │ ├── main │ ├── pofile │ │ ├── pofile.cup │ │ └── pofile.flex │ ├── scala-2.12 │ │ └── ru │ │ │ └── makkarpov │ │ │ └── scalingua │ │ │ └── Compat.scala │ ├── scala-2.13 │ │ └── ru │ │ │ └── makkarpov │ │ │ └── scalingua │ │ │ └── Compat.scala │ └── scala │ │ └── ru │ │ └── makkarpov │ │ └── scalingua │ │ ├── I18n.scala │ │ ├── InsertableIterator.scala │ │ ├── Macros.scala │ │ ├── extract │ │ ├── ExtractorSession.scala │ │ ├── ExtractorSettings.scala │ │ ├── MessageExtractor.scala │ │ ├── TaggedParseException.scala │ │ └── TaggedParser.scala │ │ ├── plural │ │ ├── Expression.scala │ │ ├── ParsedPlural.scala │ │ ├── Parser.scala │ │ └── Suffix.scala │ │ └── pofile │ │ ├── Message.scala │ │ ├── MessageFlag.scala │ │ ├── MessageHeader.scala │ │ ├── MessageLocation.scala │ │ ├── MultipartString.scala │ │ ├── NewLinePrintWriter.scala │ │ ├── ParserTest.scala │ │ ├── PoFile.scala │ │ └── parse │ │ ├── Comment.scala │ │ ├── ErrorReportingParser.scala │ │ ├── LexerException.scala │ │ ├── MutableHeader.scala │ │ ├── MutableMultipartString.scala │ │ ├── MutablePlurals.scala │ │ ├── ParseUtils.scala │ │ └── ParserException.scala │ └── test │ └── scala │ └── ru │ └── makkarpov │ └── scalingua │ └── test │ ├── CustomI18nTest.scala │ ├── IStringTest.scala │ ├── LVInterpolationTest.scala │ ├── MacroTest.scala │ ├── MockLang.scala │ ├── ParserTest.scala │ └── StringUtilsTest.scala └── twirl └── shared └── src ├── main └── scala │ └── ru │ └── makkarpov │ └── scalingua │ └── twirl │ ├── I18n.scala │ └── PlayUtils.scala └── test └── scala └── ru └── makkarpov └── scalingua └── twirl └── test └── PlayTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | core/target 2 | sbt-plugin/target 3 | scalingua/target 4 | project/target 5 | 6 | ### Scala ### 7 | *.class 8 | *.log 9 | 10 | # sbt specific 11 | .cache 12 | .history 13 | .lib/ 14 | dist/* 15 | target/ 16 | lib_managed/ 17 | src_managed/ 18 | project/boot/ 19 | project/plugins/project/ 20 | 21 | # Scala-IDE specific 22 | .scala_dependencies 23 | .worksheet 24 | 25 | 26 | ### SBT ### 27 | # Simple Build Tool 28 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 29 | 30 | target/ 31 | lib_managed/ 32 | src_managed/ 33 | project/boot/ 34 | .history 35 | .cache 36 | 37 | 38 | ### Linux ### 39 | *~ 40 | 41 | # temporary files which can be created if a process still has a handle open of a deleted file 42 | .fuse_hidden* 43 | 44 | # KDE directory preferences 45 | .directory 46 | 47 | # Linux trash folder which might appear on any partition or disk 48 | .Trash-* 49 | 50 | 51 | ### Intellij ### 52 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 53 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 54 | 55 | .idea 56 | *.iml 57 | *.iws 58 | *.ipr 59 | 60 | ## Plugin-specific files: 61 | 62 | # IntelliJ 63 | /out/ 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | ### Windows ### 78 | # Windows image file caches 79 | Thumbs.db 80 | ehthumbs.db 81 | 82 | # Folder config file 83 | Desktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msm 92 | *.msp 93 | 94 | # Windows shortcuts 95 | *.lnk 96 | 97 | 98 | ### OSX ### 99 | .DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Icon must end with two \r 104 | Icon 105 | 106 | 107 | # Thumbnails 108 | ._* 109 | 110 | # Files that might appear in the root of a volume 111 | .DocumentRevisions-V100 112 | .fseventsd 113 | .Spotlight-V100 114 | .TemporaryItems 115 | .Trashes 116 | .VolumeIcon.icns 117 | 118 | # Directories potentially created on remote AFP share 119 | .AppleDB 120 | .AppleDesktop 121 | Network Trash Folder 122 | Temporary Items 123 | .apdisk -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | dist: xenial 4 | jdk: openjdk8 5 | 6 | script: 7 | - sbt +test scripted 8 | 9 | before_cache: 10 | - rm -fv $HOME/.ivy2/.sbt.ivy.lock 11 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete 12 | - find $HOME/.sbt -name "*.lock" -print -delete 13 | 14 | cache: 15 | directories: 16 | - $HOME/.sbt 17 | - $HOME/.cache/coursier 18 | - $HOME/.ivy2/cache -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | name := "scalingua-root" 4 | version := "1.2.0-SNAPSHOT" 5 | crossPaths := true 6 | 7 | publishArtifact := false 8 | publishTo := Some(Resolver.file("Transient repository", file("/tmp/unused"))) 9 | 10 | val common = Seq( 11 | organization := "ru.makkarpov", 12 | version := (LocalRootProject / version).value, 13 | 14 | crossPaths := true, 15 | scalaVersion := "2.12.18", //should be the same for all projects for cross-build to work 16 | crossScalaVersions := Seq(scalaVersion.value, "2.13.12"), //no support fo scala3 macros yet 17 | javacOptions ++= Seq( "-source", "1.8", "-target", "1.8" ), 18 | scalacOptions ++= Seq( "-Xfatal-warnings", "-feature", "-deprecation" ), 19 | 20 | libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.17" % Test, 21 | 22 | Test / publishArtifact := false, 23 | Test / envVars := Map("SCALANATIVE_MIN_SIZE"-> "100m", "SCALANATIVE_MAX_SIZE"-> "100m"), 24 | publishMavenStyle := true, 25 | 26 | licenses := Seq("Apache 2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")), 27 | homepage := Some(url("https://github.com/makkarpov/scalingua")), 28 | organizationName := "Maxim Karpov", 29 | organizationHomepage := Some(url("https://github.com/makkarpov")), 30 | scmInfo := Some(ScmInfo( 31 | browseUrl = url("https://github.com/makkarpov/scalingua"), 32 | connection = "scm:git://github.com/makkarpov/scalingua.git" 33 | )), 34 | 35 | // Seems that SBT key `developers` is producing incorrect results 36 | pomExtra := { 37 | 38 | 39 | makkarpov 40 | Maxim Karpov 41 | https://github.com/makkarpov 42 | 43 | 44 | }, 45 | 46 | publishTo := { 47 | val nexus = "https://oss.sonatype.org/" 48 | if (isSnapshot.value) 49 | Some("snapshots" at nexus + "content/repositories/snapshots") 50 | else 51 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 52 | }, 53 | // double publishes sbt artifact for some reason 54 | publishConfiguration := publishConfiguration.value.withOverwrite(true) 55 | ) 56 | 57 | lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) 58 | .crossType(CrossType.Pure) 59 | .settings(common) 60 | .settings( 61 | name := "Scalingua Core", 62 | normalizedName := "scalingua-core", 63 | description := "A minimal set of runtime classes for Scalingua", 64 | 65 | libraryDependencies += "org.portable-scala" %%% "portable-scala-reflect" % "1.1.2" 66 | ) 67 | 68 | lazy val scalingua = crossProject(JSPlatform, JVMPlatform, NativePlatform) 69 | .crossType(CrossType.Full) 70 | .enablePlugins(ParserGenerator, AssemblyPlugin) 71 | .settings(common) 72 | .settings( 73 | name := "Scalingua", 74 | normalizedName := "scalingua", 75 | description := "A simple gettext-like internationalization library for Scala", 76 | 77 | libraryDependencies ++= Seq( 78 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 79 | "com.github.vbmacher" % "java-cup-runtime" % "11b-20160615", 80 | "com.grack" % "nanojson" % "1.2" 81 | ), 82 | 83 | assembly / assemblyShadeRules := Seq( 84 | ShadeRule.rename("java_cup.runtime.**" -> "ru.makkarpov.scalingua.pofile.shaded_javacup.@1").inAll 85 | ), 86 | 87 | // include only CUP: 88 | assembly / assemblyExcludedJars := (assembly / fullClasspath).value.filterNot { f => 89 | f.data.getName.contains("java-cup-runtime") 90 | } 91 | ) 92 | .dependsOn(core) 93 | 94 | lazy val scalingua_shadedCup = project.in(file("target/shaded-cup")) 95 | .settings(common) 96 | .settings( 97 | name := "Scalingua shaded", 98 | normalizedName := "scalingua-shaded", 99 | description := "Scalingua with shaded CUP runtime to prevent conflicts", 100 | 101 | Compile / packageBin := (scalingua.jvm / Compile / assembly).value, 102 | libraryDependencies := (scalingua.jvm / libraryDependencies).value.filterNot(_.name.contains("java-cup")) 103 | ) 104 | .dependsOn(scalingua.jvm.dependencies:_*) 105 | 106 | def playSettings = List( 107 | // Recent versions of Play supports only recent version of Scala 108 | //todo support scala3 109 | crossScalaVersions := Seq("2.13.12"), 110 | //workaround having to use same scalaversion for all projects for crossbuild to work 111 | libraryDependencies := (if (scalaVersion.value.startsWith("2.12")) Nil else libraryDependencies.value), 112 | (Compile / publishArtifact) := (if (scalaVersion.value.startsWith("2.12")) false else (Compile / publishArtifact).value), 113 | (Compile / sources) := (if (scalaVersion.value.startsWith("2.12")) Nil else (Compile / sources).value), 114 | (Test / sources) := (if (scalaVersion.value.startsWith("2.12")) Nil else (Test / sources).value), 115 | (Test / loadedTestFrameworks) := (if (scalaVersion.value.startsWith("2.12")) Map() else (Test / loadedTestFrameworks).value), 116 | ) 117 | 118 | lazy val twirl = crossProject(JSPlatform, JVMPlatform) 119 | .crossType(CrossType.Full) 120 | .settings(common) 121 | .settings( 122 | name := "Scalingua Twirl module", 123 | normalizedName := "scalingua-twirl", 124 | description := "An integration module for Twirl", 125 | libraryDependencies ++= Seq( 126 | "org.playframework.twirl" %%% "twirl-api" % "2.0.1", 127 | ), 128 | ) 129 | .settings(playSettings) 130 | .dependsOn(scalingua) 131 | 132 | lazy val play = project 133 | .settings(common) 134 | .settings( 135 | name := "Scalingua Play module", 136 | normalizedName := "scalingua-play", 137 | description := "An integration module for Play Framework", 138 | libraryDependencies ++= Seq( 139 | "org.playframework" %% "play" % "3.0.0" 140 | ), 141 | ) 142 | .settings(playSettings) 143 | .dependsOn(twirl.jvm) 144 | 145 | lazy val plugin = project 146 | .in(file("sbt-plugin")) 147 | .enablePlugins(SbtPlugin) 148 | .settings(common) 149 | .settings( 150 | name := "Scalingua SBT plugin", 151 | normalizedName := "scalingua-sbt", 152 | description := "SBT plugin that compiles locales, manages locations of *.pot files and so on", 153 | 154 | crossPaths := false, 155 | crossScalaVersions := Seq(scalaVersion.value), //sbt only runs on 2.12 156 | 157 | scriptedLaunchOpts ++= Seq( 158 | "-Xmx1024M", "-XX:MaxPermSize=256M", "-Dscalingua.version=" + (LocalRootProject / version).value 159 | ), 160 | scriptedBufferLog := false, 161 | scripted := scripted.dependsOn( 162 | scalingua.jvm / publishLocal, 163 | core.jvm / publishLocal, 164 | scalingua.js / publishLocal, 165 | core.js / publishLocal).evaluated, 166 | pluginCrossBuild / sbtVersion := "1.2.8", //https://github.com/sbt/sbt/issues/5049 167 | ).dependsOn(scalingua.jvm) 168 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/CompiledLanguage.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import java.io.{DataInputStream, IOException, InputStream} 20 | 21 | import ru.makkarpov.scalingua.MergedLanguage.MessageData 22 | 23 | object CompiledLanguage { 24 | class EnglishTags extends TaggedLanguage { 25 | private var singularTag: Map[String, String] = _ 26 | private var pluralTag: Map[String, (String, String)] = _ 27 | 28 | protected def initialize(is: InputStream): Unit = { 29 | if (is == null) 30 | throw new NullPointerException("inputStream") 31 | 32 | val singTag = Map.newBuilder[String, String] 33 | val plurTag = Map.newBuilder[String, (String, String)] 34 | 35 | val dis = new DataInputStream(is) 36 | var flag = true 37 | 38 | try { 39 | dis.readUTF() 40 | 41 | while (flag) 42 | dis.readByte() match { 43 | case 0 => flag = false 44 | 45 | case 5 => 46 | val id = dis.readUTF() 47 | val msg = dis.readUTF() 48 | singTag += id -> msg 49 | 50 | case 6 => 51 | val id = dis.readUTF() 52 | val msgSing = dis.readUTF() 53 | val msgPlur = dis.readUTF() 54 | plurTag += id -> (msgSing, msgPlur) 55 | } 56 | } finally dis.close() 57 | 58 | singularTag = singTag.result() 59 | pluralTag = plurTag.result() 60 | } 61 | 62 | /** @inheritdoc */ 63 | override def taggedSingular(tag: String): String = singularTag.getOrElse(tag, tag) 64 | 65 | /** @inheritdoc */ 66 | override def taggedPlural(tag: String, n: Long): String = 67 | pluralTag.get(tag) match { 68 | case Some((sing, plur)) => if (n != 1) plur else sing 69 | case None => tag 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * A compiled representation of .po file that requires much less code to parse. We cannot embed all strings 76 | * in Scala file since Java has limit on constant pool (65k) and on length of single string (also 65k). 77 | * So we extract them to separate file and keep in Scala class only compiled plural function. 78 | * 79 | * This class is just a stub, actual implementation will be generated by SBT plugin. 80 | */ 81 | abstract class CompiledLanguage extends Language with PluralFunction with TaggedLanguage { 82 | // We cannot pass input stream as constructor parameter here, since we need to read resource 83 | // from same ClassLoader that loaded `Language_xx_XX` instance, but we cannot use `getClass` in 84 | // constructor parameter. So we delay the initializaiton of Maps until getClass will be available 85 | 86 | private var _id: LanguageId = _ 87 | private var singular: Map[String, String] = _ 88 | private var singularCtx: Map[(String, String), String] = _ 89 | private var plural: Map[String, Seq[String]] = _ 90 | private var pluralCtx: Map[(String, String), Seq[String]] = _ 91 | private var singularTag: Map[String, String] = _ 92 | private var pluralTag: Map[String, Seq[String]] = _ 93 | 94 | protected def initialize(is: InputStream): Unit = { 95 | if (is == null) 96 | throw new NullPointerException("inputStream") 97 | 98 | val sing = Map.newBuilder[String, String] 99 | val plur = Map.newBuilder[String, Seq[String]] 100 | val singCtx = Map.newBuilder[(String, String), String] 101 | val plurCtx = Map.newBuilder[(String, String), Seq[String]] 102 | val singTag = Map.newBuilder[String, String] 103 | val plurTag = Map.newBuilder[String, Seq[String]] 104 | 105 | val dis = new DataInputStream(is) 106 | var flag = true 107 | 108 | try { 109 | dis.readUTF() // Source hash for caching purposes 110 | 111 | _id = LanguageId(dis.readUTF(), dis.readUTF()) 112 | 113 | def readPlurals: Seq[String] = 114 | (0 until dis.readUnsignedByte()) map (_ => dis.readUTF()) 115 | 116 | while (flag) dis.readByte() match { 117 | case 0 => flag = false 118 | 119 | case 1 => 120 | val id = dis.readUTF() 121 | sing += id -> dis.readUTF() 122 | 123 | case 2 => 124 | val ctx = dis.readUTF() 125 | val id = dis.readUTF() 126 | singCtx += (ctx, id) -> dis.readUTF() 127 | 128 | case 3 => 129 | val id = dis.readUTF() 130 | plur += id -> readPlurals 131 | 132 | case 4 => 133 | val ctx = dis.readUTF() 134 | val id = dis.readUTF() 135 | plurCtx += (ctx, id) -> readPlurals 136 | 137 | case 5 => 138 | val tag = dis.readUTF() 139 | val msg = dis.readUTF() 140 | singTag += tag -> msg 141 | 142 | case 6 => 143 | val tag = dis.readUTF() 144 | val msg = readPlurals 145 | plurTag += tag -> msg 146 | } 147 | } finally dis.close() 148 | 149 | singular = sing.result() 150 | plural = plur.result() 151 | singularCtx = singCtx.result() 152 | pluralCtx = plurCtx.result() 153 | singularTag = singTag.result() 154 | pluralTag = plurTag.result() 155 | } 156 | 157 | override def id: LanguageId = _id 158 | 159 | def messageData = MessageData(singular, singularCtx, plural, pluralCtx) 160 | 161 | override def singular(msgid: String): String = singular.getOrElse(msgid, msgid) 162 | 163 | override def singular(msgctx: String, msgid: String): String = singularCtx.getOrElse(msgctx -> msgid, msgid) 164 | 165 | override def plural(msgid: String, msgidPlural: String, n: Long): String = plural.get(msgid) match { 166 | case Some(tr) => tr(plural(n)) 167 | case None => if (n == 1) msgid else msgidPlural 168 | } 169 | 170 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = 171 | pluralCtx.get(msgctx -> msgid) match { 172 | case Some(tr) => tr(plural(n)) 173 | case None => if (n == 1) msgid else msgidPlural 174 | } 175 | 176 | override def taggedSingular(tag: String): String = 177 | if (singularTag.contains(tag)) singularTag(tag) else taggedFallback.taggedSingular(tag) 178 | 179 | override def taggedPlural(tag: String, n: Long): String = 180 | if (pluralTag.contains(tag)) pluralTag(tag)(plural(n)) else taggedFallback.taggedPlural(tag, n) 181 | 182 | override def merge(other: Language): Language = other match { 183 | case ml: MergedLanguage => new MergedLanguage(id, messageData.merge(ml.data), this, this) 184 | case cc: CompiledLanguage => new MergedLanguage(id, messageData.merge(cc.messageData), this, this) 185 | case _: Language.English => other 186 | case _ => throw new NotImplementedError("Merge is supported only for MergedLanguage and CompiledLanguage") 187 | } 188 | 189 | def taggedFallback: TaggedLanguage 190 | } 191 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/LValue.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import scala.language.implicitConversions 20 | 21 | object LValue { 22 | implicit def unwrapLvalue[T](lValue: LValue[T])(implicit lang: Language): T = lValue.resolve 23 | } 24 | 25 | /** 26 | * Value that can be translated lazily (e.g. for definitions whose target language is not known a priori) 27 | */ 28 | class LValue[+T](func: Language => T) extends (Language => T) { 29 | def apply(lang: Language): T = func(lang) 30 | def resolve(implicit lang: Language) = func(lang) 31 | def genericResolve = func(Language.English) 32 | override def toString: String = s"LValue($genericResolve)" 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/Language.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | object Language { 20 | /** 21 | * Implicit conversion to derive language from available `LanguageId` and `Messages` 22 | */ 23 | @inline 24 | implicit def $providedLanguage(implicit msg: Messages, lang: LanguageId): Language = msg.apply(lang) 25 | 26 | /** 27 | * A fallback English language that always returns the same message strings. 28 | */ 29 | class English(tags: TaggedLanguage) extends Language { 30 | override def id = LanguageId("en", "US") 31 | 32 | override def singular(msgid: String): String = msgid 33 | override def singular(msgctx: String, msgid: String): String = msgid 34 | override def plural(msgid: String, msgidPlural: String, n: Long): String = if (n != 1) msgidPlural else msgid 35 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = 36 | if (n != 1) msgidPlural else msgid 37 | 38 | override def taggedSingular(tag: String): String = tags.taggedSingular(tag) 39 | override def taggedPlural(tag: String, n: Long): String = tags.taggedPlural(tag, n) 40 | 41 | override def merge(other: Language): Language = other 42 | } 43 | 44 | val English = new English(TaggedLanguage.Identity) 45 | } 46 | 47 | /** 48 | * Base trait for objects reprensenting languages. 49 | */ 50 | trait Language extends TaggedLanguage { 51 | /** 52 | * A exact (with country part) ID of this language. 53 | */ 54 | def id: LanguageId 55 | 56 | /** 57 | * Resolve singular form of message without a context. 58 | * 59 | * @param msgid A message to resolve 60 | * @return Resolved message or `msgid` itself. 61 | */ 62 | def singular(msgid: String): String 63 | 64 | /** 65 | * Resolve singular form of message with a context. 66 | * 67 | * @param msgctx A context of message 68 | * @param msgid A message to resolve 69 | * @return Resolved message or `msgid` itself. 70 | */ 71 | def singular(msgctx: String, msgid: String): String 72 | 73 | /** 74 | * Resolve plural form of message without a context 75 | * 76 | * @param msgid A singular form of message 77 | * @param msgidPlural A plural form of message 78 | * @param n Numeral representing which form to choose 79 | * @return Resolved plural form of message 80 | */ 81 | def plural(msgid: String, msgidPlural: String, n: Long): String 82 | 83 | /** 84 | * Resolve plural form of message with a context. 85 | * 86 | * @param msgctx A context of message 87 | * @param msgid A singular form of message 88 | * @param msgidPlural A plural form of message 89 | * @param n Numeral representing which form to choose 90 | * @return Resolved plural form of message 91 | */ 92 | def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String 93 | 94 | /** 95 | * Merges this language with specified `other`. Conflicting messages will be resolved 96 | * using `this` language. Plural function will be used from `this` language. 97 | * 98 | * @param other Language to merge 99 | * @return Merged language 100 | */ 101 | def merge(other: Language): Language 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/LanguageId.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | object LanguageId { 20 | private val languagePattern = "([a-zA-Z]{2,3})(?:[_-]([a-zA-Z]{2,3}))?".r 21 | 22 | /** 23 | * Creates `LanguageId` instance from language codes like `en` or `en-US` 24 | * 25 | * @param s Language code 26 | * @return `Some` with `LanguageId` instance if language code was parsed successfully, or `None` otherwise. 27 | */ 28 | def get(s: String): Option[LanguageId] = s match { 29 | case languagePattern(lang, country) => 30 | Some(LanguageId(lang.toLowerCase, if (country eq null) "" else country.toUpperCase)) 31 | case _ => None 32 | } 33 | 34 | /** 35 | * Creates `LanguageId` instance from language codes like `en` or `en-US` 36 | * 37 | * @param s Language code 38 | * @return `LanguageId` instance 39 | */ 40 | def apply(s: String): LanguageId = get(s).getOrElse(throw new IllegalArgumentException(s"Unrecognized language '$s'")) 41 | } 42 | 43 | /** 44 | * Class representing a pair of language and country (e.g. `en_US`) 45 | * 46 | * @param language ISO code of language 47 | * @param country ISO code of country, may be empty for generic languages. 48 | */ 49 | final case class LanguageId(language: String, country: String) { 50 | /** 51 | * @return Whether the country part is present 52 | */ 53 | @inline def hasCountry = country.nonEmpty 54 | 55 | override def toString: String = language + (if (hasCountry) "-" else "") + country 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/MergedLanguage.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua 2 | 3 | import ru.makkarpov.scalingua.MergedLanguage.MessageData 4 | 5 | /** 6 | * Created by makkarpov on 11.06.17. 7 | */ 8 | object MergedLanguage { 9 | case class MessageData(singular: Map[String, String], singularCtx: Map[(String, String), String], 10 | plural: Map[String, Seq[String]], pluralCtx: Map[(String, String), Seq[String]]) { 11 | 12 | def merge(other: MessageData): MessageData = 13 | MessageData(other.singular ++ singular, other.singularCtx ++ singularCtx, 14 | other.plural ++ plural, other.pluralCtx ++ pluralCtx) 15 | } 16 | } 17 | 18 | class MergedLanguage(val id: LanguageId, val data: MessageData, plural: PluralFunction, tags: TaggedLanguage) extends Language { 19 | /** 20 | * Resolve singular form of message without a context. 21 | * 22 | * @param msgid A message to resolve 23 | * @return Resolved message or `msgid` itself. 24 | */ 25 | override def singular(msgid: String): String = data.singular.getOrElse(msgid, msgid) 26 | 27 | /** 28 | * Resolve singular form of message with a context. 29 | * 30 | * @param msgctx A context of message 31 | * @param msgid A message to resolve 32 | * @return Resolved message or `msgid` itself. 33 | */ 34 | override def singular(msgctx: String, msgid: String): String = data.singularCtx.getOrElse(msgctx -> msgid, msgid) 35 | 36 | /** 37 | * Resolve plural form of message without a context 38 | * 39 | * @param msgid A singular form of message 40 | * @param msgidPlural A plural form of message 41 | * @param n Numeral representing which form to choose 42 | * @return Resolved plural form of message 43 | */ 44 | override def plural(msgid: String, msgidPlural: String, n: Long): String = data.plural.get(msgid) match { 45 | case Some(tr) => tr(plural.plural(n)) 46 | case None => if (n == 1) msgid else msgidPlural 47 | } 48 | 49 | /** 50 | * Resolve plural form of message with a context. 51 | * 52 | * @param msgctx A context of message 53 | * @param msgid A singular form of message 54 | * @param msgidPlural A plural form of message 55 | * @param n Numeral representing which form to choose 56 | * @return Resolved plural form of message 57 | */ 58 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = 59 | data.pluralCtx.get(msgctx -> msgid) match { 60 | case Some(tr) => tr(plural.plural(n)) 61 | case None => if (n == 1) msgid else msgidPlural 62 | } 63 | 64 | /** 65 | * Returns singular string resolved by tag 66 | * 67 | * @param tag String tag 68 | * @return Resolved string 69 | */ 70 | override def taggedSingular(tag: String): String = tags.taggedSingular(tag) 71 | 72 | /** 73 | * Returns plural string resolved by tag with respect to quantity 74 | * 75 | * @param tag String tag 76 | * @param n Quantity 77 | * @return Resolved string 78 | */ 79 | override def taggedPlural(tag: String, n: Long): String = tags.taggedPlural(tag, n) 80 | 81 | /** 82 | * Merges this language with specified `other`. Conflicting messages will be resolved 83 | * using `this` language. Plural function will be used from `this` language. 84 | * 85 | * @param other Language to merge 86 | * @return Merged language 87 | */ 88 | override def merge(other: Language): Language = other match { 89 | case ml: MergedLanguage => new MergedLanguage(id, data.merge(ml.data), plural, this) 90 | case cc: CompiledLanguage => new MergedLanguage(id, data.merge(cc.messageData), plural, cc) 91 | case Language.English => other 92 | case _ => throw new NotImplementedError("Merge is supported only for MergedLanguage and CompiledLanguage") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/Messages.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import org.portablescala.reflect._ 20 | 21 | object Messages { 22 | /** 23 | * Load all available languages that are compiled by SBT plugin. 24 | * 25 | * @param pkg Package to seek in. Must be equal to `localePackage` SBT setting. 26 | * @return A loaded `Messages` 27 | */ 28 | def compiled(pkg: String = "locales"): Messages = { 29 | val clsOpt = Reflect.lookupLoadableModuleClass(fullyQualifiedClassName(pkg)) 30 | 31 | clsOpt match { 32 | case Some(value) => value.loadModule().asInstanceOf[Messages] 33 | case None => 34 | throw new RuntimeException(s"Failed to load compiled languages from package '$pkg'") 35 | } 36 | } 37 | 38 | /** 39 | * Loads all available languages that are compiled by SBT plugin in context of specified class loader 40 | * 41 | * @param ctx ClassLoader to use 42 | * @param pkg Package to seek in. Must be equal to `localePackage` SBT setting. 43 | * @return A loaded `Messages` 44 | */ 45 | def compiledContext(ctx: ClassLoader, pkg: String = "locales"): Messages = { 46 | val clsOpt = Reflect.lookupLoadableModuleClass(fullyQualifiedClassName(pkg), ctx) 47 | 48 | clsOpt match { 49 | case Some(value) => value.loadModule().asInstanceOf[Messages] 50 | case None => 51 | throw new RuntimeException(s"Failed to load compiled languages from package '$pkg' in context of $ctx") 52 | } 53 | } 54 | 55 | private def fullyQualifiedClassName(pkg: String) = pkg + (if (pkg.nonEmpty) "." else "") + "Languages$" 56 | } 57 | 58 | /** 59 | * Class representing a collection of languages. 60 | * 61 | * @param langs Available languages 62 | */ 63 | class Messages(tags: TaggedLanguage, langs: Language*) { 64 | private val (byLang, byCountry) = { 65 | val lng = Map.newBuilder[String, Language] 66 | val cntr = Map.newBuilder[LanguageId, Language] 67 | var lngs = Set.empty[String] 68 | 69 | for (l <- langs) { 70 | if (!lngs.contains(l.id.language)) { 71 | lng += l.id.language -> l 72 | lngs += l.id.language 73 | } 74 | 75 | cntr += l.id -> l 76 | } 77 | 78 | (lng.result(), cntr.result()) 79 | } 80 | 81 | private val fallback = new Language.English(tags) 82 | 83 | /** 84 | * Retrieves a language from message set by it's ID. The languages are tried in this order: 85 | * 1) Exact language, e.g. `ru_RU` 86 | * 2) Language matched only by language id, e.g. `ru_**` 87 | * 3) Fallback English language 88 | * 89 | * @param lang Language ID to fetch 90 | * @return Fetched language if available, or `Language.English` otherwise. 91 | */ 92 | def apply(lang: LanguageId): Language = byCountry.getOrElse(lang, byLang.getOrElse(lang.language, fallback)) 93 | 94 | /** 95 | * Test whether this messages contains specified language, either exact (`ru_RU`) or fuzzy (`ru_**`). 96 | * 97 | * @param lang Language ID to test 98 | * @return Boolean indicating whether specified language is available 99 | */ 100 | def contains(lang: LanguageId): Boolean = byCountry.contains(lang) || byLang.contains(lang.language) 101 | 102 | /** 103 | * Test whether this messages contains specified language exactly. 104 | * 105 | * @param lang 106 | * @return 107 | */ 108 | def containsExact(lang: LanguageId): Boolean = byCountry.contains(lang) 109 | 110 | /** 111 | * @return Set of all languages defined in this Messages 112 | */ 113 | def definedLanguages: Set[LanguageId] = byCountry.keySet 114 | 115 | /** 116 | * Merge this messages with `other`. Conflicting messages will be resolved from `this`. 117 | */ 118 | def merge(other: Messages): Messages = { 119 | val newLanguages = Seq.newBuilder[Language] 120 | 121 | for (langId <- definedLanguages ++ other.definedLanguages) { 122 | newLanguages += 123 | (if (containsExact(langId) && other.containsExact(langId)) this (langId).merge(other(langId)) 124 | else if (containsExact(langId)) this (langId) 125 | else other(langId)) 126 | } 127 | 128 | new Messages(tags, newLanguages.result():_*) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/OutputFormat.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | object OutputFormat { 20 | /** 21 | * String interpolation format that does nothing at all it's already strings. 22 | */ 23 | implicit val StringFormat: OutputFormat[String] = new OutputFormat[String] { 24 | override def convert(s: String): String = s 25 | override def escape(s: String): String = s 26 | } 27 | } 28 | 29 | /** 30 | * An implicit evidence that strings could be interpolated into type `R`. 31 | * 32 | * @tparam R Result type of interpolation 33 | */ 34 | trait OutputFormat[R] { 35 | /** 36 | * Convert resulting string into type `R` 37 | * 38 | * @param s Complete interpolated string 39 | * @return An instance of `R` 40 | */ 41 | def convert(s: String): R 42 | 43 | /** 44 | * Escape interpolation variable. 45 | * 46 | * @param s A string contents of interpolation variable 47 | * @return A escaped string that will be inserted into interpolation output 48 | */ 49 | def escape(s: String): String 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/PluralFunction.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | /** 20 | * Trait representing `Plural-Forms` *.po header, either statically compiled by SBT plugin or dynamically parsed. 21 | */ 22 | trait PluralFunction { 23 | /** 24 | * Number of plural forms in language 25 | */ 26 | def numPlurals: Int 27 | 28 | /** 29 | * A plural form for number `n` 30 | */ 31 | def plural(n: Long): Int 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/ru/makkarpov/scalingua/TaggedLanguage.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua 2 | 3 | object TaggedLanguage { 4 | val Identity = new TaggedLanguage { 5 | override def taggedSingular(tag: String): String = tag 6 | override def taggedPlural(tag: String, n: Long): String = tag 7 | } 8 | } 9 | 10 | trait TaggedLanguage { 11 | /** 12 | * Returns singular string resolved by tag 13 | * 14 | * @param tag String tag 15 | * @return Resolved string 16 | */ 17 | def taggedSingular(tag: String): String 18 | 19 | /** 20 | * Returns plural string resolved by tag with respect to quantity 21 | * 22 | * @param tag String tag 23 | * @param n Quantity 24 | * @return Resolved string 25 | */ 26 | def taggedPlural(tag: String, n: Long): String 27 | } 28 | -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/I18n.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import play.api.http.HeaderNames 20 | import play.api.mvc.RequestHeader 21 | import ru.makkarpov.scalingua 22 | import ru.makkarpov.scalingua._ 23 | 24 | import scala.language.experimental.macros 25 | import scala.language.implicitConversions 26 | 27 | trait I18n extends scalingua.twirl.I18n { 28 | // The conversion from `RequestHeader` to `LanguageId` will imply information loss: 29 | // Suppose browser sent the following language precedence list `xx_YY`, `zz_WW`, `en_US`. 30 | // This conversion will have no information about available languages, so it will result in 31 | // `LanguageId("xx", "YY")` even if this language is absent. By supplying implicit `Messages` 32 | // too, we will have enough information to skip unsupported languages and return supported 33 | // one with respect to priority. 34 | 35 | implicit def requestHeader2Language(implicit rq: RequestHeader, msg: Messages): Language = 36 | rq.headers.get(HeaderNames.ACCEPT_LANGUAGE).map(PlayUtils.languageFromAccept).getOrElse(Language.English) 37 | } 38 | 39 | object I18n extends I18n -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/MessagesProvider.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import javax.inject.{Inject, Provider} 20 | 21 | import play.api.{Application, Environment} 22 | import ru.makkarpov.scalingua.Messages 23 | 24 | class MessagesProvider @Inject()(conf: ScalinguaConfig, env: Environment) extends Provider[Messages] { 25 | override def get(): Messages = Messages.compiledContext(env.classLoader, conf.localePackage) 26 | } 27 | -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/PlayUtils.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import play.api.mvc.RequestHeader 20 | import ru.makkarpov.scalingua._ 21 | 22 | import scala.language.experimental.macros 23 | 24 | object PlayUtils { 25 | // Modified to match only correct doubles 26 | private val qPattern = ";\\s*q=((?:[0-9]+\\.)?[0-9]+)".r 27 | 28 | def languageFromAccept(accept: String)(implicit msg: Messages): Language = { 29 | val langs = (for { 30 | value0 <- accept.split(',') 31 | value = value0.trim 32 | } yield { 33 | qPattern.findFirstMatchIn(value) match { 34 | case Some(m) => (BigDecimal(m.group(1)), m.before.toString) 35 | case None => (BigDecimal(1.0), value) // “The default value is q=1.” 36 | } 37 | }).sortBy(-_._1).iterator 38 | 39 | while (langs.hasNext) { 40 | val (_, id) = langs.next() 41 | val lng = LanguageId.get(id) 42 | 43 | if (lng.isDefined && msg.contains(lng.get)) 44 | return msg(lng.get) 45 | } 46 | 47 | Language.English 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/ScalinguaConfig.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import play.api.Configuration 20 | 21 | class ScalinguaConfig(cfg: Configuration) { 22 | val localePackage: String = cfg.getOptional[String]("scalingua.package").getOrElse("locales") 23 | } -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/ScalinguaConfigProvider.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import javax.inject.{Inject, Provider} 20 | 21 | import play.api.Configuration 22 | 23 | class ScalinguaConfigProvider @Inject() (cfg: Configuration) extends Provider[ScalinguaConfig] { 24 | override def get(): ScalinguaConfig = new ScalinguaConfig(cfg) 25 | } 26 | -------------------------------------------------------------------------------- /play/src/main/scala/ru/makkarpov/scalingua/play/ScalinguaPlugin.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play 18 | 19 | import javax.inject.Inject 20 | 21 | import play.api.{Application, BuiltInComponentsFromContext, Configuration, Environment} 22 | import play.api.inject.{Binding, Module} 23 | import ru.makkarpov.scalingua.Messages 24 | 25 | trait ScalinguaComponents { 26 | def configuration: Configuration 27 | def environment: Environment 28 | 29 | lazy val messages = new MessagesProvider(new ScalinguaConfigProvider(configuration).get(), environment).get() 30 | } 31 | 32 | class ScalinguaModule extends Module { 33 | override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq( 34 | bind[ScalinguaConfig].toProvider[ScalinguaConfigProvider], 35 | bind[Messages].toProvider[MessagesProvider] 36 | ) 37 | } -------------------------------------------------------------------------------- /play/src/test/scala/ru/makkarpov/scalingua/play/test/MockEnglishLang.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play.test 18 | 19 | import ru.makkarpov.scalingua.{Language, LanguageId} 20 | 21 | case class MockEnglishLang(lid: String) extends Language { 22 | override val id: LanguageId = LanguageId(lid) 23 | 24 | override def singular(msgid: String): String = msgid 25 | override def singular(msgctx: String, msgid: String): String = msgid 26 | override def plural(msgid: String, msgidPlural: String, n: Long): String = if (n == 1) msgid else msgidPlural 27 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = 28 | if (n == 1) msgid else msgidPlural 29 | 30 | override def taggedSingular(tag: String): String = tag 31 | override def taggedPlural(tag: String, n: Long): String = tag 32 | 33 | override def merge(other: Language): Language = throw new NotImplementedError("MockEnglishLang.merge") 34 | } 35 | -------------------------------------------------------------------------------- /play/src/test/scala/ru/makkarpov/scalingua/play/test/PlayTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.play.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.{Language, Messages, TaggedLanguage} 22 | import ru.makkarpov.scalingua.play.I18n._ 23 | 24 | class PlayTest extends AnyFlatSpec with Matchers { 25 | it should "handle 'Accept' header" in { 26 | implicit val messages = new Messages( 27 | TaggedLanguage.Identity, 28 | MockEnglishLang("aa-AA"), 29 | MockEnglishLang("aa-AX"), 30 | MockEnglishLang("bb-BB"), 31 | MockEnglishLang("cc-CC") 32 | ) 33 | 34 | import ru.makkarpov.scalingua.play.PlayUtils.{languageFromAccept => f} 35 | 36 | f("") shouldBe Language.English 37 | f("cc").id.toString shouldBe "cc-CC" 38 | f("bb").id.toString shouldBe "bb-BB" 39 | f("aa").id.toString shouldBe "aa-AA" 40 | f("aa-RR").id.toString shouldBe "aa-AA" 41 | f("aa-AX").id.toString shouldBe "aa-AX" 42 | f("bb, cc").id.toString shouldBe "bb-BB" 43 | f("cc, bb").id.toString shouldBe "cc-CC" 44 | f("xx, yy, zz") shouldBe Language.English 45 | f("tt, tt, cc, tt, tt").id.toString shouldBe "cc-CC" 46 | f("bb, cc; q=0.8").id.toString shouldBe "bb-BB" 47 | f("cc; q=0.8, bb").id.toString shouldBe "bb-BB" 48 | f("aa; q=0.2; bb; q=0.4, cc; q=0.8").id.toString shouldBe "cc-CC" 49 | f("aa; q=0.8, bb; q=0.4, cc; q=0.2").id.toString shouldBe "aa-AA" 50 | 51 | // No exceptions should be thrown on incorrect inputs; English should be returned instead: 52 | f("111111111") shouldBe Language.English 53 | f("aa-AA-ww") shouldBe Language.English 54 | f("aa-AX; q=W") shouldBe Language.English 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /project/ParserGenerator.scala: -------------------------------------------------------------------------------- 1 | import sbt.{Def, _} 2 | import Keys._ 3 | import sbt.plugins.JvmPlugin 4 | import scala.sys.process._ 5 | 6 | object ParserGenerator extends AutoPlugin { 7 | object autoImport { 8 | val generateLexer = taskKey[Seq[File]]("Generate JFlex lexer") 9 | val generateParser = taskKey[Seq[File]]("Generate CUP parser") 10 | } 11 | 12 | import autoImport._ 13 | 14 | override def requires = JvmPlugin 15 | 16 | override def projectSettings: Seq[Def.Setting[_]] = 17 | inConfig(Compile)(configurationSettings) ++ inConfig(Test)(configurationSettings) 18 | 19 | private def generationSettings(extension: String, dir: String, task: TaskKey[Seq[File]]): Seq[Def.Setting[_]] = Seq( 20 | sourceDirectories in task := Seq(baseDirectory.value / ".." / "shared"/ "src" / "main" / "pofile"), 21 | includeFilter in task := extension, 22 | excludeFilter in task := HiddenFileFilter, 23 | 24 | sources in task := 25 | Defaults.collectFiles(sourceDirectories in task, includeFilter in task, excludeFilter in task).value, 26 | 27 | target in task := crossTarget.value / dir, 28 | 29 | sourceGenerators += task.taskValue, 30 | managedSourceDirectories += (target in task).value 31 | ) 32 | 33 | private def configurationSettings: Seq[Def.Setting[_]] = 34 | generationSettings("*.flex", "generated-lexer", generateLexer) ++ 35 | generationSettings("*.cup", "generated-parser", generateParser) ++ 36 | Seq( 37 | generateLexer := generateLexerTask.value, 38 | generateParser := generateParserTask.value 39 | ) 40 | 41 | private def runJava(mainClass: Class[_], args: String*): Unit = { 42 | val loc = new File(mainClass.getProtectionDomain.getCodeSource.getLocation.toURI).getCanonicalPath 43 | val cmdline = Seq("java", "-cp", loc, mainClass.getName) ++ args 44 | val exitCode = cmdline.! 45 | if (exitCode != 0) 46 | throw new RuntimeException(s"Process '${cmdline.mkString(" ")}' exited with code $exitCode") 47 | } 48 | 49 | private def process(src: TaskKey[Seq[File]], tgt: SettingKey[File])(f: (File, File) => Unit) = Def.task[Seq[File]] { 50 | val srcFiles = src.value 51 | val targetDir = tgt.value 52 | 53 | if (srcFiles.isEmpty) Nil 54 | else synchronized { // some weird threading issues, i'm too lazy to debug them. 55 | if (targetDir.exists()) IO.delete(targetDir) 56 | 57 | IO.createDirectory(targetDir) 58 | srcFiles.foreach(f(_, targetDir)) 59 | IO.listFiles(targetDir) 60 | } 61 | } 62 | 63 | def generateLexerTask = 64 | process(sources in generateLexer, target in generateLexer) { (f, t) => 65 | val skel = new File("project/skeleton.jflex") 66 | runJava(classOf[jflex.Main], f.getCanonicalPath, "-d", t.getCanonicalPath, "--skel", skel.getCanonicalPath) 67 | } 68 | 69 | def generateParserTask = 70 | process(sources in generateParser, target in generateParser) { (f, t) => 71 | runJava(classOf[java_cup.Main], "-destdir", t.getCanonicalPath, "-locations", f.getCanonicalPath) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.9.7 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | resolvers += Resolver.url("bintray-sbt-plugins", url("https://dl.bintray.com/eed3si9n/sbt-plugins/"))(Resolver.ivyStylePatterns) 3 | 4 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9") 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") 6 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") 7 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0") 8 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") 9 | 10 | libraryDependencies ++= Seq( 11 | "de.jflex" % "jflex" % "1.6.1", 12 | "com.github.vbmacher" % "java-cup" % "11b-20160615" 13 | ) -------------------------------------------------------------------------------- /project/scripted.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Scalingua [![Build Status](https://travis-ci.org/makkarpov/scalingua.svg?branch=master)](https://travis-ci.org/makkarpov/scalingua) [![Latest version](https://maven-badges.herokuapp.com/maven-central/ru.makkarpov/scalingua_2.11/badge.svg?subject=version)](http://search.maven.org/#search%7Cga%7C1%7Cscalingua%20AND%20g%3A%22ru.makkarpov%22) [![Join the chat at https://gitter.im/makkarpov/scalingua](https://badges.gitter.im/makkarpov/scalingua.svg)](https://gitter.im/makkarpov/scalingua?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | ========= 3 | 4 | Have you ever wondered that there is no **gettext**-like library for Scala? **Scalingua** is here to fix it! It comes with 5 | lightweight runtime library and full-featured compile-time macros and SBT plugin that will combine the powers of **gettext** 6 | and **Scala** in a single library. 7 | 8 | Scalingua consists of four modules: 9 | 10 | * `core` — a minimal runtime components for internationalization. It's very lightweight (~ 30 kB) and provides basic 11 | functions like loading precompiled translations. 12 | * `scalingua` itself — library with macros that have these features: 13 | * String interpolator that makes internationalization of strings as easy as writing one letter before them; 14 | * Plural string interpolator that adds plural suffixes for you; 15 | * Macros for strings with contexts (`msgctxt`) and plural strings; 16 | * Macros leaves no dependency on `scalingua` library — you can include it in `provided` scope and it will not break anything; 17 | * Macros will extract all your strings to separate `*.pot` file every compilation run and keep this file up-to-date with during incremental compilation; 18 | * You can translate more complex formats like HTML — all placeholders will be escaped, so no XSS attacks are possible; 19 | * You can re-use existing macros from this library to implement custom translation utilities (e.g. create `th` interpolator that will translate HTML) or move `I18n` method to your `Utils` object, but in this case you will have dependency on `scalingua` 20 | * `scalingua-sbt` — SBT plugin with small but important task: 21 | * Automatically sets compiler parameters so you don't have to remember them; 22 | * Parses `*.po` files and compiles them to efficient binary files and Scala classes. 23 | * `scalingua-play` — Integration module for Play framework: 24 | * Dependency injection bindings to summon `Messages` through DI; 25 | * HTML output format with ready to use functions and interpolators; 26 | * Extraction of visitor language from request headers. 27 | 28 | Getting started 29 | =============== 30 | 31 | * [Using with Scala](https://github.com/makkarpov/scalingua/wiki/Using-with-Scala) in projects like GUI and console applications 32 | * [Using with Play](https://github.com/makkarpov/scalingua/wiki/Using-with-Play) in projects based on Play Framework 33 | -------------------------------------------------------------------------------- /sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/GenerationContext.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.plugin 18 | 19 | import java.io.{BufferedReader, DataInputStream, FileInputStream, InputStreamReader} 20 | import java.nio.charset.StandardCharsets 21 | 22 | import ru.makkarpov.scalingua.LanguageId 23 | import sbt._ 24 | 25 | object GenerationContext { 26 | val HashMarker = "## Hash: ## " 27 | val ScalaHashPrefix = s"// $HashMarker" 28 | } 29 | 30 | case class GenerationContext(pkg: String, implicitCtx: Option[String], lang: LanguageId, hasTags: Boolean, 31 | src: File, target: File, log: Logger) 32 | { 33 | val srcHash = src.hashString 34 | 35 | def mergeContext(ctx: Option[String]): Option[String] = (implicitCtx, ctx) match { 36 | case (None, None) => None 37 | case (Some(x), None) => Some(x) 38 | case (None, Some(y)) => Some(y) 39 | case (Some(x), Some(y)) => Some(x + ":" + y) 40 | } 41 | 42 | def filePrefix = "/" + pkg.replace('.', '/') + (if (pkg.nonEmpty) "/" else "") 43 | 44 | def checkBinaryHash: Boolean = target.exists() && { 45 | val storedHash = { 46 | val is = new DataInputStream(new FileInputStream(target)) 47 | try is.readUTF() 48 | catch { 49 | case t: Throwable => 50 | t.printStackTrace() 51 | "" 52 | } finally is.close() 53 | } 54 | 55 | srcHash == storedHash 56 | } 57 | 58 | def checkTextHash: Boolean = target.exists() && { 59 | import GenerationContext.HashMarker 60 | 61 | val storedHash = { 62 | val rd = new BufferedReader(new InputStreamReader(new FileInputStream(target), StandardCharsets.UTF_8)) 63 | try { 64 | val l = rd.readLine() 65 | if ((l ne null) && l.contains(HashMarker)) { 66 | val idx = l.indexOf(HashMarker) 67 | l.substring(idx + HashMarker.length) 68 | } else "" 69 | } catch { 70 | case t: Throwable => 71 | t.printStackTrace() 72 | "" 73 | } finally rd.close() 74 | } 75 | 76 | srcHash == storedHash 77 | } 78 | } -------------------------------------------------------------------------------- /sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/ParseFailedException.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.plugin 2 | 3 | case class ParseFailedException(message: String, cause: Throwable) extends Exception(message, cause) 4 | with sbt.UnprintableException { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /sbt-plugin/src/main/scala/ru/makkarpov/scalingua/plugin/PoCompilerStrategy.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.plugin 18 | 19 | import java.io.{ByteArrayOutputStream, OutputStream} 20 | 21 | sealed trait PoCompilerStrategy { 22 | /** Specifies if plugin must generate Languages object indexing available languages. */ 23 | def generatesIndex: Boolean = true 24 | 25 | /** Specifies if plugin must package *.po files into binary files. */ 26 | def isPackagingNecessary: Boolean = true 27 | 28 | def getEnglishTagsDefinition(EnglishTagsClass: String): String = s"object $EnglishTagsClass" 29 | 30 | def getEnglishTagsInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String 31 | 32 | def getCompiledLanguageDefinition(ctx: GenerationContext): String = 33 | s"object Language_${ctx.lang.language}_${ctx.lang.country}" 34 | 35 | def getCompiledLanguageInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String 36 | } 37 | 38 | object PoCompilerStrategy { 39 | def getStrategy(definition: String): PoCompilerStrategy = definition match { 40 | case "ReadFromResources" => new ReadFromResourcesStrategy 41 | case "InlineBase64" => new InlineBase64Strategy 42 | case "LoadInRuntime" => new LoadInRuntimeStrategy 43 | case _ => throw new IllegalArgumentException("Cannot create PoCompilerStrategy.") 44 | } 45 | } 46 | 47 | class ReadFromResourcesStrategy extends PoCompilerStrategy { 48 | override def getEnglishTagsInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 49 | s""" 50 | | initialize({ 51 | | val str = getClass.getResourceAsStream("${ctx.filePrefix}compiled_english_tags.bin") 52 | | if (str eq null) 53 | | throw new NullPointerException("Compiled english tags not found!") 54 | | str 55 | | }) 56 | """.stripMargin 57 | 58 | override def getCompiledLanguageInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 59 | s""" 60 | | initialize({ 61 | | val str = getClass.getResourceAsStream("${ctx.filePrefix}data_${ctx.lang.language}_${ctx.lang.country}.bin") 62 | | if (str eq null) { 63 | | throw new IllegalArgumentException("Resource not found for language ${ctx.lang.language}_${ctx.lang.country}") 64 | | } 65 | | str 66 | | }) 67 | """.stripMargin 68 | } 69 | 70 | class InlineBase64Strategy extends PoCompilerStrategy { 71 | 72 | import InlineBase64Strategy._ 73 | 74 | override val isPackagingNecessary: Boolean = false 75 | 76 | override def getEnglishTagsInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 77 | s""" 78 | | val arr = ${inlineBase64Arr(getStream)} 79 | | 80 | | initialize({ 81 | | import java.util.Base64 82 | | import java.io.ByteArrayInputStream 83 | | val decoded = Base64.getDecoder().decode(arr.mkString("").getBytes()) 84 | | val str = new ByteArrayInputStream(decoded) 85 | | if (str eq null) 86 | | throw new NullPointerException("Compiled english tags not found!") 87 | | str 88 | | }) 89 | """.stripMargin 90 | 91 | override def getCompiledLanguageInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 92 | s""" 93 | | val arr = ${inlineBase64Arr(getStream)} 94 | | 95 | | initialize({ 96 | | import java.util.Base64 97 | | import java.io.ByteArrayInputStream 98 | | val decoded = Base64.getDecoder().decode(arr.mkString("").getBytes()) 99 | | val str = new ByteArrayInputStream(decoded) 100 | | if (str eq null) { 101 | | throw new IllegalArgumentException("Resource not found for language ${ctx.lang.language}_${ctx.lang.country}") 102 | | } 103 | | str 104 | | }) 105 | """.stripMargin 106 | } 107 | 108 | object InlineBase64Strategy { 109 | 110 | import java.util.Base64 111 | 112 | private val JavaStringLiteralLimit = 65535 113 | 114 | private def inlineBase64Arr(getStream: OutputStream => Unit): String = { 115 | val out = new ByteArrayOutputStream() 116 | getStream(out) 117 | val str = Base64.getEncoder.encodeToString(out.toByteArray) 118 | val arrElements = str.sliding(JavaStringLiteralLimit, JavaStringLiteralLimit).map(s => { 119 | s""""${s}"""" 120 | }).mkString(",") 121 | s"Array($arrElements)" 122 | } 123 | } 124 | 125 | class LoadInRuntimeStrategy extends PoCompilerStrategy { 126 | override def generatesIndex: Boolean = false 127 | 128 | override def getEnglishTagsDefinition(EnglishTagsClass: String): String = s"class ${EnglishTagsClass}(is: java.io.InputStream)" 129 | 130 | override def getEnglishTagsInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 131 | s"initialize(is)" 132 | 133 | override def getCompiledLanguageDefinition(ctx: GenerationContext): String = 134 | s"class Language_${ctx.lang.language}_${ctx.lang.country}(is: java.io.InputStream)" 135 | 136 | override def getCompiledLanguageInitializationBlock(ctx: GenerationContext, getStream: OutputStream => Unit): String = 137 | s"initialize(is)" 138 | } 139 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/fail-on-invalid-locales/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "some.test.pkg" -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/fail-on-invalid-locales/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/fail-on-invalid-locales/src/test/locales/invalid.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | #: src/test/scala/Test.scala:19 15 | #: src/test/scala/Test.scala:12 16 | msgid "Hello, world!" 17 | msgstr "Привет, мир!" 18 | 19 | #: src/test/scala/Test.scala:13 20 | #: src/test/scala/Test.scala:20 21 | msgid "There is %(n) dog!" 22 | msgid_plural "There is %(n) dogs!" 23 | msgstr[0] "Здесь %(n) собака!" 24 | msgstr[1] "Здесь %(n) собаки!" 25 | msgstr[2] "Здесь %(n) собак!" 26 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/fail-on-invalid-locales/test: -------------------------------------------------------------------------------- 1 | -> test -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/implicit-context/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "ru.makkarpov" 13 | implicitContext in Test := Some("ru.makkarpov") -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/implicit-context/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/implicit-context/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | msgid "Hello, world!" 15 | msgstr "Привет, мир!" 16 | 17 | msgctxt "something" 18 | msgid "Hello, world!" 19 | msgstr "Привет, мир в контексте something!" -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/implicit-context/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val messages = Messages.compiled("ru.makkarpov") 8 | implicit val languageId = LanguageId("ru-RU") 9 | 10 | it should "correctly translate strings" in { 11 | t"Hello, world!" shouldBe "Привет, мир!" 12 | 13 | tc("something", "Hello, world!") shouldBe "Привет, мир в контексте something!" 14 | } 15 | 16 | it should "include contextual strings in Messages" in { 17 | val lang = messages(languageId) 18 | 19 | lang.singular("Hello, world!") shouldBe "Hello, world!" 20 | lang.singular("ru.makkarpov", "Hello, world!") shouldBe "Привет, мир!" 21 | lang.singular("something", "Hello, world!") shouldBe "Hello, world!" 22 | lang.singular("ru.makkarpov:something", "Hello, world!") shouldBe "Привет, мир в контексте something!" 23 | } 24 | 25 | it should "reference contextual strings" in { 26 | implicit val mockLang = new Language { 27 | override def singular(msgctx: String, msgid: String): String = msgctx + "/" + msgid 28 | override def plural(msgid: String, msgidPlural: String, n: Long): String = fail 29 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = fail 30 | override def singular(msgid: String): String = fail 31 | override def taggedSingular(tag: String): String = fail 32 | override def taggedPlural(tag: String, n: Long): String = fail 33 | 34 | override def merge(other: Language): Language = fail 35 | 36 | def fail = throw new IllegalArgumentException("Called an unexpected method") 37 | 38 | override def id: LanguageId = LanguageId("xx-XX") 39 | } 40 | 41 | t"Hello, world!" shouldBe "ru.makkarpov/Hello, world!" 42 | tc("context", "Hello, world!") shouldBe "ru.makkarpov:context/Hello, world!" 43 | } 44 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/implicit-context/test: -------------------------------------------------------------------------------- 1 | > test -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/build.sbt: -------------------------------------------------------------------------------- 1 | name := "inline-scala-js" 2 | 3 | enablePlugins(ScalaJSPlugin, Scalingua) 4 | 5 | libraryDependencies ++= Seq( 6 | "ru.makkarpov" %%% "scalingua" % { 7 | val v = System.getProperty("scalingua.version") 8 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 9 | else v 10 | }, 11 | "org.scalatest" %%% "scalatest" % "3.2.2" % Test 12 | ) 13 | 14 | localePackage in Test := "some.test.pkg" 15 | compileLocalesStrategy in Test := "InlineBase64" 16 | scalaJSUseMainModuleInitializer := true 17 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | #: src/test/scala/Test.scala:12 3 | #: src/test/scala/Test.scala:19 4 | #: src/test/scala/Test.scala:26 5 | #: src/test/scala/Test.scala:33 6 | #: src/test/scala/Test.scala:40 7 | msgid "Hello, world!" 8 | msgstr "" 9 | 10 | #: src/test/scala/Test.scala:13 11 | #: src/test/scala/Test.scala:20 12 | #: src/test/scala/Test.scala:27 13 | #: src/test/scala/Test.scala:34 14 | #: src/test/scala/Test.scala:41 15 | msgid "There is %(n) dog!" 16 | msgid_plural "There is %(n) dogs!" 17 | msgstr[0] "" 18 | msgstr[1] "" 19 | 20 | #: src/test/scala/Test.scala:47 21 | msgid "Percents! %" 22 | msgstr "" 23 | 24 | #: src/test/scala/Test.scala:48 25 | msgid "Percents!! %" 26 | msgstr "" 27 | 28 | #: src/test/scala/Test.scala:51 29 | msgid "Percents with variables%%: %(x), percents%%" 30 | msgstr "" 31 | 32 | #: src/test/scala/Test.scala:53 33 | msgid "Percents after variable: %(x)%%" 34 | msgstr "" 35 | 36 | #: src/test/scala/Test.scala:56 37 | msgid "Look, I have %(x) percent: %%!" 38 | msgid_plural "Look, I have %(x) percents: %%!" 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: src/test/scala/Test.scala:68 43 | msgid "Привет, мир!" 44 | msgstr "" 45 | 46 | #: src/test/scala/Test.scala:69 47 | msgid "Weird\u2019quotes" 48 | msgstr "" 49 | 50 | #: src/test/scala/Test.scala:74 51 | msgid "There is %(n) string in this file!" 52 | msgid_plural "There are %(n) strings in this file!" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | #: src/test/scala/Test.scala:77 57 | msgid "And it works!" 58 | msgstr "" 59 | 60 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.3 2 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") 2 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 3 | val ver = System.getProperty("scalingua.version") 4 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 5 | ver 6 | }) 7 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | #: src/test/scala/Test.scala:19 15 | #: src/test/scala/Test.scala:12 16 | msgid "Hello, world!" 17 | msgstr "Привет, мир!" 18 | 19 | #: src/test/scala/Test.scala:13 20 | #: src/test/scala/Test.scala:20 21 | msgid "There is %(n) dog!" 22 | msgid_plural "There is %(n) dogs!" 23 | msgstr[0] "Здесь %(n) собака!" 24 | msgstr[1] "Здесь %(n) собаки!" 25 | msgstr[2] "Здесь %(n) собак!" 26 | 27 | #: src/test/scala/Test.scala:47 28 | msgid "Percents! %" 29 | msgstr "Проценты! %" 30 | 31 | #: src/test/scala/Test.scala:48 32 | msgid "Percents!! %" 33 | msgstr "Проценты!! %" 34 | 35 | #: src/test/scala/Test.scala:51 36 | msgid "Percents with variables%%: %(x), percents%%" 37 | msgstr "Проценты с перменными%%: %(x), проценты%%" 38 | 39 | #: src/test/scala/Test.scala:53 40 | msgid "Percents after variable: %(x)%%" 41 | msgstr "Проценты после переменной: %(x)%%" 42 | 43 | #: src/test/scala/Test.scala:56 44 | msgid "Look, I have %(x) percent: %%!" 45 | msgid_plural "Look, I have %(x) percents: %%!" 46 | msgstr[0] "Смотри, у меня %(x) процент: %%!" 47 | msgstr[1] "Смотри, у меня %(x) процента: %%!" 48 | msgstr[2] "Смотри, у меня %(x) процентов: %%!" 49 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val messages = Messages.compiled("some.test.pkg") 8 | 9 | it should "provide correct messages for en_US" in { 10 | implicit val langId = LanguageId("en-US") 11 | 12 | t("Hello, world!") shouldBe "Hello, world!" 13 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 14 | } 15 | 16 | it should "provide correct messages for ru_RU" in { 17 | implicit val langId = LanguageId("ru-RU") 18 | 19 | t("Hello, world!") shouldBe "Привет, мир!" 20 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 21 | } 22 | 23 | it should "provide english messages for absent languages" in { 24 | implicit val langId = LanguageId("xx-QQ") 25 | 26 | t("Hello, world!") shouldBe "Hello, world!" 27 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 28 | } 29 | 30 | it should "provide correct messages for other countries (ru_XX)" in { 31 | implicit val langId = LanguageId("ru-XX") 32 | 33 | t("Hello, world!") shouldBe "Привет, мир!" 34 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 35 | } 36 | 37 | it should "provide correct messages for generic languages (ru)" in { 38 | implicit val langId = LanguageId("ru") 39 | 40 | t("Hello, world!") shouldBe "Привет, мир!" 41 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 42 | } 43 | 44 | it should "handle percent signs" in { 45 | implicit val langId = LanguageId("ru-RU") 46 | 47 | t"Percents! %" shouldBe "Проценты! %" 48 | t("Percents!! %%") shouldBe "Проценты!! %" 49 | 50 | val x = 1 51 | t"Percents with variables%: $x, percents%" shouldBe s"Проценты с перменными%: $x, проценты%" 52 | 53 | t"Percents after variable: $x%%" shouldBe s"Проценты после переменной: $x%" 54 | 55 | // Plural: 56 | p"Look, I have $x percent${S.s}: %!" shouldBe s"Смотри, у меня $x процент: %!" 57 | } 58 | 59 | it should "reject invalid percent signs" in { 60 | """ 61 | val x = 123 62 | t"Test: $x% qweqwe" 63 | """ shouldNot typeCheck 64 | } 65 | 66 | it should "escape unicode literals" in { 67 | implicit val langId = LanguageId("en-US") 68 | t"Привет, мир!" shouldBe "Привет, мир!" 69 | t"Weird’quotes" shouldBe "Weird’quotes" 70 | } 71 | 72 | it should "work around Java string literal limit" in { 73 | implicit val langId = LanguageId("pl-PL") 74 | p("There is %(n) string in this file!", "There are %(n) strings in this file!", 7000) shouldBe 75 | "W tym pliku jest 7000 łańcuchów znaków!" 76 | 77 | t("And it works!") shouldBe "A mimo to działa!" 78 | 79 | } 80 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/test: -------------------------------------------------------------------------------- 1 | > test 2 | $ exec chmod +x verify-pot.sh 3 | $ exec bash ./verify-pot.sh -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/inline-scala-js/verify-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "target/scala-2.12/messages/test.pot" ]; then 4 | echo "test.pot does not exists!" >&2 5 | exit 1 6 | fi 7 | 8 | tail -n +2 "target/scala-2.12/messages/test.pot" > generated.pot 9 | diff -u generated.pot messages.pot || exit 1 -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "some.test.pkg" 13 | compileLocalesStrategy in Test := "LoadInRuntime" 14 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | #: src/test/scala/Test.scala:19 3 | #: src/test/scala/Test.scala:26 4 | #: src/test/scala/Test.scala:33 5 | #: src/test/scala/Test.scala:40 6 | #: src/test/scala/Test.scala:47 7 | msgid "Hello, world!" 8 | msgstr "" 9 | 10 | #: src/test/scala/Test.scala:20 11 | #: src/test/scala/Test.scala:27 12 | #: src/test/scala/Test.scala:34 13 | #: src/test/scala/Test.scala:41 14 | #: src/test/scala/Test.scala:48 15 | msgid "There is %(n) dog!" 16 | msgid_plural "There is %(n) dogs!" 17 | msgstr[0] "" 18 | msgstr[1] "" 19 | 20 | #: src/test/scala/Test.scala:54 21 | msgid "Percents! %" 22 | msgstr "" 23 | 24 | #: src/test/scala/Test.scala:55 25 | msgid "Percents!! %" 26 | msgstr "" 27 | 28 | #: src/test/scala/Test.scala:58 29 | msgid "Percents with variables%%: %(x), percents%%" 30 | msgstr "" 31 | 32 | #: src/test/scala/Test.scala:60 33 | msgid "Percents after variable: %(x)%%" 34 | msgstr "" 35 | 36 | #: src/test/scala/Test.scala:63 37 | msgid "Look, I have %(x) percent: %%!" 38 | msgid_plural "Look, I have %(x) percents: %%!" 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: src/test/scala/Test.scala:75 43 | msgid "Привет, мир!" 44 | msgstr "" 45 | 46 | #: src/test/scala/Test.scala:76 47 | msgid "Weird\u2019quotes" 48 | msgstr "" 49 | 50 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | #: src/test/scala/Test.scala:19 15 | #: src/test/scala/Test.scala:12 16 | msgid "Hello, world!" 17 | msgstr "Привет, мир!" 18 | 19 | #: src/test/scala/Test.scala:13 20 | #: src/test/scala/Test.scala:20 21 | msgid "There is %(n) dog!" 22 | msgid_plural "There is %(n) dogs!" 23 | msgstr[0] "Здесь %(n) собака!" 24 | msgstr[1] "Здесь %(n) собаки!" 25 | msgstr[2] "Здесь %(n) собак!" 26 | 27 | #: src/test/scala/Test.scala:47 28 | msgid "Percents! %" 29 | msgstr "Проценты! %" 30 | 31 | #: src/test/scala/Test.scala:48 32 | msgid "Percents!! %" 33 | msgstr "Проценты!! %" 34 | 35 | #: src/test/scala/Test.scala:51 36 | msgid "Percents with variables%%: %(x), percents%%" 37 | msgstr "Проценты с перменными%%: %(x), проценты%%" 38 | 39 | #: src/test/scala/Test.scala:53 40 | msgid "Percents after variable: %(x)%%" 41 | msgstr "Проценты после переменной: %(x)%%" 42 | 43 | #: src/test/scala/Test.scala:56 44 | msgid "Look, I have %(x) percent: %%!" 45 | msgid_plural "Look, I have %(x) percents: %%!" 46 | msgstr[0] "Смотри, у меня %(x) процент: %%!" 47 | msgstr[1] "Смотри, у меня %(x) процента: %%!" 48 | msgstr[2] "Смотри, у меня %(x) процентов: %%!" 49 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{Language, LanguageId, Messages, TaggedLanguage} 4 | import ru.makkarpov.scalingua.I18n._ 5 | import ru.makkarpov.scalingua.{CompiledLanguage, PluralFunction, TaggedLanguage} 6 | import some.test.pkg._ 7 | 8 | class Test extends AnyFlatSpec with Matchers { 9 | it should "skip index generation" in { 10 | val compiledMessages: scala.util.Try[Messages] = scala.util.Try(Messages.compiled("some.test.pkg")) 11 | compiledMessages shouldBe 'Failure 12 | } 13 | 14 | implicit val messages = ManuallyLoadedLanguages 15 | 16 | it should "provide correct messages for en_US" in { 17 | implicit val langId = LanguageId("en-US") 18 | 19 | t("Hello, world!") shouldBe "Hello, world!" 20 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 21 | } 22 | 23 | it should "provide correct messages for ru_RU" in { 24 | implicit val langId = LanguageId("ru-RU") 25 | 26 | t("Hello, world!") shouldBe "Привет, мир!" 27 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 28 | } 29 | 30 | it should "provide english messages for absent languages" in { 31 | implicit val langId = LanguageId("xx-QQ") 32 | 33 | t("Hello, world!") shouldBe "Hello, world!" 34 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 35 | } 36 | 37 | it should "provide correct messages for other countries (ru_XX)" in { 38 | implicit val langId = LanguageId("ru-XX") 39 | 40 | t("Hello, world!") shouldBe "Привет, мир!" 41 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 42 | } 43 | 44 | it should "provide correct messages for generic languages (ru)" in { 45 | implicit val langId = LanguageId("ru") 46 | 47 | t("Hello, world!") shouldBe "Привет, мир!" 48 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 49 | } 50 | 51 | it should "handle percent signs" in { 52 | implicit val langId = LanguageId("ru-RU") 53 | 54 | t"Percents! %" shouldBe "Проценты! %" 55 | t("Percents!! %%") shouldBe "Проценты!! %" 56 | 57 | val x = 1 58 | t"Percents with variables%: $x, percents%" shouldBe s"Проценты с перменными%: $x, проценты%" 59 | 60 | t"Percents after variable: $x%%" shouldBe s"Проценты после переменной: $x%" 61 | 62 | // Plural: 63 | p"Look, I have $x percent${S.s}: %!" shouldBe s"Смотри, у меня $x процент: %!" 64 | } 65 | 66 | it should "reject invalid percent signs" in { 67 | """ 68 | val x = 123 69 | t"Test: $x% qweqwe" 70 | """ shouldNot typeCheck 71 | } 72 | 73 | it should "escape unicode literals" in { 74 | implicit val langId = LanguageId("en-US") 75 | t"Привет, мир!" shouldBe "Привет, мир!" 76 | t"Weird’quotes" shouldBe "Weird’quotes" 77 | } 78 | } 79 | 80 | object ManuallyLoadedLanguages extends Messages( 81 | TaggedLanguage.Identity, 82 | new Language_ru_RU(getClass.getResourceAsStream("/some/test/pkg/data_ru_RU.bin")) 83 | ) 84 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/test: -------------------------------------------------------------------------------- 1 | > test 2 | $ exec chmod +x verify-pot.sh 3 | $ exec bash ./verify-pot.sh -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/load-in-runtime/verify-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "target/scala-2.12/messages/test.pot" ]; then 4 | echo "test.pot does not exists!" >&2 5 | exit 1 6 | fi 7 | 8 | tail -n +2 "target/scala-2.12/messages/test.pot" > generated.pot 9 | diff -u generated.pot messages.pot || exit 1 -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/build.sbt: -------------------------------------------------------------------------------- 1 | resolvers in ThisBuild += Resolver.defaultLocal 2 | libraryDependencies in ThisBuild ++= Seq( 3 | "ru.makkarpov" %% "scalingua" % { 4 | val v = System.getProperty("scalingua.version") 5 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 6 | else v 7 | }, 8 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 9 | ) 10 | 11 | lazy val subA = project 12 | .enablePlugins(Scalingua) 13 | .settings( 14 | localePackage in Compile := "subA", 15 | implicitContext in Compile := Some("subA") 16 | ) 17 | 18 | lazy val subB = project 19 | .enablePlugins(Scalingua) 20 | .settings( 21 | localePackage in Compile := "subB", 22 | implicitContext in Compile := Some("subB") 23 | ) 24 | 25 | lazy val root = project.in(file(".")).dependsOn(subA, subB) -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val languageId = LanguageId("ru-RU") 8 | 9 | val subAMessages = Messages.compiled("subA") 10 | val subBMessages = Messages.compiled("subB") 11 | 12 | it should "translate SubA separately" in { 13 | implicit val messages = subAMessages 14 | 15 | SubA.testA shouldBe "Тест первый" 16 | SubA.testB shouldBe "Тест второй" 17 | SubB.testA shouldBe "Test A" 18 | SubB.testB shouldBe "Test B" 19 | } 20 | 21 | it should "translate SubB separately" in { 22 | implicit val messages = subBMessages 23 | 24 | SubA.testA shouldBe "Test A" 25 | SubA.testB shouldBe "Test B" 26 | SubB.testA shouldBe "Первый тест" 27 | SubB.testB shouldBe "Второй тест" 28 | } 29 | 30 | it should "translate merged messages" in { 31 | implicit val messages = subAMessages.merge(subBMessages) 32 | 33 | SubA.testA shouldBe "Тест первый" 34 | SubA.testB shouldBe "Тест второй" 35 | SubB.testA shouldBe "Первый тест" 36 | SubB.testB shouldBe "Второй тест" 37 | } 38 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/subA/src/main/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | msgid "Test A" 15 | msgstr "Тест первый" 16 | 17 | msgid "Test B" 18 | msgstr "Тест второй" 19 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/subA/src/main/scala/SubA.scala: -------------------------------------------------------------------------------- 1 | import ru.makkarpov.scalingua.Language 2 | import ru.makkarpov.scalingua.I18n._ 3 | 4 | object SubA { 5 | def testA(implicit msgs: Language) = t"Test A" 6 | def testB(implicit msgs: Language) = t"Test B" 7 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/subB/src/main/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | msgid "Test A" 15 | msgstr "Первый тест" 16 | 17 | msgid "Test B" 18 | msgstr "Второй тест" 19 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/subB/src/main/scala/SubB.scala: -------------------------------------------------------------------------------- 1 | import ru.makkarpov.scalingua.Language 2 | import ru.makkarpov.scalingua.I18n._ 3 | 4 | object SubB { 5 | def testA(implicit msgs: Language) = t"Test A" 6 | def testB(implicit msgs: Language) = t"Test B" 7 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/merging/test: -------------------------------------------------------------------------------- 1 | > test -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/src/test/locales/de_DE.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "PO-Revision-Date: 2018-02-23 11:49+0300\n" 5 | "Last-Translator: Maxim Karpov \n" 6 | "Language-Team: German\n" 7 | "Language: de\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=ISO-8859-1\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 12 | 13 | # !Generated: 2018-02-23 11:47:45 14 | #: src/test/scala/Test.scala:19 src/test/scala/Test.scala:25 15 | #: src/test/scala/Test.scala:13 src/test/scala/Test.scala:31 16 | msgid "Good evening!" 17 | msgstr "Guten Abend!" 18 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "PO-Revision-Date: 2018-02-23 11:49+0300\n" 5 | "Last-Translator: Maxim Karpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=ISO-8859-5\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | # !Generated: 2018-02-23 11:47:45 15 | #: src/test/scala/Test.scala:19 src/test/scala/Test.scala:25 16 | #: src/test/scala/Test.scala:13 src/test/scala/Test.scala:31 17 | msgid "Good evening!" 18 | msgstr "Добрый вечер!" 19 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/src/test/locales/zh_ZH.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "PO-Revision-Date: 2018-02-23 11:49+0300\n" 5 | "Last-Translator: Maxim Karpov \n" 6 | "Language-Team: Chinese\n" 7 | "Language: zh_ZH\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=ASCII\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | 12 | # !Generated: 2018-02-23 11:47:45 13 | #: src/test/scala/Test.scala:19 src/test/scala/Test.scala:25 14 | #: src/test/scala/Test.scala:13 src/test/scala/Test.scala:31 15 | msgid "Good evening!" 16 | msgstr "晚上好!" 17 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | // test whether Scalingua is able to compile messages for multiple languages at once: 7 | class Test extends AnyFlatSpec with Matchers { 8 | implicit val messages = Messages.compiled() 9 | 10 | it should "provide messages for English" in { 11 | implicit val lang = LanguageId("en-US") 12 | 13 | t"Good evening!" shouldBe "Good evening!" 14 | } 15 | 16 | it should "provide messages for German" in { 17 | implicit val lang = LanguageId("de-DE") 18 | 19 | t"Good evening!" shouldBe "Guten Abend!" 20 | } 21 | 22 | it should "provide messages for Russian" in { 23 | implicit val lang = LanguageId("ru-RU") 24 | 25 | t"Good evening!" shouldBe "Добрый вечер!" 26 | } 27 | 28 | it should "provide messages for Chinese" in { 29 | implicit val lang = LanguageId("zh-ZH") 30 | 31 | t"Good evening!" shouldBe "晚上好!" 32 | } 33 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/multilang/test: -------------------------------------------------------------------------------- 1 | > test -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "some.test.pkg" 13 | escapeUnicode in Test := false -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | #: src/test/scala/Test.scala:11 3 | msgid "Hello, world!" 4 | msgstr "" 5 | 6 | #: src/test/scala/Test.scala:12 7 | msgid "Привет, мир!" 8 | msgstr "" 9 | 10 | #: src/test/scala/Test.scala:13 11 | msgid "Weird’quotes" 12 | msgstr "" 13 | 14 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val messages = Messages.compiled("some.test.pkg") 8 | implicit val langId = LanguageId("en-US") 9 | 10 | it should "disable escaping of unicode" in { 11 | t("Hello, world!") shouldBe "Hello, world!" 12 | t("Привет, мир!") shouldBe "Привет, мир!" 13 | t"Weird’quotes" shouldBe "Weird’quotes" 14 | } 15 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/test: -------------------------------------------------------------------------------- 1 | > test 2 | $ exec chmod +x verify-pot.sh 3 | $ exec bash ./verify-pot.sh -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/no-escape-unicode/verify-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "target/scala-2.12/messages/test.pot" ]; then 4 | echo "test.pot does not exists!" >&2 5 | exit 1 6 | fi 7 | 8 | tail -n +2 "target/scala-2.12/messages/test.pot" > generated.pot 9 | diff -u generated.pot messages.pot || exit 1 -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "some.test.pkg" -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | #: src/test/scala/Test.scala:12 3 | #: src/test/scala/Test.scala:19 4 | #: src/test/scala/Test.scala:26 5 | #: src/test/scala/Test.scala:33 6 | #: src/test/scala/Test.scala:40 7 | msgid "Hello, world!" 8 | msgstr "" 9 | 10 | #: src/test/scala/Test.scala:13 11 | #: src/test/scala/Test.scala:20 12 | #: src/test/scala/Test.scala:27 13 | #: src/test/scala/Test.scala:34 14 | #: src/test/scala/Test.scala:41 15 | msgid "There is %(n) dog!" 16 | msgid_plural "There is %(n) dogs!" 17 | msgstr[0] "" 18 | msgstr[1] "" 19 | 20 | #: src/test/scala/Test.scala:47 21 | msgid "Percents! %" 22 | msgstr "" 23 | 24 | #: src/test/scala/Test.scala:48 25 | msgid "Percents!! %" 26 | msgstr "" 27 | 28 | #: src/test/scala/Test.scala:51 29 | msgid "Percents with variables%%: %(x), percents%%" 30 | msgstr "" 31 | 32 | #: src/test/scala/Test.scala:53 33 | msgid "Percents after variable: %(x)%%" 34 | msgstr "" 35 | 36 | #: src/test/scala/Test.scala:56 37 | msgid "Look, I have %(x) percent: %%!" 38 | msgid_plural "Look, I have %(x) percents: %%!" 39 | msgstr[0] "" 40 | msgstr[1] "" 41 | 42 | #: src/test/scala/Test.scala:68 43 | msgid "Привет, мир!" 44 | msgstr "" 45 | 46 | #: src/test/scala/Test.scala:69 47 | msgid "Weird\u2019quotes" 48 | msgstr "" 49 | 50 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | #: src/test/scala/Test.scala:19 15 | #: src/test/scala/Test.scala:12 16 | msgid "Hello, world!" 17 | msgstr "Привет, мир!" 18 | 19 | #: src/test/scala/Test.scala:13 20 | #: src/test/scala/Test.scala:20 21 | msgid "There is %(n) dog!" 22 | msgid_plural "There is %(n) dogs!" 23 | msgstr[0] "Здесь %(n) собака!" 24 | msgstr[1] "Здесь %(n) собаки!" 25 | msgstr[2] "Здесь %(n) собак!" 26 | 27 | #: src/test/scala/Test.scala:47 28 | msgid "Percents! %" 29 | msgstr "Проценты! %" 30 | 31 | #: src/test/scala/Test.scala:48 32 | msgid "Percents!! %" 33 | msgstr "Проценты!! %" 34 | 35 | #: src/test/scala/Test.scala:51 36 | msgid "Percents with variables%%: %(x), percents%%" 37 | msgstr "Проценты с перменными%%: %(x), проценты%%" 38 | 39 | #: src/test/scala/Test.scala:53 40 | msgid "Percents after variable: %(x)%%" 41 | msgstr "Проценты после переменной: %(x)%%" 42 | 43 | #: src/test/scala/Test.scala:56 44 | msgid "Look, I have %(x) percent: %%!" 45 | msgid_plural "Look, I have %(x) percents: %%!" 46 | msgstr[0] "Смотри, у меня %(x) процент: %%!" 47 | msgstr[1] "Смотри, у меня %(x) процента: %%!" 48 | msgstr[2] "Смотри, у меня %(x) процентов: %%!" 49 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val messages = Messages.compiled("some.test.pkg") 8 | 9 | it should "provide correct messages for en_US" in { 10 | implicit val langId = LanguageId("en-US") 11 | 12 | t("Hello, world!") shouldBe "Hello, world!" 13 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 14 | } 15 | 16 | it should "provide correct messages for ru_RU" in { 17 | implicit val langId = LanguageId("ru-RU") 18 | 19 | t("Hello, world!") shouldBe "Привет, мир!" 20 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 21 | } 22 | 23 | it should "provide english messages for absent languages" in { 24 | implicit val langId = LanguageId("xx-QQ") 25 | 26 | t("Hello, world!") shouldBe "Hello, world!" 27 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "There is 7 dogs!" 28 | } 29 | 30 | it should "provide correct messages for other countries (ru_XX)" in { 31 | implicit val langId = LanguageId("ru-XX") 32 | 33 | t("Hello, world!") shouldBe "Привет, мир!" 34 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 35 | } 36 | 37 | it should "provide correct messages for generic languages (ru)" in { 38 | implicit val langId = LanguageId("ru") 39 | 40 | t("Hello, world!") shouldBe "Привет, мир!" 41 | p("There is %(n) dog!", "There is %(n) dogs!", 7) shouldBe "Здесь 7 собак!" 42 | } 43 | 44 | it should "handle percent signs" in { 45 | implicit val langId = LanguageId("ru-RU") 46 | 47 | t"Percents! %" shouldBe "Проценты! %" 48 | t("Percents!! %%") shouldBe "Проценты!! %" 49 | 50 | val x = 1 51 | t"Percents with variables%: $x, percents%" shouldBe s"Проценты с перменными%: $x, проценты%" 52 | 53 | t"Percents after variable: $x%%" shouldBe s"Проценты после переменной: $x%" 54 | 55 | // Plural: 56 | p"Look, I have $x percent${S.s}: %!" shouldBe s"Смотри, у меня $x процент: %!" 57 | } 58 | 59 | it should "reject invalid percent signs" in { 60 | """ 61 | val x = 123 62 | t"Test: $x% qweqwe" 63 | """ shouldNot typeCheck 64 | } 65 | 66 | it should "escape unicode literals" in { 67 | implicit val langId = LanguageId("en-US") 68 | t"Привет, мир!" shouldBe "Привет, мир!" 69 | t"Weird’quotes" shouldBe "Weird’quotes" 70 | } 71 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/test: -------------------------------------------------------------------------------- 1 | > test 2 | $ exec chmod +x verify-pot.sh 3 | $ exec bash ./verify-pot.sh -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/simple-lang/verify-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "target/scala-2.12/messages/test.pot" ]; then 4 | echo "test.pot does not exists!" >&2 5 | exit 1 6 | fi 7 | 8 | tail -n +2 "target/scala-2.12/messages/test.pot" > generated.pot 9 | diff -u generated.pot messages.pot || exit 1 -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(Scalingua) 2 | 3 | libraryDependencies ++= Seq( 4 | "ru.makkarpov" %% "scalingua" % { 5 | val v = System.getProperty("scalingua.version") 6 | if(v == null) throw new RuntimeException("Scalingua version is not defined") 7 | else v 8 | }, 9 | "org.scalatest" %% "scalatest" % "3.1.0" % Test 10 | ) 11 | 12 | localePackage in Test := "some.test.pkg" 13 | taggedFile in Test := Some(file("tagged.json")) -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/messages.pot: -------------------------------------------------------------------------------- 1 | 2 | #: src/test/scala/Test.scala:12 3 | #: src/test/scala/Test.scala:21 4 | msgid "Test" 5 | msgstr "" 6 | 7 | # cmt 8 | #: tagged-messages.json 9 | #~ test.plural.key 10 | msgid "singular form" 11 | msgid_plural "plural forms" 12 | msgstr[0] "" 13 | msgstr[1] "" 14 | 15 | #: tagged-messages.json 16 | #~ test.key 17 | msgid "test message" 18 | msgstr "" 19 | 20 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ru.makkarpov" % "scalingua-sbt" % { 2 | val ver = System.getProperty("scalingua.version") 3 | if(ver == null) throw new RuntimeException("Scalingua version is not defined") 4 | ver 5 | }) 6 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/src/test/locales/ru_RU.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: test 0.1\n" 4 | "PO-Revision-Date: 2016-04-20 12:45+0300\n" 5 | "Last-Translator: makkarpov \n" 6 | "Language-Team: Russian\n" 7 | "Language: ru-RU\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 12 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 13 | 14 | #: src/test/scala/Test.scala:12 15 | msgid "Test" 16 | msgstr "Тест" 17 | 18 | #: tagged-messages.json 19 | #~ test.plural.key 20 | msgid "singular form" 21 | msgid_plural "plural forms" 22 | msgstr[0] "Единственное число" 23 | msgstr[1] "Почти единственное число" 24 | msgstr[2] "Множественное число" 25 | 26 | #: tagged-messages.json 27 | #~ test.key 28 | msgid "test message" 29 | msgstr "Тестовое сообщение" 30 | 31 | 32 | -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/src/test/scala/Test.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import ru.makkarpov.scalingua.{LanguageId, Messages, Language} 4 | import ru.makkarpov.scalingua.I18n._ 5 | 6 | class Test extends AnyFlatSpec with Matchers { 7 | implicit val messages = Messages.compiled("some.test.pkg") 8 | 9 | it should "provide correct messages for en_US" in { 10 | implicit val languageId = LanguageId("en-US") 11 | 12 | t"Test" shouldBe "Test" 13 | tag("test.key") shouldBe "test message" 14 | ptag("test.plural.key", 1) shouldBe "singular form" 15 | ptag("test.plural.key", 2) shouldBe "plural forms" 16 | } 17 | 18 | it should "provide correct messages for ru_RU" in { 19 | implicit val languageId = LanguageId("ru-RU") 20 | 21 | t"Test" shouldBe "Тест" 22 | tag("test.key") shouldBe "Тестовое сообщение" 23 | ptag("test.plural.key", 1) shouldBe "Единственное число" 24 | ptag("test.plural.key", 2) shouldBe "Почти единственное число" 25 | ptag("test.plural.key", 5) shouldBe "Множественное число" 26 | } 27 | 28 | it should "provide correct messages for lazy strings" in { 29 | val l = lptag("test.plural.key", 2) 30 | 31 | { 32 | implicit val id = LanguageId("ru-RU") 33 | l.resolve shouldBe "Почти единственное число" 34 | } 35 | 36 | { 37 | implicit val id = LanguageId("en-US") 38 | l.resolve shouldBe "plural forms" 39 | } 40 | 41 | { 42 | implicit val id = LanguageId("xx-YY") 43 | l.resolve shouldBe "plural forms" 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/tagged.json: -------------------------------------------------------------------------------- 1 | { 2 | "test.key":"test message", 3 | "test.plural.key": { 4 | "message": "singular form", 5 | "plural": "plural forms", 6 | "comments": "cmt" 7 | } 8 | } -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/test: -------------------------------------------------------------------------------- 1 | > test 2 | $ exec chmod +x verify-pot.sh 3 | $ exec bash ./verify-pot.sh -------------------------------------------------------------------------------- /sbt-plugin/src/sbt-test/main/tagged-strings/verify-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "target/scala-2.12/messages/test.pot" ]; then 4 | echo "test.pot does not exists!" >&2 5 | exit 1 6 | fi 7 | 8 | tail -n +2 "target/scala-2.12/messages/test.pot" > generated.pot 9 | diff -u generated.pot messages.pot || exit 1 -------------------------------------------------------------------------------- /scalingua/shared/src/main/pofile/pofile.cup: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse; 2 | 3 | import java_cup.runtime.*; 4 | import ru.makkarpov.scalingua.pofile.*; 5 | import ru.makkarpov.scalingua.pofile.Message.*; 6 | import scala.Option; 7 | import scala.collection.immutable.Seq; 8 | import scala.collection.mutable.Builder; 9 | 10 | class PoParser; 11 | 12 | parser code {: 13 | MutableHeader header = new MutableHeader(); 14 | MutableMultipartString mps = new MutableMultipartString(); 15 | MutablePlurals plurals = new MutablePlurals(); 16 | Builder> messages; 17 | :}; 18 | 19 | terminal MSGID, MSGID_PLURAL, MSGSTR, MSGCTXT; 20 | terminal int MSGSTR_PLURAL; 21 | terminal String STRING; 22 | terminal Comment COMMENT; 23 | 24 | nonterminal headerBuilder, stringBuilder, pluralsBuilder, entriesBuilder; 25 | nonterminal MessageHeader header; 26 | nonterminal MultipartString string; 27 | nonterminal Seq plurals; 28 | nonterminal Option context; 29 | nonterminal MultipartString msgid, msgidPlural, msgstr; 30 | nonterminal Message entry; 31 | nonterminal Seq entries; 32 | 33 | start with entries; 34 | 35 | context ::= MSGCTXT string:s {: RESULT = ParseUtils.some(s); :} 36 | | {: RESULT = ParseUtils.none(); :}; 37 | 38 | header ::= {: header.reset(); :} headerBuilder {: RESULT = header.result(); :}; 39 | 40 | headerBuilder ::= headerBuilder COMMENT:c {: header.add(c, cxleft, cxright); :} 41 | | {: /* epsilon */ :}; 42 | 43 | string ::= {: mps.reset(); :} stringBuilder {: RESULT = mps.result(); :}; 44 | 45 | stringBuilder ::= STRING:s {: mps.add(s); :} 46 | | stringBuilder STRING:s {: mps.add(s); :}; 47 | 48 | plurals ::= {: plurals.reset(); :} pluralsBuilder {: RESULT = plurals.result(); :}; 49 | 50 | pluralsBuilder ::= MSGSTR_PLURAL:p string:s {: plurals.add(p, s, pxleft, sxright); :} 51 | | pluralsBuilder MSGSTR_PLURAL:p string:s {: plurals.add(p, s, pxleft, sxright); :}; 52 | 53 | msgid ::= MSGID string:s {: RESULT = s; :}; 54 | 55 | msgidPlural ::= MSGID_PLURAL string:s {: RESULT = s; :}; 56 | 57 | msgstr ::= MSGSTR string:s {: RESULT = s; :}; 58 | 59 | entry ::= header:h context:c msgid:i msgstr:s {: RESULT = new Singular(h, c, i, s); :} 60 | | header:h context:c msgid:i msgidPlural:p plurals:s {: RESULT = new Plural(h, c, i, p, s); :}; 61 | 62 | entries ::= {: messages = ParseUtils.newBuilder(); :} entriesBuilder header {: RESULT = messages.result(); :}; 63 | 64 | entriesBuilder ::= entriesBuilder entry:e {: ParseUtils.add(messages, e); :} 65 | | {: /* epsilon */ :}; 66 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/pofile/pofile.flex: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse; 2 | 3 | import java_cup.runtime.Symbol; 4 | import java_cup.runtime.ComplexSymbolFactory.Location; 5 | import java_cup.runtime.ComplexSymbolFactory.ComplexSymbol; 6 | 7 | %% 8 | 9 | %class PoLexer 10 | %unicode 11 | %line 12 | %column 13 | %char 14 | %cup 15 | %public 16 | 17 | %{ 18 | private String filename = ""; 19 | private StringBuilder string = new StringBuilder(); 20 | private char commentTag = '\0'; 21 | private Location storedLocation; 22 | 23 | private Location loc() { 24 | return new Location(filename, yyline, yycolumn, yychar); 25 | } 26 | 27 | private void storeLoc() { 28 | storedLocation = loc(); 29 | } 30 | 31 | private Location storedLoc() { 32 | Location r = storedLocation; 33 | storedLocation = null; 34 | 35 | if (r == null) 36 | throw new RuntimeException("No stored location available"); 37 | 38 | return r; 39 | } 40 | 41 | private Symbol sym(Location left, int id, Object arg) { 42 | Location right = new Location(filename, yyline, yycolumn + yylength(), yychar + yylength()); 43 | return new ComplexSymbol(PoParserSym.terminalNames[id], id, left, right, arg); 44 | } 45 | 46 | private Symbol sym(int id, Object arg) { 47 | return sym(loc(), id, arg); 48 | } 49 | 50 | private Symbol sym(int id) { 51 | return sym(loc(), id, null); 52 | } 53 | 54 | private void lexerError(String msg) { 55 | throw new LexerException(loc(), msg); 56 | } 57 | 58 | private int extractNum() { 59 | return Integer.parseInt(yytext().replaceAll("[^0-9]+", "")); 60 | } 61 | 62 | private char extractEscape() { 63 | return (char) Integer.parseInt(yytext().replaceAll("[^0-9a-fA-F]+", ""), 16); 64 | } 65 | 66 | public PoLexer(java.io.Reader reader, String filename) { 67 | this(reader); 68 | this.filename = filename; 69 | } 70 | %} 71 | 72 | LineSeparator = \r|\n|\r\n 73 | InputCharacter = [^\r\n] 74 | WhiteSpace = {LineSeparator}|\s 75 | Digit = [0-9] 76 | HexDigit = [0-9a-fA-F] 77 | 78 | MsgId = "msgid" 79 | MsgIdPlural = "msgid_plural" 80 | MsgContext = "msgctxt" 81 | MsgString = "msgstr" 82 | MsgStringPlural = "msgstr[" {Digit}+ "]" 83 | Comment = "#" 84 | CommentFirst = \S|" " 85 | UnicodeEscape = \\u{HexDigit}{HexDigit}{HexDigit}{HexDigit} 86 | 87 | %state STRING 88 | %state LINE_START 89 | %state COMMENT_START 90 | %state COMMENT 91 | 92 | %% 93 | 94 | { 95 | {MsgContext} { return sym(PoParserSym.MSGCTXT); } 96 | {MsgId} { return sym(PoParserSym.MSGID); } 97 | {MsgIdPlural} { return sym(PoParserSym.MSGID_PLURAL); } 98 | {MsgString} { return sym(PoParserSym.MSGSTR); } 99 | {MsgStringPlural} { return sym(PoParserSym.MSGSTR_PLURAL, extractNum()); } 100 | 101 | // comments can occur only at the beginning of line 102 | ^{Comment} { storeLoc(); yybegin(COMMENT_START); } 103 | {Comment} { lexerError("Comments can only occur at the beginning of line"); } 104 | 105 | \" { storeLoc(); string.setLength(0); yybegin(STRING); } 106 | {WhiteSpace} {} 107 | 108 | [^] { lexerError("Unrecognized token"); } 109 | } 110 | 111 | { 112 | \" { yybegin(YYINITIAL); return sym(storedLoc(), PoParserSym.STRING, string.toString()); } 113 | [^\r\n\"\\]+ { string.append(yytext()); } 114 | \\r { string.append('\r'); } 115 | \\n { string.append('\n'); } 116 | \\t { string.append('\t'); } 117 | \\b { string.append('\b'); } 118 | \\f { string.append('\f'); } 119 | \\\\ { string.append('\\'); } 120 | \\\" { string.append('"'); } 121 | \\' { string.append('\''); } 122 | {UnicodeEscape} { string.append(extractEscape()); } 123 | \\ { lexerError("Unrecognized escape sequence"); } 124 | {LineSeparator} { lexerError("Unterminated string"); } 125 | } 126 | 127 | { 128 | {CommentFirst} { commentTag = yycharat(0); string.setLength(0); yybegin(COMMENT); } 129 | [^] { lexerError("Invalid comment tag character"); } 130 | } 131 | 132 | { 133 | [^\r\n]+ { string.append(yytext()); } 134 | {LineSeparator} { yybegin(YYINITIAL); return sym(storedLoc(), PoParserSym.COMMENT, new Comment(commentTag, string.toString())); } 135 | } 136 | 137 | <> { return sym(PoParserSym.EOF); } -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala-2.12/ru/makkarpov/scalingua/Compat.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import ru.makkarpov.scalingua.extract.MessageExtractor 20 | 21 | import scala.reflect.macros.whitebox 22 | 23 | object Compat { 24 | type Context = whitebox.Context 25 | def prettyPrint(c: Context)(e: c.Tree): String = c.universe.showCode(e) 26 | def termName(c: Context)(s: String): c.TermName = c.universe.TermName(c.freshName(s)) 27 | def typecheck(c: Context)(e: c.Tree): c.Tree = c.typecheck(e) 28 | 29 | def processEscapes(s: String) = scala.StringContext.processEscapes(s) 30 | 31 | implicit class MutSetOps[A](s: scala.collection.mutable.Set[A]) { 32 | def filterInPlace(p: A => Boolean) = s.retain(p) 33 | } 34 | 35 | val CollectionConverters = scala.collection.JavaConverters 36 | } 37 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala-2.13/ru/makkarpov/scalingua/Compat.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import ru.makkarpov.scalingua.extract.MessageExtractor 20 | 21 | import scala.reflect.macros.whitebox 22 | 23 | object Compat { 24 | type Context = whitebox.Context 25 | def prettyPrint(c: Context)(e: c.Tree): String = c.universe.showCode(e) 26 | def termName(c: Context)(s: String): c.TermName = c.universe.TermName(c.freshName(s)) 27 | def typecheck(c: Context)(e: c.Tree): c.Tree = c.typecheck(e) 28 | 29 | def processEscapes(s: String) = scala.StringContext.processEscapes(s) 30 | 31 | val CollectionConverters = scala.jdk.CollectionConverters 32 | } 33 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/I18n.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | import ru.makkarpov.scalingua.plural.Suffix 20 | 21 | import scala.language.experimental.macros 22 | import scala.language.implicitConversions 23 | import scala.reflect.internal.annotations.compileTimeOnly 24 | 25 | trait I18n { 26 | type LString = LValue[String] 27 | 28 | val S = Suffix 29 | 30 | // Since I want to keep StringInterpolator AnyVal, I will extract it to a place where there is no path-dependency 31 | // and provide here only implicit conversion function. 32 | implicit def stringContext2Interpolator(sc: StringContext): I18n.StringInterpolator = 33 | new I18n.StringInterpolator(sc) 34 | 35 | implicit def long2MacroExtension(v: Long): I18n.PluralMacroExtensions = 36 | new I18n.PluralMacroExtensions(v) 37 | 38 | implicit def int2MacroExtension(i: Int): I18n.PluralMacroExtensions = 39 | new I18n.PluralMacroExtensions(i) 40 | 41 | implicit def string2SuffixExtension(s: String): Suffix.GenericSuffixExtension = 42 | new Suffix.GenericSuffixExtension(s) 43 | 44 | def t(msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 45 | macro Macros.singular[String] 46 | 47 | def lt(msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LString = 48 | macro Macros.lazySingular[String] 49 | 50 | def tc(ctx: String, msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 51 | macro Macros.singularCtx[String] 52 | 53 | def ltc(ctx: String, msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LString = 54 | macro Macros.lazySingularCtx[String] 55 | 56 | def p(msg: String, msgPlural: String, n: Long, args: (String, Any)*) 57 | (implicit lang: Language, outputFormat: OutputFormat[String]): String = 58 | macro Macros.plural[String] 59 | 60 | def lp(msg: String, msgPlural: String, n: Long, args: (String, Any)*) 61 | (implicit outputFormat: OutputFormat[String]): LString = 62 | macro Macros.lazyPlural[String] 63 | 64 | def pc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) 65 | (implicit lang: Language, outputFormat: OutputFormat[String]): String = 66 | macro Macros.pluralCtx[String] 67 | 68 | def lpc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) 69 | (implicit outputFormat: OutputFormat[String]): LString = 70 | macro Macros.lazyPluralCtx[String] 71 | 72 | // Tagged: 73 | 74 | def tag(tag: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 75 | macro Macros.singularTag[String] 76 | 77 | def ltag(tag: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LValue[String] = 78 | macro Macros.lazySingularTag[String] 79 | 80 | def ptag(tag: String, n: Long, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 81 | macro Macros.pluralTag[String] 82 | 83 | def lptag(tag: String, n: Long, args: (String, Any)*)(implicit outputFormat: OutputFormat[String]): LValue[String] = 84 | macro Macros.lazyPluralTag[String] 85 | } 86 | 87 | object I18n extends I18n { 88 | class StringInterpolator(val sc: StringContext) extends AnyVal { 89 | def t(args: Any*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 90 | macro Macros.interpolate[String] 91 | 92 | def lt(args: Any*)(implicit outputFormat: OutputFormat[String]): LString = 93 | macro Macros.lazyInterpolate[String] 94 | 95 | def p(args: Any*)(implicit lang: Language, outputFormat: OutputFormat[String]): String = 96 | macro Macros.pluralInterpolate[String] 97 | 98 | def lp(args: Any*)(implicit outputFormat: OutputFormat[String]): LString = 99 | macro Macros.lazyPluralInterpolate[String] 100 | } 101 | 102 | class PluralMacroExtensions(val l: Long) extends AnyVal { 103 | def nVar: Long = throw new IllegalStateException(".nVars should not remain after macro expansion") 104 | } 105 | } -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/InsertableIterator.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua 18 | 19 | object InsertableIterator { 20 | implicit class IteratorExtensions[T](val it: Iterator[T]) extends AnyVal { 21 | def insertable = new InsertableIterator[T](it) 22 | } 23 | } 24 | 25 | class InsertableIterator[T](backing: Iterator[T]) extends Iterator[T] with scala.collection.BufferedIterator[T] { 26 | private var queue = List.empty[T] 27 | 28 | override def hasNext: Boolean = queue.nonEmpty || backing.hasNext 29 | 30 | override def next(): T = queue match { 31 | case Nil => backing.next() 32 | case head :: rest => 33 | queue = rest 34 | head 35 | } 36 | 37 | override def head: T = queue match { 38 | case Nil => 39 | val el = backing.next() 40 | queue ::= el 41 | el 42 | 43 | case head :: _ => head 44 | } 45 | 46 | def unnext(t: T): Unit = queue ::= t 47 | } 48 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/extract/ExtractorSettings.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.extract 18 | 19 | import java.io.File 20 | 21 | import ru.makkarpov.scalingua.Compat._ 22 | 23 | object ExtractorSettings { 24 | val SettingsPrefix = "scalingua:" 25 | 26 | def fromContext(c: Context): ExtractorSettings = { 27 | val setts = c.settings 28 | .filter(_.startsWith(SettingsPrefix)) 29 | .map(_.substring(SettingsPrefix.length).split("=", 2)) 30 | .map{ 31 | case Array(k, v) => k -> v 32 | case Array(x) => c.abort(c.enclosingPosition, s"Invalid setting: `$x`") 33 | }.toMap 34 | 35 | val enable = setts.contains("target") 36 | val targetFile = new File(setts.getOrElse("target", "messages.pot")) 37 | val baseDir = setts.getOrElse("baseDir", "") 38 | val taggedFile = setts.get("taggedFile").map(new File(_)).filter(_.exists) 39 | val implicitContext = setts.get("implicitContext").filter(_.nonEmpty) 40 | val escapeUnicode = setts.get("escapeUnicode").exists(_ == "true") 41 | 42 | ExtractorSettings(enable, new File(baseDir), targetFile, implicitContext, taggedFile, escapeUnicode) 43 | } 44 | } 45 | 46 | case class ExtractorSettings(enable: Boolean, srcBaseDir: File, targetFile: File, implicitContext: Option[String], 47 | taggedFile: Option[File], escapeUnicode: Boolean) { 48 | def mergeContext(ctx: Option[String]): Option[String] = (implicitContext, ctx) match { 49 | case (None, None) => None 50 | case (Some(x), None) => Some(x) 51 | case (None, Some(y)) => Some(y) 52 | case (Some(x), Some(y)) => Some(x + ":" + y) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/extract/MessageExtractor.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.extract 18 | 19 | import ru.makkarpov.scalingua.Compat._ 20 | 21 | object MessageExtractor { 22 | private var session = Option.empty[ExtractorSession] 23 | 24 | def setupSession(c: Context): ExtractorSession = { 25 | val r = 26 | try { 27 | session match { 28 | case None => new ExtractorSession(c.universe, ExtractorSettings.fromContext(c)) 29 | case Some(sess) if c.universe eq sess.global => sess 30 | case Some(sess) => 31 | sess.finish() 32 | new ExtractorSession(c.universe, ExtractorSettings.fromContext(c)) 33 | } 34 | } catch { 35 | case e: TaggedParseException => c.abort(c.enclosingPosition, e.getMessage) 36 | } 37 | 38 | session = Some(r) 39 | r 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/extract/TaggedParseException.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.extract 2 | 3 | case class TaggedParseException(msg: String) extends RuntimeException(msg) { 4 | def this(msg: String, cause: Throwable) = { 5 | this(msg) 6 | initCause(cause) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/extract/TaggedParser.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.extract 2 | 3 | import java.io.{File, FileInputStream, InputStreamReader} 4 | import java.nio.charset.StandardCharsets 5 | 6 | import com.grack.nanojson.{JsonObject, JsonParser, JsonParserException} 7 | import ru.makkarpov.scalingua.pofile.Message.{Plural, Singular} 8 | import ru.makkarpov.scalingua.pofile._ 9 | import ru.makkarpov.scalingua.Compat.CollectionConverters._ 10 | 11 | object TaggedParser { 12 | val TaggedFileName = "tagged-messages.json" 13 | 14 | case class TaggedMessage(tag: String, msg: String, plural: Option[String], comment: Seq[String]) { 15 | def toMessage: Message = { 16 | val header = MessageHeader(comment, Nil, MessageLocation(TaggedFileName) :: Nil, MessageFlag.empty, Some(tag)) 17 | 18 | plural match { 19 | case None => Singular(header, None, MultipartString(msg), MultipartString.empty) 20 | case Some(p) => Plural(header, None, MultipartString(msg), MultipartString(p), 21 | Seq(MultipartString.empty, MultipartString.empty)) 22 | } 23 | } 24 | } 25 | 26 | /* 27 | * Format for tagged JSON file: 28 | * 29 | * { 30 | * "some.message.tag": { 31 | * "message": "...", // message itself, mandatory 32 | * "plural": "...", // plural version of message, optional 33 | * "comments": [ "...", "..."] // comments, optional 34 | * }, 35 | * 36 | * // or, simply: 37 | * "some.other.message.tag": "message" 38 | * } 39 | */ 40 | def parse(f: File): Seq[TaggedMessage] = { 41 | val ret = Vector.newBuilder[TaggedMessage] 42 | 43 | try { 44 | val obj = { 45 | val r = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8) 46 | try JsonParser.`object`().from(r) finally r.close() 47 | } 48 | 49 | for (k <- obj.keySet().asScala) obj.get(k) match { 50 | case v: JsonObject => 51 | if (!v.has("message")) 52 | throw TaggedParseException(s"Object with key '$k' has no 'message' field") 53 | 54 | if (!v.isString("message")) 55 | throw TaggedParseException(s"Object with key '$k' has non-string 'message' field") 56 | 57 | val msg = v.getString("message") 58 | 59 | val plural = 60 | if (v.has("plural")) { 61 | if (!v.isString("plural")) 62 | throw TaggedParseException(s"Object with key '$k' has non-string 'plural' field") 63 | Some(v.getString("plural")) 64 | } else None 65 | 66 | val comments = 67 | if (v.has("comments")) { 68 | if (v.isString("comments")) v.getString("comments") :: Nil 69 | else v.getArray("comments").asScala.toList.map(_.asInstanceOf[String]) 70 | } else Nil 71 | 72 | ret += TaggedMessage(k, msg, plural, comments) 73 | 74 | case v: String => 75 | ret += TaggedMessage(k, v, None, Nil) 76 | } 77 | } catch { 78 | case e: JsonParserException => 79 | throw new TaggedParseException(s"Tagged JSON syntax error at ${f.getCanonicalPath}:${e.getLinePosition}:${e.getCharPosition}", e) 80 | } 81 | 82 | ret.result() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/plural/Expression.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.plural 18 | 19 | object Expression { 20 | case class Constant(v: Long) extends Expression { 21 | override def eval(n: Long): Long = v 22 | 23 | override def boolResult: Boolean = (v == 0) || (v == 1) 24 | override def strictResult = false 25 | 26 | override protected def writeBool(sb: StringBuilder) = sb.append(v != 0) 27 | override protected def writeLong(sb: StringBuilder) = sb.append(v) += 'L' 28 | } 29 | 30 | case object Variable extends Expression { 31 | override def eval(n: Long): Long = n 32 | 33 | override def boolResult: Boolean = false 34 | override def strictResult: Boolean = true 35 | 36 | override protected def writeLong(sb: StringBuilder) = sb ++= "arg" 37 | } 38 | 39 | case class UnaryOp(op: String, arg: Expression) extends Expression { 40 | override def eval(n: Long): Long = { 41 | val v = arg.eval(n) 42 | op match { 43 | case "+" => v 44 | case "-" => -v 45 | case "~" => ~v 46 | case "!" => if (v == 0) 1 else 0 47 | } 48 | } 49 | 50 | override def boolResult: Boolean = op == "!" 51 | override def strictResult: Boolean = true 52 | 53 | override protected def writeBool(sb: StringBuilder): StringBuilder = 54 | if (arg.boolResult) arg.asBool(sb ++= "!(") += ')' 55 | else arg.asBool(sb += '(') ++= ") == 0" 56 | 57 | override protected def writeLong(sb: StringBuilder): StringBuilder = 58 | arg.asLong(sb ++= op += '(') += ')' 59 | } 60 | 61 | case class BinaryOp(op: String, left: Expression, right: Expression) extends Expression { 62 | @inline private def long(b: Boolean): Long = if (b) 1 else 0 63 | 64 | override def eval(n: Long): Long = op match { 65 | case "+" => left.eval(n) + right.eval(n) 66 | case "-" => left.eval(n) - right.eval(n) 67 | case "*" => left.eval(n) * right.eval(n) 68 | case "/" => left.eval(n) / right.eval(n) 69 | case "%" => left.eval(n) % right.eval(n) 70 | 71 | case "<<" => left.eval(n) << right.eval(n) 72 | case ">>" => left.eval(n) >> right.eval(n) 73 | 74 | case "<=" => long(left.eval(n) <= right.eval(n)) 75 | case "<" => long(left.eval(n) < right.eval(n)) 76 | case ">=" => long(left.eval(n) >= right.eval(n)) 77 | case ">" => long(left.eval(n) > right.eval(n)) 78 | case "==" => long(left.eval(n) == right.eval(n)) 79 | case "!=" => long(left.eval(n) != right.eval(n)) 80 | 81 | case "&" => left.eval(n) & right.eval(n) 82 | case "|" => left.eval(n) | right.eval(n) 83 | case "^" => left.eval(n) ^ right.eval(n) 84 | 85 | case "&&" => long((left.eval(n) != 0) && (right.eval(n) != 0)) 86 | case "||" => long((left.eval(n) != 0) || (right.eval(n) != 0)) 87 | } 88 | 89 | override def boolResult: Boolean = op match { 90 | case ">" | "<" | ">=" | "<=" | "!=" | "==" => true 91 | case "&&" | "||" => true 92 | case "&" | "|" | "^" => left.boolResult && right.boolResult 93 | case _ => false 94 | } 95 | 96 | override def strictResult: Boolean = true 97 | 98 | override protected def writeBool(sb: StringBuilder): StringBuilder = op match { 99 | case ">" | "<" | ">=" | "<=" | "!=" | "==" => 100 | if (left.boolResult && right.boolResult) 101 | right.asBool(left.asBool(sb += '(') += ')' ++= op += '(') += ')' 102 | else 103 | right.asLong(left.asLong(sb += '(') += ')' ++= op += '(') += ')' 104 | case _ => right.asBool(left.asBool(sb += '(') += ')' ++= op += '(') += ')' 105 | } 106 | 107 | 108 | override protected def writeLong(sb: StringBuilder): StringBuilder = 109 | right.asLong(left.asLong(sb += '(') += ')' ++= op += '(') += ')' 110 | } 111 | 112 | case class TernaryOp(cond: Expression, ifTrue: Expression, ifFalse: Expression) extends Expression { 113 | override def eval(n: Long): Long = if (cond.eval(n) != 0) ifTrue.eval(n) else ifFalse.eval(n) 114 | 115 | override def boolResult: Boolean = ifTrue.boolResult && ifFalse.boolResult 116 | override def strictResult: Boolean = ifTrue.strictResult || ifFalse.strictResult 117 | 118 | override protected def writeLong(sb: StringBuilder): StringBuilder = writeBool(sb) // type-invariant 119 | override protected def writeBool(sb: StringBuilder): StringBuilder = { 120 | sb ++= "if (" 121 | cond.asBool(sb) 122 | sb ++= ") (" 123 | if (boolResult) ifTrue.asBool(sb) 124 | else ifTrue.asLong(sb) 125 | sb ++= ") else (" 126 | if (boolResult) ifFalse.asBool(sb) 127 | else ifFalse.asLong(sb) 128 | sb += ')' 129 | } 130 | } 131 | } 132 | 133 | sealed trait Expression { 134 | def boolResult: Boolean 135 | def strictResult: Boolean 136 | 137 | protected def writeBool(sb: StringBuilder): StringBuilder = sb 138 | protected def writeLong(sb: StringBuilder): StringBuilder = sb 139 | 140 | def asBool(sb: StringBuilder): StringBuilder = 141 | if (boolResult || !strictResult) writeBool(sb) 142 | else { 143 | sb += '(' 144 | writeLong(sb) 145 | sb ++= ") != 0" 146 | } 147 | 148 | def asLong(sb: StringBuilder): StringBuilder = 149 | if (!boolResult || !strictResult) writeLong(sb) 150 | else { 151 | sb ++= "if (" 152 | writeBool(sb) 153 | sb ++= ") 1 else 0" 154 | } 155 | 156 | def scalaExpression = asLong(new StringBuilder).result() 157 | override def toString: String = scalaExpression 158 | 159 | def eval(n: Long): Long 160 | } 161 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/plural/ParsedPlural.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.plural 18 | 19 | import ru.makkarpov.scalingua.PluralFunction 20 | 21 | object ParsedPlural { 22 | val English = new ParsedPlural { 23 | override def expr = Parser("!(n == 1)") 24 | override def numPlurals: Int = 2 25 | override def plural(n: Long): Int = if (n != 1) 1 else 0 26 | } 27 | 28 | private val header = "^\\s*nplurals=(\\d+); plural=(.*);?$".r 29 | 30 | def fromHeader(s: String): ParsedPlural = s match { 31 | case header(num, func) => new ParsedPlural { 32 | val expr = Parser(func) 33 | override def numPlurals: Int = num.toInt 34 | override def plural(n: Long): Int = expr.eval(n).toInt 35 | } 36 | 37 | case _ => throw new IllegalArgumentException(s"Bad plurals string format: $s") 38 | } 39 | } 40 | 41 | trait ParsedPlural extends PluralFunction { 42 | def expr: Expression 43 | } 44 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/plural/Suffix.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.plural 18 | 19 | /** 20 | * Represents plural suffixes that will be understood by macros. If tree typechecks to either `Suffix.S` or 21 | * `Suffix.ES`, it's considered as plural suffix. No instances of `Suffix.*` exists. 22 | */ 23 | object Suffix { 24 | sealed trait Generic extends Suffix 25 | sealed trait S extends Suffix 26 | sealed trait ES extends Suffix 27 | 28 | case class GenericSuffixExtension(s: String) extends AnyVal { 29 | def &>(plur: String): Suffix.Generic = 30 | throw new IllegalArgumentException("&> should not remain after macro expansion") 31 | } 32 | 33 | def s: S = throw new IllegalArgumentException(".s or .es should not remain after macro expansion") 34 | def es: ES = throw new IllegalArgumentException(".s or .es should not remain after macro expansion") 35 | } 36 | 37 | sealed trait Suffix -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/Message.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | object Message { 20 | case class Singular(header: MessageHeader, context: Option[MultipartString], message: MultipartString, 21 | translation: MultipartString) extends Message 22 | 23 | case class Plural(header: MessageHeader, context: Option[MultipartString], message: MultipartString, 24 | plural: MultipartString, translations: Seq[MultipartString]) extends Message 25 | } 26 | 27 | sealed trait Message { 28 | def header: MessageHeader 29 | 30 | def context: Option[MultipartString] 31 | def message: MultipartString 32 | } 33 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/MessageFlag.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | object MessageFlag extends Enumeration { 20 | val Fuzzy = Value("fuzzy") 21 | 22 | val empty: ValueSet = ValueSet() 23 | } 24 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/MessageHeader.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | case class MessageHeader(comments: Seq[String], extractedComments: Seq[String], locations: Seq[MessageLocation], 20 | flags: MessageFlag.ValueSet, tag: Option[String]) { 21 | def isTagged: Boolean = tag.isDefined 22 | } 23 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/MessageLocation.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | import java.io.File 20 | 21 | object MessageLocation { 22 | implicit case object LocationOrdering extends Ordering[MessageLocation] { 23 | override def compare(x: MessageLocation, y: MessageLocation): Int = { 24 | //fs independent comparison 25 | val r = x.file.toString.compareTo(y.file.toString) match { 26 | case 0 => x.line.compare(y.line) 27 | case x => x 28 | } 29 | r 30 | } 31 | } 32 | 33 | def apply(file: String): MessageLocation = MessageLocation(new File(file), -1) 34 | } 35 | 36 | case class MessageLocation(file: File, line: Int) { 37 | def fileString: String = file.toString.replace("""\""", "/") 38 | } 39 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/MultipartString.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | object MultipartString { 20 | val empty = new MultipartString() 21 | 22 | // Method is kept to allow use of xx.map(MultipartString.apply) 23 | def apply(s: String): MultipartString = new MultipartString(s) 24 | } 25 | 26 | case class MultipartString(parts: String*) { 27 | def merge = parts.mkString 28 | def isEmpty = parts.forall(_.isEmpty) 29 | 30 | override def toString = s"MultipartString(${parts.mkString(", ")})" 31 | } 32 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/NewLinePrintWriter.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile 2 | 3 | import java.io.{PrintWriter, Writer} 4 | 5 | //uses system independent new lines 6 | class NewLinePrintWriter(out: Writer, autoFlush: Boolean) 7 | extends PrintWriter(out, autoFlush) { 8 | def this(out: Writer) = this(out, false) 9 | 10 | override def println(): Unit = { 11 | print("\n") 12 | if (autoFlush) flush() 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/ParserTest.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile 2 | 3 | import java.io.StringReader 4 | import java_cup.runtime.ComplexSymbolFactory 5 | 6 | import parse.{ErrorReportingParser, PoLexer, PoParser, PoParserSym} 7 | 8 | object ParserTest extends App { 9 | val text = "# comment\n#. ex comment\n#: test.scala:10\n#, fuzzy,qwe\n#~ some.tag\nmsgid \"ololo\"\nmsgstr \"test\\u00A7\"\n" 10 | 11 | println("INPUT:") 12 | 13 | println(text) 14 | 15 | println("LISTING TOKENS:") 16 | 17 | val lexer = new PoLexer(new StringReader(text)) 18 | var token: java_cup.runtime.Symbol = _ 19 | do { 20 | token = lexer.next_token() 21 | println(token) 22 | } while (token.sym != PoParserSym.EOF) 23 | 24 | println("PARSING:") 25 | 26 | val parser = new ErrorReportingParser(new PoLexer(new StringReader(text))) 27 | val ret = parser.parse() 28 | 29 | println(ret) 30 | println(s"value = ${ret.value}") 31 | } 32 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/PoFile.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.pofile 18 | 19 | import java.io._ 20 | import java.nio.charset.StandardCharsets 21 | import java.text.SimpleDateFormat 22 | import java.util.Date 23 | 24 | import ru.makkarpov.scalingua.StringUtils 25 | import ru.makkarpov.scalingua.pofile.parse.{ErrorReportingParser, PoLexer} 26 | 27 | object PoFile { 28 | /** 29 | * This file parsing code has a few assumptions for the structure of .po file (which are always held in case when 30 | * file is generated by code in this object). 31 | * 32 | * 1. Each message has comments before the first `msgctxt` or `msgid`. 33 | * 2. Multi-line string literals are separated only by empty strings. 34 | * 3. All message entries are in following order: 35 | * * singluar: [msgctxt] msgid msgstr 36 | * * plural: [msgctxt] msgid msgid_plural msgstr[0] .. msgstr[N] 37 | * 4. After entry key there is always a string literal, e.g. no enties like "msgstr\n\"\"" 38 | * 5. Encoding of file is always UTF-8 39 | * 40 | * These assumptions helps to simplify parsing code a lot. 41 | */ 42 | 43 | val encoding = StandardCharsets.UTF_8 44 | 45 | val GeneratedPrefix = "!Generated:" 46 | private def headerComment(s: String) = s"# $GeneratedPrefix $s" 47 | 48 | def apply(f: File): Seq[Message] = apply(new FileInputStream(f), f.getName) 49 | 50 | def apply(is: InputStream, filename: String = ""): Seq[Message] = { 51 | val parser = new ErrorReportingParser(new PoLexer(new InputStreamReader(is, StandardCharsets.UTF_8), filename)) 52 | parser.parse().value.asInstanceOf[Seq[Message]] 53 | } 54 | 55 | def update(f: File, messages: Seq[Message], escapeUnicode: Boolean = true, includeHeaderComment: Boolean = true): Unit = { 56 | val output = new NewLinePrintWriter(new OutputStreamWriter(new FileOutputStream(f), encoding), false) 57 | try { 58 | if (includeHeaderComment) { 59 | output.println(headerComment(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))) 60 | output.println() 61 | } 62 | 63 | def printEntry(s: String, m: MultipartString): Unit = { 64 | output.print(s + " ") 65 | 66 | if (m.parts.isEmpty) output.println("\"\"") 67 | else for (p <- m.parts) output.println("\"" + StringUtils.escape(p, escapeUnicode) + "\"") 68 | } 69 | 70 | for (m <- messages) { 71 | for (s <- m.header.comments) 72 | output.println(s"# $s") 73 | 74 | for (s <- m.header.extractedComments) 75 | output.println(s"#. $s") 76 | 77 | for (s <- m.header.locations.sorted) 78 | if (s.line < 0) 79 | output.println(s"#: ${s.fileString}") 80 | else 81 | output.println(s"#: ${s.fileString}:${s.line}") 82 | 83 | if (m.header.flags.nonEmpty) 84 | output.println(s"#, " + m.header.flags.map(_.toString).mkString(", ")) 85 | 86 | for (t <- m.header.tag) 87 | output.println(s"#~ $t") 88 | 89 | for (c <- m.context) 90 | printEntry("msgctxt", c) 91 | 92 | printEntry("msgid", m.message) 93 | 94 | m match { 95 | case Message.Singular(_, _, _, tr) => 96 | printEntry("msgstr", tr) 97 | 98 | case Message.Plural(_, _, _, id, trs) => 99 | printEntry("msgid_plural", id) 100 | 101 | for ((m, i) <- trs.zipWithIndex) 102 | printEntry(s"msgstr[$i]", m) 103 | } 104 | 105 | output.println() 106 | } 107 | } finally output.close() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/Comment.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | case class Comment(commentTag: Char, comment: String) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/ErrorReportingParser.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import java_cup.runtime.Symbol 4 | import java_cup.runtime.ComplexSymbolFactory 5 | import java_cup.runtime.ComplexSymbolFactory.ComplexSymbol 6 | 7 | class ErrorReportingParser(lex: PoLexer) 8 | extends PoParser(lex, new ComplexSymbolFactory()) { 9 | 10 | private def report(pos: Symbol, msg: String): Unit = { 11 | if (pos.isInstanceOf[ComplexSymbol]) { 12 | throw new ParserException(pos.asInstanceOf[ComplexSymbol].xleft, 13 | pos.asInstanceOf[ComplexSymbol].xright, 14 | msg) 15 | } else { 16 | throw new RuntimeException( 17 | "Complex symbol expected for error reporting, got instead: " + 18 | pos) 19 | } 20 | } 21 | 22 | override def report_error(message: String, info: AnyRef): Unit = { 23 | report_fatal_error(message, info) 24 | } 25 | 26 | override def syntax_error(cur_token: Symbol): Unit = { 27 | unrecovered_syntax_error(cur_token) 28 | } 29 | 30 | override def unrecovered_syntax_error(cur_token: Symbol): Unit = { 31 | report(cur_token, "syntax error") 32 | } 33 | 34 | override def report_fatal_error(message: String, info: AnyRef): Unit = { 35 | if (info.isInstanceOf[ComplexSymbol]) { 36 | report(info.asInstanceOf[ComplexSymbol], message) 37 | } else { 38 | throw new RuntimeException( 39 | "Complex symbol expected for error reporting, got instead: " + 40 | info) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/LexerException.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import java_cup.runtime.ComplexSymbolFactory.Location 4 | 5 | case class LexerException(loc: Location, msg: String) 6 | extends RuntimeException(s"at ${loc.getUnit}:${loc.getLine}:${loc.getColumn}: $msg") { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/MutableHeader.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import java.io.File 4 | 5 | import java_cup.runtime.ComplexSymbolFactory.Location 6 | import ru.makkarpov.scalingua.pofile.{MessageFlag, MessageHeader, MessageLocation, PoFile} 7 | 8 | import scala.collection.mutable 9 | 10 | class MutableHeader { 11 | private var _startLoc: Location = _ 12 | private var _endLoc: Location = _ 13 | 14 | private var comments: mutable.Builder[String, Seq[String]] = _ 15 | private var extractedComments: mutable.Builder[String, Seq[String]] = _ 16 | private var locations: mutable.Builder[MessageLocation, Seq[MessageLocation]] = _ 17 | private var flags: MessageFlag.ValueSet = _ 18 | private var tag: Option[String] = _ 19 | 20 | private def parseComment(cmt: Comment, left: Location, right: Location): Unit = cmt.commentTag match { 21 | case ' ' => 22 | val str = cmt.comment.trim 23 | if (!str.startsWith(PoFile.GeneratedPrefix)) 24 | comments += str 25 | case '.' => extractedComments += cmt.comment.trim 26 | case ':' => 27 | // It seems that GNU .po utilities can combine locations in a single line: 28 | // #: some.file:123 other.file:456 29 | // but specifications does not specify how to handle spaces in a string. 30 | // So ignore there references, Scalingua itself will never produce such lines. 31 | val str = cmt.comment.trim 32 | val idx = str.lastIndexOf(':') 33 | if (idx != -1) { 34 | val file = str.substring(0, idx) 35 | val line = 36 | try str.substring(idx + 1) 37 | catch { 38 | case _: NumberFormatException => throw ParserException(left, right, "cannot parse line number") 39 | } 40 | 41 | locations += MessageLocation(new File(file), line.toInt) 42 | } else { 43 | locations += MessageLocation(new File(str), -1) 44 | } 45 | 46 | case ',' => 47 | val addFlags = cmt.comment.trim.split(",").flatMap { s => 48 | try Some(MessageFlag.withName(s.toLowerCase)) 49 | catch { case _: NoSuchElementException => None } 50 | } 51 | 52 | flags = addFlags.foldLeft(flags)(_ + _) 53 | 54 | case '~' => tag = Some(cmt.comment.trim) 55 | 56 | case _ => // ignore 57 | } 58 | 59 | def reset(): Unit = { 60 | _startLoc = null 61 | _endLoc = null 62 | 63 | comments = Vector.newBuilder 64 | extractedComments = Vector.newBuilder 65 | locations = Vector.newBuilder 66 | flags = MessageFlag.ValueSet() 67 | tag = None 68 | } 69 | 70 | def add(cmt: Comment, left: Location, right: Location): Unit = { 71 | if (_startLoc == null) { 72 | _startLoc = left 73 | } 74 | 75 | _endLoc = right 76 | parseComment(cmt, left, right) 77 | } 78 | 79 | def result(): MessageHeader = 80 | MessageHeader(comments.result(), extractedComments.result(), locations.result(), flags, tag) 81 | } 82 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/MutableMultipartString.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import ru.makkarpov.scalingua.pofile.MultipartString 4 | 5 | import scala.collection.mutable 6 | 7 | class MutableMultipartString { 8 | private var parts: mutable.Builder[String, Seq[String]] = _ 9 | 10 | reset() 11 | 12 | def reset(): Unit = parts = Seq.newBuilder[String] 13 | def add(s: String): Unit = parts += s 14 | def result: MultipartString = MultipartString(parts.result():_*) 15 | } 16 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/MutablePlurals.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import java_cup.runtime.ComplexSymbolFactory.Location 4 | 5 | import ru.makkarpov.scalingua.pofile.MultipartString 6 | 7 | class MutablePlurals { 8 | private var _startLoc: Location = _ 9 | private var _endLoc: Location = _ 10 | private var _parts: Map[Int, MultipartString] = Map.empty 11 | 12 | def reset(): Unit = { 13 | _startLoc = null 14 | _parts = Map.empty 15 | } 16 | 17 | def add(n: Int, str: MultipartString, left: Location, right: Location): Unit = { 18 | if (_parts.contains(n)) 19 | throw ParserException(left, right, s"duplicate plural message index: $n") 20 | 21 | if (_startLoc == null) 22 | _startLoc = left 23 | 24 | _endLoc = right 25 | 26 | _parts += n -> str 27 | } 28 | 29 | def result(): scala.collection.immutable.Seq[MultipartString] = { 30 | val cnt = _parts.size 31 | 32 | for (i <- 0 until cnt) 33 | if (!_parts.contains(i)) 34 | throw ParserException(_startLoc, _endLoc, s"non-contiguous indices of plural strings: index $i is absent") 35 | 36 | (0 until cnt).map(_parts) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/ParseUtils.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import scala.collection.mutable 4 | 5 | object ParseUtils { 6 | // Interfacing with Scala from Java can be very unfriendly, so here are the utility methods for it. 7 | def none[T]: Option[T] = None 8 | def some[T](x: T): Option[T] = Option(x) 9 | def newBuilder[T]: mutable.Builder[T, scala.collection.immutable.Seq[T]] = Vector.newBuilder[T] 10 | def add[T](b: mutable.Builder[T, scala.collection.immutable.Seq[T]], x: T): Unit = b += x 11 | } 12 | -------------------------------------------------------------------------------- /scalingua/shared/src/main/scala/ru/makkarpov/scalingua/pofile/parse/ParserException.scala: -------------------------------------------------------------------------------- 1 | package ru.makkarpov.scalingua.pofile.parse 2 | 3 | import java_cup.runtime.ComplexSymbolFactory.Location 4 | 5 | case class ParserException(left: Location, right: Location, msg: String) 6 | extends RuntimeException(s"at ${left.getUnit}:${left.getLine}:${left.getColumn}: $msg") { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/CustomI18nTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.{I18n, Language, Macros, OutputFormat} 22 | 23 | import scala.language.experimental.macros 24 | 25 | class CustomI18nTest extends AnyFlatSpec with Matchers { 26 | case class CStr(s: String) 27 | 28 | implicit val CStrFormat: OutputFormat[CStr] = new OutputFormat[CStr] { 29 | override def convert(s: String): CStr = CStr(s"C{$s}") 30 | override def escape(s: String): String = s"[$s]" 31 | } 32 | 33 | object CustomI18n extends I18n { 34 | def ct(msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[CStr]): CStr = 35 | macro Macros.singular[CStr] 36 | } 37 | 38 | implicit val mockLang: MockLang = new MockLang("") 39 | 40 | import CustomI18n._ 41 | 42 | it should "handle custom I18n classes via traits" in { 43 | t"Hello, world!" shouldBe "{s:Hello, world!}" 44 | } 45 | 46 | it should "handle custom methods in I18n classes" in { 47 | ct("Hello, world!").s shouldBe "C{{s:Hello, world!}}" 48 | ct("Hello, %(what)!", "what" -> "world").s shouldBe "C{{s:Hello, %(what)[[world]]!}}" 49 | 50 | """ ct("Hello, %(x)!", "y" -> 1) """ shouldNot compile 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/IStringTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.I18n._ 22 | 23 | class IStringTest extends AnyFlatSpec with Matchers { 24 | val mockLang1 = new MockLang("1") 25 | val mockLang2 = new MockLang("2") 26 | val mockLang3 = new MockLang("3") 27 | 28 | it should "handle internationalized strings when surrounding implicit lang is not present" in { 29 | val t = lt"Hello, world!" 30 | 31 | t.resolve(mockLang1) shouldBe "{s1:Hello, world!}" 32 | t.resolve(mockLang2) shouldBe "{s2:Hello, world!}" 33 | } 34 | 35 | it should "handle internationalized strings when implicit lang is present" in { 36 | implicit val lang = mockLang3 37 | 38 | val t = lt"12345" 39 | 40 | t.resolve(mockLang1) shouldBe "{s1:12345}" 41 | t.resolve(mockLang2) shouldBe "{s2:12345}" 42 | t.resolve shouldBe "{s3:12345}" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/LVInterpolationTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.I18n._ 22 | import ru.makkarpov.scalingua.LValue 23 | 24 | class LVInterpolationTest extends AnyFlatSpec with Matchers { 25 | val langLvalue = new LValue[String](l => s"L'${l.id.toString}'") 26 | 27 | it should "interpolate LValues with correct languages" in { 28 | implicit val lang = new MockLang("1") 29 | 30 | val str = "Hello" 31 | 32 | t"$str, $langLvalue" shouldBe "{s1:%(str)[Hello], %(langLvalue)[L'mock-p1']}" 33 | } 34 | 35 | it should "generate LValues with nested LValues" in { 36 | val lstr = lt"Hello, $langLvalue" 37 | 38 | lstr(new MockLang("1")) shouldBe "{s1:Hello, %(langLvalue)[L'mock-p1']}" 39 | lstr(new MockLang("2")) shouldBe "{s2:Hello, %(langLvalue)[L'mock-p2']}" 40 | 41 | val l2 = lt"$langLvalue%(a) $langLvalue%(b)" 42 | 43 | l2(new MockLang("1")) shouldBe "{s1:%(a)[L'mock-p1'] %(b)[L'mock-p1']}" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/MacroTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.I18n._ 22 | 23 | class MacroTest extends AnyFlatSpec with Matchers { 24 | implicit val mockLang: MockLang = new MockLang("") 25 | 26 | it should "handle string interpolations" in { 27 | t"Hello, world!" shouldBe "{s:Hello, world!}" 28 | 29 | val x = 10 30 | 31 | t"Number is $x" shouldBe s"{s:Number is %(x)[10]}" 32 | t"Number is $x%(y)" shouldBe s"{s:Number is %(y)[10]}" 33 | 34 | """ t"Number is ${x+10}" """ shouldNot typeCheck 35 | t"Number is ${x+10}%(y)" shouldBe s"{s:Number is %(y)[20]}" 36 | } 37 | 38 | it should "handle singular forms" in { 39 | t("Hello, world!") shouldBe "{s:Hello, world!}" 40 | 41 | """ t("Test %(x)") """ shouldNot typeCheck 42 | """ t("Test", "x" -> 20) """ shouldNot typeCheck 43 | """ t("Test %(y)", "x" -> 20) """ shouldNot typeCheck 44 | 45 | t("Test %(x)", "x" -> 20) shouldBe "{s:Test %(x)[20]}" 46 | t("Test %(x)", ("x", 30)) shouldBe "{s:Test %(x)[30]}" 47 | t("Test %(x)%(x)%(x)", "x" -> true) shouldBe "{s:Test %(x)[true]%(x)[true]%(x)[true]}" 48 | 49 | """ val s = "1"; t(s) """ shouldNot typeCheck 50 | """ val s = "x"; t("1", s -> 1) """ shouldNot typeCheck 51 | 52 | { val i = 2; t("1%(1)3", "1" -> i ) } shouldBe "{s:1%(1)[2]3}" 53 | } 54 | 55 | it should "handle contextual singular forms" in { 56 | tc("test", "Hello, world!") shouldBe "{sc:test:Hello, world!}" 57 | 58 | """ tc("1", "%(x)") """ shouldNot typeCheck 59 | """ tc("1", "2", "3" -> 4) """ shouldNot typeCheck 60 | """ tc("1", "%(2)", "3" -> 4) """ shouldNot typeCheck 61 | 62 | tc("1", "%(2)", "2" -> 3) shouldBe "{sc:1:%(2)[3]}" 63 | tc("1", "%(2)%(3)", "2" -> 4, "3" -> 5) shouldBe "{sc:1:%(2)[4]%(3)[5]}" 64 | 65 | """ val s = "1"; tc(s, "1") """ shouldNot typeCheck 66 | """ val s = "2"; tc("1", s) """ shouldNot typeCheck 67 | """ val s = "3"; tc("1", "2", s -> 4) """ shouldNot typeCheck 68 | 69 | { val i = 3; tc("1", "%(2)", "2" -> i) } shouldBe "{sc:1:%(2)[3]}" 70 | } 71 | 72 | it should "handle plural forms" in { 73 | p("Hello, %(n) world!", "Hello, %(n) worlds!", 1) shouldBe "{p:Hello, %(n)[1] world!:Hello, %(n)[1] worlds!:1}" 74 | 75 | // %(n) should be present in string 76 | """ p("Test", "Tests", 1) """ shouldNot typeCheck 77 | 78 | val n = 10 + 20 79 | p("%(n)", "%(n)s", n) shouldBe "{p:%(n)[30]:%(n)[30]s:30}" 80 | 81 | """ p("1%(n)", "2", 3) """ shouldNot typeCheck 82 | """ p("1", "2%(n)", 3) """ shouldNot typeCheck 83 | """ p("1%(n)%(x)", "2%(n)", 3) """ shouldNot typeCheck 84 | """ p("1%(n)", "2%(n)%(x)", 3) """ shouldNot typeCheck 85 | 86 | p("%(n)s%(x)", "%(x)p%(n)", 3, "x" -> 2) shouldBe "{p:%(n)[3]s%(x)[2]:%(x)[2]p%(n)[3]:3}" 87 | 88 | """ p("1%(n)%(x)", "2%(n)%(y)", 3, "x" -> 1, "y" -> 2) """ shouldNot typeCheck 89 | """ val s = "%(n)"; p(s, "%(n)", 3) """ shouldNot typeCheck 90 | """ val s = "%(n)"; p("%(n)", s, 3) """ shouldNot typeCheck 91 | } 92 | 93 | it should "handle contextual plural forms" in { 94 | pc("1", "2%(n)", "%(n)3", 4) shouldBe "{pc:1:2%(n)[4]:%(n)[4]3:4}" 95 | 96 | """ pc("0", "1", "2%(n)", 3) """ shouldNot typeCheck 97 | 98 | """ pc("0", "1%(n)", "2", 3) """ shouldNot typeCheck 99 | """ pc("0", "1", "2%(n)", 3) """ shouldNot typeCheck 100 | """ pc("0", "1%(n)%(x)", "2%(n)", 3) """ shouldNot typeCheck 101 | """ pc("0", "1%(n)", "2%(n)%(x)", 3) """ shouldNot typeCheck 102 | 103 | pc("1", "%(x)%(y)%(n)%(x)", "%(y)%(n)%(x)%(y)", 1, "x" -> 2, "y" -> 3) shouldBe 104 | "{pc:1:%(x)[2]%(y)[3]%(n)[1]%(x)[2]:%(y)[3]%(n)[1]%(x)[2]%(y)[3]:1}" 105 | 106 | """ val s = "1"; pc(s, "%(n)", "%(n)", 1) """ shouldNot typeCheck 107 | """ val s = "%(n)"; pc("1", s, "%(n)", 1) """ shouldNot typeCheck 108 | } 109 | 110 | it should "handle multiline strings" in { 111 | t"""1 112 | 2 113 | 3""" shouldBe "{s:1\n2\n3}" 114 | 115 | t"""1 116 | |2""" shouldBe "{s:1\n |2}" 117 | 118 | t("""1 119 | 2 120 | 3""") shouldBe "{s:1\n2\n3}" 121 | 122 | 123 | t("""1 124 | |2 125 | |3""".stripMargin) shouldBe "{s:1\n2\n3}" 126 | 127 | 128 | t("""1 129 | #2 130 | #3 131 | """.stripMargin('#')) shouldBe "{s:1\n2\n3}" 132 | 133 | t("""1 134 | |2 135 | """.stripMargin('#')) shouldBe "{s:1\n |2}" 136 | 137 | t("1".stripMargin('1')) shouldBe "{s:}" 138 | 139 | """ val c = '1'; t("1".stripMargin(c)) """ shouldNot typeCheck 140 | """ val s = "1"; t(s.stripMargin('1')) """ shouldNot typeCheck 141 | } 142 | 143 | it should "handle plural strings" in { 144 | val n = 10 145 | val k = 5L 146 | val s = "black" 147 | 148 | p"I have $n fox${S.es}" shouldBe "{p:I have %(n)[10] fox:I have %(n)[10] foxes:10}" 149 | p"I have $k $s cat${S.s}" shouldBe "{p:I have %(k)[5] %(s)[black] cat:I have %(k)[5] %(s)[black] cats:5}" 150 | p"Msg $n" shouldBe "{p:Msg %(n)[10]:Msg %(n)[10]:10}" 151 | 152 | // Multiple integer variables, each could be a plural number: 153 | """ p"test $n $n" """ shouldNot typeCheck 154 | """ p"$k test $n" """ shouldNot typeCheck 155 | 156 | // Multiple `.nVar` candidates: 157 | """ p"${n.nVar} ${k.nVar}" """ shouldNot typeCheck 158 | """ p"${n.nVar} ${n.nVar}" """ shouldNot typeCheck 159 | 160 | // No `nVar`s: 161 | """ p"Simple string" """ shouldNot typeCheck 162 | """ p"$s str" """ shouldNot typeCheck 163 | """ p"${S.s}${S.es}" """ shouldNot typeCheck 164 | 165 | // Many many many suffixes: 166 | p"${S.s}${S.s}${S.es}${S.es}${S.s}${S.es}$n" shouldBe "{p:%(n)[10]:ssesesses%(n)[10]:10}" 167 | 168 | // Adjust suffix to case: 169 | p"I HAVE $n CAT${S.s}" shouldBe "{p:I HAVE %(n)[10] CAT:I HAVE %(n)[10] CATS:10}" 170 | p"$n CAT${S.s} cat${S.s} fox${S.es} FOX${S.es}" shouldBe 171 | "{p:%(n)[10] CAT cat fox FOX:%(n)[10] CATS cats foxes FOXES:10}" 172 | 173 | // `a &> b` operator: 174 | p"There ${"is" &> "are"} $n pen${S.s}" shouldBe "{p:There is %(n)[10] pen:There are %(n)[10] pens:10}" 175 | """ p"${p &> "1"}" """ shouldNot typeCheck 176 | """ p"${"1" &> p}" """ shouldNot typeCheck 177 | 178 | p"${S.s}${"" &> ""}${"x" &> ""}${"" &> "y"}${S.es}${"z" &> "w"}$n" shouldBe "{p:xz%(n)[10]:syesw%(n)[10]:10}" 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/MockLang.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import ru.makkarpov.scalingua.{Language, LanguageId} 20 | 21 | class MockLang(s: String) extends Language { 22 | override def id: LanguageId = LanguageId("mock", "p" + s) 23 | override def singular(msgid: String): String = 24 | parts("s", msgid) 25 | override def singular(msgctx: String, msgid: String): String = 26 | parts("sc", msgctx, msgid) 27 | override def plural(msgid: String, msgidPlural: String, n: Long): String = 28 | parts("p", msgid, msgidPlural, n) 29 | override def plural(msgctx: String, msgid: String, msgidPlural: String, n: Long): String = 30 | parts("pc", msgctx, msgid, msgidPlural, n) 31 | 32 | private def parts(code: String, args: Any*): String = { 33 | s"{$code$s:" + args.map(_.toString.replaceAll("%\\(([^)]+)\\)", "%%($1)[%($1)]")).mkString(":") + "}" 34 | } 35 | 36 | override def merge(other: Language): Language = throw new NotImplementedError("MockLang.merge") 37 | 38 | override def taggedSingular(tag: String): String = parts("ts", tag) 39 | override def taggedPlural(tag: String, n: Long): String = parts("tp", tag, n) 40 | } -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/ParserTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.plural.Expression 22 | 23 | class ParserTest extends AnyFlatSpec with Matchers { 24 | import ru.makkarpov.scalingua.plural.Parser.{apply => p} 25 | 26 | it should "evaluate simple expressions" in { 27 | p("0").eval(0) shouldBe 0 28 | p("1").eval(0) shouldBe 1 29 | p("0").eval(1) shouldBe 0 30 | p("3").eval(999) shouldBe 3 31 | p("n;").eval(0) shouldBe 0 32 | p("n;").eval(10) shouldBe 10 33 | } 34 | 35 | it should "evaluate simple unary expresions" in { 36 | p("-0").eval(0) shouldBe 0 37 | p("~0").eval(0) shouldBe (~0L) 38 | p("+0").eval(0) shouldBe 0 39 | p("!2").eval(0) shouldBe 0 40 | p("!0").eval(0) shouldBe 1 41 | p("-n").eval(10) shouldBe -10 42 | } 43 | 44 | it should "evaluate expressions with operators of single precedence" in { 45 | p("2+2+2+2+2").eval(0) shouldBe 10 46 | p("2-2+2-2+2").eval(0) shouldBe 2 47 | p("2*2*2*2*2").eval(0) shouldBe 32 48 | p("2/2*2/2*2").eval(0) shouldBe 2 49 | p("1&1&1&1&1").eval(0) shouldBe 1 50 | p("1&0&1&1&1").eval(0) shouldBe 0 51 | } 52 | 53 | it should "evaluate expressions with operators of mixed precedence" in { 54 | p("2*2+2/2-2").eval(0) shouldBe 3 55 | p("2*2+1&2-2").eval(0) shouldBe 0 56 | p("1*3<<1==n").eval(0) shouldBe 0 57 | p("1*3<<1==n").eval(6) shouldBe 1 58 | p("-1 + -3").eval(0) shouldBe -4 59 | p("1+ +3").eval(0) shouldBe 4 60 | } 61 | 62 | it should "evaluate expressions with parentheses" in { 63 | p("(1)").eval(0) shouldBe 1 64 | p("(((1)))").eval(0) shouldBe 1 65 | p("(2+2)*(2+2)+2").eval(0) shouldBe 18 66 | p("((2 == 2)+1)*3").eval(0) shouldBe 6 67 | } 68 | 69 | it should "evaluate ternary operator" in { 70 | p("n ? 1 : 4").eval(0) shouldBe 4 71 | p("n ? 1 : 4").eval(2) shouldBe 1 72 | p("n ? 1 : 4").eval(-1) shouldBe 1 73 | p("0 ? 1 : 0 ? 2 : 0 ? 3 : 4").eval(0) shouldBe 4 74 | p("(n ? 0 : 1) ? 2 : 3").eval(0) shouldBe 2 75 | p("(n ? 0 : 1) ? 2 : 3").eval(1) shouldBe 3 76 | } 77 | 78 | it should "discard invalid expressions" in { 79 | def err(f: => Expression): Unit = an [IllegalArgumentException] shouldBe thrownBy(f) 80 | 81 | err { p("") } 82 | err { p("+") } 83 | err { p("1 +") } 84 | err { p("1 1") } 85 | err { p("n n") } 86 | err { p("n 1") } 87 | err { p("1 + + + 4") } 88 | err { p("(1") } 89 | err { p(")") } 90 | err { p(")(") } 91 | err { p("n ? 1") } 92 | err { p("n : 1") } 93 | err { p("n : 1 ? 2") } 94 | err { p("n ? 1 : n ? 2") } 95 | err { p("n;1")} 96 | } 97 | 98 | it should "evaluate real-world examples" in { 99 | // Russian: 100 | val r = p("(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);") 101 | 102 | r.eval(0) shouldBe 2 103 | r.eval(1) shouldBe 0 104 | r.eval(2) shouldBe 1 105 | r.eval(3) shouldBe 1 106 | r.eval(5) shouldBe 2 107 | r.eval(9) shouldBe 2 108 | r.eval(11) shouldBe 2 109 | r.eval(13) shouldBe 2 110 | r.eval(21) shouldBe 0 111 | r.eval(23) shouldBe 1 112 | r.eval(25) shouldBe 2 113 | r.eval(50) shouldBe 2 114 | r.eval(51) shouldBe 0 115 | r.eval(111) shouldBe 2 116 | } 117 | 118 | it should "compile expressions" in { 119 | p("1").scalaExpression shouldBe "1L" 120 | p("n").scalaExpression shouldBe "arg" 121 | p("2+2").scalaExpression shouldBe "(2L)+(2L)" 122 | p("(n==2)").scalaExpression shouldBe "if ((arg)==(2L)) 1 else 0" 123 | p("(n==2)+1").scalaExpression shouldBe "(if ((arg)==(2L)) 1 else 0)+(1L)" 124 | p("n==2?3:10").scalaExpression shouldBe "if ((arg)==(2L)) (3L) else (10L)" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /scalingua/shared/src/test/scala/ru/makkarpov/scalingua/test/StringUtilsTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.OutputFormat 22 | 23 | class StringUtilsTest extends AnyFlatSpec with Matchers { 24 | it should "escape strings" in { 25 | import ru.makkarpov.scalingua.StringUtils.{escape => f} 26 | 27 | // Samples where no escape is required: 28 | f("") shouldBe "" 29 | f("a string") shouldBe "a string" 30 | f("привет, мир!") shouldBe "привет, мир!" 31 | 32 | // Samples with simple one-letter escapes: 33 | f("\n") shouldBe "\\n" 34 | f("\\") shouldBe "\\\\" 35 | f("a string with \r, \n, \b, \f, \t, \', \", \\") shouldBe "a string with \\r, \\n, \\b, \\f, \\t, \', \\\", \\\\" 36 | 37 | // Samples with unicode escapes: 38 | f("⚡ high voltage ⚡") shouldBe "\\u26A1 high voltage \\u26A1" 39 | f("look! A cat: \uD83D\uDC31") shouldBe "look! A cat: \\uD83D\\uDC31" 40 | } 41 | 42 | it should "unescape strings" in { 43 | import ru.makkarpov.scalingua.StringUtils.{unescape => f} 44 | 45 | // Samples where string is treated literally: 46 | f("") shouldBe "" 47 | f("a string") shouldBe "a string" 48 | f("привет, мир!") shouldBe "привет, мир!" 49 | 50 | // Samples with simple one-letter escapes: 51 | f("\\n") shouldBe "\n" 52 | f("\\\\") shouldBe "\\" 53 | f("a string with \\r, \\n, \\b, \\f, \\t, \\', \\\", \\\\") shouldBe "a string with \r, \n, \b, \f, \t, \', \", \\" 54 | 55 | // Samples with unicode escapes: 56 | f("\\u26A1 high voltage \\u26A1") shouldBe "⚡ high voltage ⚡" 57 | f("look! A cat: \\uD83D\\uDC31") shouldBe "look! A cat: \uD83D\uDC31" 58 | f("\\u0000") shouldBe "\u0000" 59 | 60 | // Samples with invalid escapes: 61 | an [IllegalArgumentException] shouldBe thrownBy { f("\\x") } 62 | an [IllegalArgumentException] shouldBe thrownBy { f("\\") } 63 | an [IllegalArgumentException] shouldBe thrownBy { f("\\u") } 64 | an [IllegalArgumentException] shouldBe thrownBy { f("\\u000") } 65 | an [IllegalArgumentException] shouldBe thrownBy { f("\\uXXXX") } 66 | } 67 | 68 | it should "interpolate strings" in { 69 | import ru.makkarpov.scalingua.StringUtils.{interpolate => f} 70 | 71 | // Samples with literal strings: 72 | f[String]("") shouldBe "" 73 | f[String]("a string") shouldBe "a string" 74 | 75 | // Samples with escaped interpolation symbols: 76 | f[String]("%%") shouldBe "%" 77 | f[String]("%%%%") shouldBe "%%" 78 | f[String]("%%(notvar)") shouldBe "%(notvar)" 79 | f[String]("%%(test)%%") shouldBe "%(test)%" 80 | 81 | // Samples with interpolations: 82 | f[String]("%(a)", "a" -> "a") shouldBe "a" 83 | f[String]("%(a)%(a)%(a)", "a" -> "xy") shouldBe "xyxyxy" 84 | f[String]("%%%(a)%%%%(x)", "a" -> "zwx") shouldBe "%zwx%%(x)" 85 | f[String]("%(a)%(b)%(c)", "a" -> "1", "b" -> 2, "c" -> 3L) shouldBe "123" 86 | 87 | // Samples with invalid interpolations: 88 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%()", "" -> "") } 89 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%(") } 90 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%") } 91 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%(undefined)") } 92 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%[]") } 93 | an [IllegalArgumentException] shouldBe thrownBy { f[String]("%(a)%(b)", "a" -> true) } 94 | 95 | // Custom format interpolations: 96 | case class CStr(s: String) 97 | implicit val CStrFormat = new OutputFormat[CStr] { 98 | override def convert(s: String): CStr = CStr("{" + s + "}") 99 | override def escape(s: String): String = "[" + s + "]" 100 | } 101 | 102 | f[CStr]("a%(a)a%(a)a", "a" -> "a").s shouldBe "{a[a]a[a]a}" 103 | } 104 | 105 | it should "extract variables from strings" in { 106 | import ru.makkarpov.scalingua.StringUtils.{extractVariables => f} 107 | 108 | // Literal strings 109 | f("") shouldBe Set.empty 110 | f("a string") shouldBe Set.empty 111 | 112 | // Escaped placeholders, but still literal: 113 | f("%%") shouldBe Set.empty 114 | f("%%%%") shouldBe Set.empty 115 | f("%%(notvar)") shouldBe Set.empty 116 | f("%%(xx)%%") shouldBe Set.empty 117 | 118 | // With variables: 119 | f("%(a)") shouldBe Set("a") 120 | f("%(a)%(a)%(a)") shouldBe Set("a") 121 | f("%%%(a)%%%%(x)") shouldBe Set("a") 122 | f("%(a)%(b)%(c)") shouldBe Set("a", "b", "c") 123 | 124 | // Invalid: 125 | an [IllegalArgumentException] shouldBe thrownBy { f("%()") } 126 | an [IllegalArgumentException] shouldBe thrownBy { f("%(") } 127 | an [IllegalArgumentException] shouldBe thrownBy { f("%") } 128 | an [IllegalArgumentException] shouldBe thrownBy { f("%[]") } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /twirl/shared/src/main/scala/ru/makkarpov/scalingua/twirl/I18n.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.twirl 18 | 19 | import play.twirl.api.Html 20 | import ru.makkarpov.scalingua 21 | import ru.makkarpov.scalingua._ 22 | 23 | import scala.language.experimental.macros 24 | import scala.language.implicitConversions 25 | 26 | trait I18n extends scalingua.I18n { 27 | type LHtml = LValue[Html] 28 | 29 | implicit val htmlOutputFormat: OutputFormat[Html] = new OutputFormat[Html] { 30 | override def convert(s: String): Html = Html(s) 31 | 32 | override def escape(s: String): String = { 33 | val ret = new StringBuilder 34 | var i = 0 35 | ret.sizeHint(s.length) 36 | while (i < s.length) { 37 | s.charAt(i) match { 38 | case '<' => ret.append("<") 39 | case '>' => ret.append(">") 40 | case '"' => ret.append(""") 41 | case '\'' => ret.append("'") 42 | case '&' => ret.append("&") 43 | case c => ret += c 44 | } 45 | i += 1 46 | } 47 | ret.result() 48 | } 49 | } 50 | 51 | implicit def stringContext2Interpolator1(sc: StringContext): PlayUtils.StringInterpolator = 52 | new PlayUtils.StringInterpolator(sc) 53 | 54 | def th(msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 55 | macro Macros.singular[Html] 56 | 57 | def lth(msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[Html]): LHtml = 58 | macro Macros.lazySingular[Html] 59 | 60 | def tch(ctx: String, msg: String, args: (String, Any)*)(implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 61 | macro Macros.singularCtx[Html] 62 | 63 | def ltch(ctx: String, msg: String, args: (String, Any)*)(implicit outputFormat: OutputFormat[Html]): LHtml = 64 | macro Macros.lazySingularCtx[Html] 65 | 66 | def p(msg: String, msgPlural: String, n: Long, args: (String, Any)*) 67 | (implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 68 | macro Macros.plural[Html] 69 | 70 | def lp(msg: String, msgPlural: String, n: Long, args: (String, Any)*) 71 | (implicit outputFormat: OutputFormat[Html]): LHtml = 72 | macro Macros.lazyPlural[Html] 73 | 74 | def pc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) 75 | (implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 76 | macro Macros.pluralCtx[Html] 77 | 78 | def lpc(ctx: String, msg: String, msgPlural: String, n: Long, args: (String, Any)*) 79 | (implicit outputFormat: OutputFormat[Html]): LHtml = 80 | macro Macros.lazyPluralCtx[Html] 81 | } 82 | 83 | object I18n extends I18n -------------------------------------------------------------------------------- /twirl/shared/src/main/scala/ru/makkarpov/scalingua/twirl/PlayUtils.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.twirl 18 | 19 | import play.twirl.api.Html 20 | import ru.makkarpov.scalingua._ 21 | 22 | import scala.language.experimental.macros 23 | 24 | object PlayUtils { 25 | class StringInterpolator(val sc: StringContext) extends AnyVal { 26 | def h(args: Any*)(implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 27 | macro Macros.interpolate[Html] 28 | 29 | def lh(args: Any*)(implicit outputFormat: OutputFormat[Html]): LValue[Html] = 30 | macro Macros.lazyInterpolate[Html] 31 | 32 | def ph(args: Any*)(implicit lang: Language, outputFormat: OutputFormat[Html]): Html = 33 | macro Macros.pluralInterpolate[Html] 34 | 35 | def lph(args: Any*)(outputFormat: OutputFormat[Html]): LValue[Html] = 36 | macro Macros.lazyPluralInterpolate[Html] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /twirl/shared/src/test/scala/ru/makkarpov/scalingua/twirl/test/PlayTest.scala: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Copyright © 2016 Maxim Karpov * 3 | * * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); * 5 | * you may not use this file except in compliance with the License. * 6 | * You may obtain a copy of the License at * 7 | * * 8 | * http://www.apache.org/licenses/LICENSE-2.0 * 9 | * * 10 | * Unless required by applicable law or agreed to in writing, software * 11 | * distributed under the License is distributed on an "AS IS" BASIS, * 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 13 | * See the License for the specific language governing permissions and * 14 | * limitations under the License. * 15 | ******************************************************************************/ 16 | 17 | package ru.makkarpov.scalingua.twirl.test 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import org.scalatest.matchers.should.Matchers 21 | import ru.makkarpov.scalingua.{Language, Messages, TaggedLanguage} 22 | import ru.makkarpov.scalingua.twirl.I18n._ 23 | 24 | class PlayTest extends AnyFlatSpec with Matchers { 25 | it should "handle HTML translations" in { 26 | implicit val lang = Language.English 27 | val x = "\"List\"" 28 | h"A class $x can be used to provide simple list container".body shouldBe 29 | "A class "List<String>" can be used to provide simple list container" 30 | } 31 | } 32 | --------------------------------------------------------------------------------