├── .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 [](https://travis-ci.org/makkarpov/scalingua) [](http://search.maven.org/#search%7Cga%7C1%7Cscalingua%20AND%20g%3A%22ru.makkarpov%22) [](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 |
--------------------------------------------------------------------------------