Skip to content

Instantly share code, notes, and snippets.

@tg44
Last active May 11, 2021 14:45
Show Gist options
  • Save tg44/a7829d3877fab0a059e6675c6231f494 to your computer and use it in GitHub Desktop.
Save tg44/a7829d3877fab0a059e6675c6231f494 to your computer and use it in GitHub Desktop.
Crowdin CDN from Play framework
play {
i18n.langs: [ "en", "hu", "de", "sr", "id" ]
i18n.crowdin.distributionHash: "xxxxxxx"
i18n.crowdin.defaultLang: "en"
modules.enabled += "AppStartModule"
modules.disabled += "play.api.i18n.I18nModule"
}
import play.api.inject.Module
import helpers.{MessagesApiWithCrowdin, MessagesApiWithCrowdinProvider}
import play.api.i18n.{DefaultLangsProvider, Langs, MessagesApi}
import play.api.inject.Binding
import play.api._
//possibly some imports are missing
class AppStartModule extends Module {
override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = {
Seq(
//disabled and overrided play.api.i18n.I18nModule
bind[Langs].toProvider[DefaultLangsProvider],
bind[MessagesApi].toProvider[MessagesApiWithCrowdinProvider],
)
}
}
package helpers
import akka.stream.Materializer
import akka.stream.scaladsl.Sink
import akka.util.ByteString
import helpers.implicits.StreamsImplicits.SeqHelper
import org.slf4j.LoggerFactory
import play.api.http.HttpConfiguration
import play.api.{Configuration, Environment}
import play.api.i18n.{DefaultMessagesApi, Lang, Langs, Messages, MessagesApi}
import play.api.libs.ws.WSClient
import play.api.mvc.{Cookie, RequestHeader, Result}
import play.mvc.Http
import play.utils.Resources
import java.io.File
import java.nio.file.Path
import javax.inject.{Inject, Provider, Singleton}
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success}
@Singleton
class MessagesApiWithCrowdinProvider @Inject() (
messagesApiWithCrowdin: MessagesApiWithCrowdin
)(
implicit materializer: Materializer,
executionContext: ExecutionContext,
) extends Provider[MessagesApiWithCrowdin] {
override lazy val get: MessagesApiWithCrowdin = {
messagesApiWithCrowdin
}
}
@Singleton
class MessagesApiWithCrowdin @Inject() (
environment: Environment,
config: Configuration,
langs: Langs,
httpConfiguration: HttpConfiguration,
wsClient: WSClient,
)(
implicit materializer: Materializer,
executionContext: ExecutionContext,
) extends MessagesApi {
private val baseCrowdinUrl = "https://distributions.crowdin.net/"
private val logger = LoggerFactory.getLogger(this.getClass)
logger.info("New MessagesApiWithCrowdin initialized!")
def redownloadAndReload() = {
val defaultLang = config.get[String]("play.i18n.crowdin.defaultLang")
val dir = "tmp/messages/"
val directory = new File(dir)
if(!directory.exists()) {
directory.mkdirs();
}
langs.availables.traverseWithStreams(2) { lang =>
val toFile = new File(s"${dir}messages.${lang.code}")
wsClient
.url(
baseCrowdinUrl + config.get[String]("play.i18n.crowdin.distributionHash") + "/content/messages." + lang.code
)
.withMethod("GET")
.stream()
.flatMap { res =>
val outputStream = java.nio.file.Files.newOutputStream(toFile.toPath)
// The sink that writes to the output stream
val sink = Sink.foreach[ByteString] { bytes =>
outputStream.write(bytes.toArray)
}
// materialize and run the stream
res.bodyAsSource
.runWith(sink)
.andThen {
case result =>
// Close the output stream whether there was an error or not
outputStream.close()
// Get the result or rethrow the error
result.get
}
.map(_ => lang -> Some(toFile))
}.recover { case ex =>
logger.error(s"Language download error ${lang.code}", ex)
lang -> None
}
}.map { d =>
val langMap = d.map { case (lang, newFile) =>
val code = lang.code
val compiledMessages = loadMessages(s"messages.${code}")
val remoteMessages = newFile.fold(Map.empty[String, String]) { newFile =>
Messages.parse(Messages.UrlMessageSource(newFile.toURI.toURL), newFile.toString).fold(e => throw e, identity)
}
if(code == defaultLang) {
Map(
code -> (compiledMessages ++ remoteMessages),
"default" -> (compiledMessages ++ remoteMessages),
"default.play" -> (compiledMessages ++ remoteMessages),
)
} else {
Map(code -> (compiledMessages ++ remoteMessages))
}
}.foldLeft(Map.empty[String, Map[String, String]]) {
_ ++ _
}
messagesApi = new DefaultMessagesApi(
langMap,
langs,
langCookieName = langCookieName,
langCookieSecure = langCookieSecure,
langCookieHttpOnly = langCookieHttpOnly,
langCookieSameSite = langCookieSameSite,
httpConfiguration = httpConfiguration,
)
}.onComplete {
case Success(_) => logger.info("Language reload successful")
case Failure(ex) => logger.error("Language reload error", ex)
}
}
private var messagesApi = new DefaultMessagesApi(
loadAllMessages,
langs,
langCookieName = config.getDeprecated[String]("play.i18n.langCookieName", "application.lang.cookie"),
langCookieSecure = config.get[Boolean]("play.i18n.langCookieSecure"),
langCookieHttpOnly = config.get[Boolean]("play.i18n.langCookieHttpOnly"),
langCookieSameSite = HttpConfiguration.parseSameSite(config, "play.i18n.langCookieSameSite"),
httpConfiguration = httpConfiguration,
)
protected def loadAllMessages: Map[String, Map[String, String]] = {
(langs.availables.iterator.map { lang =>
val code = lang.code
code -> loadMessages(s"messages.${code}")
}.toMap: Map[String, Map[String, String]])
.+("default" -> loadMessages("messages")) + ("default.play" -> loadMessages("messages.default"))
}
protected def loadMessages(file: String): Map[String, String] = {
import scala.collection.JavaConverters._
environment.classLoader
.getResources(joinPaths(messagesPrefix, file))
.asScala
.toList
.filterNot(url => Resources.isDirectory(environment.classLoader, url))
.reverse
.map { messageFile =>
Messages.parse(Messages.UrlMessageSource(messageFile), messageFile.toString).fold(e => throw e, identity)
}
.foldLeft(Map.empty[String, String]) {
_ ++ _
}
}
protected def messagesPrefix = config.getDeprecated[Option[String]]("play.i18n.path", "messages.path")
protected def joinPaths(first: Option[String], second: String) =
first match {
case Some(parent) => new java.io.File(parent, second).getPath
case None => second
}
override def messages: Map[String, Map[String, String]] = messagesApi.messages
override def preferred(candidates: Seq[Lang]): Messages = messagesApi.preferred(candidates)
override def preferred(request: RequestHeader): Messages = messagesApi.preferred(request)
override def preferred(request: Http.RequestHeader): Messages = messagesApi.preferred(request)
override def apply(key: String, args: Any*)(implicit lang: Lang): String = messagesApi.apply(key, args: _*)
override def apply(keys: Seq[String], args: Any*)(implicit lang: Lang): String = messagesApi.apply(keys, args: _*)
override def translate(key: String, args: Seq[Any])(implicit lang: Lang): Option[String] =
messagesApi.translate(key, args)
override def isDefinedAt(key: String)(implicit lang: Lang): Boolean = messagesApi.isDefinedAt(key)
override def setLang(result: Result, lang: Lang): Result = messagesApi.setLang(result, lang)
override def clearLang(result: Result): Result = messagesApi.clearLang(result)
override def langCookieName: String = messagesApi.langCookieName
override def langCookieSecure: Boolean = messagesApi.langCookieSecure
override def langCookieHttpOnly: Boolean = messagesApi.langCookieHttpOnly
override def langCookieSameSite: Option[Cookie.SameSite] = messagesApi.langCookieSameSite
}
package helpers.implicits
import akka.stream.Materializer
import akka.stream.scaladsl.{Sink, Source}
import scala.concurrent.Future
object StreamsImplicits {
implicit class SeqHelper[A](s: Seq[A]) {
def traverseWithStreams[B](parallelism: Int)(f: A => Future[B])(implicit
materializer: Materializer): Future[Seq[B]] = {
if(s.nonEmpty) {
Source(s.toList).mapAsync(parallelism)(f).runWith(Sink.seq)
} else {
Future.successful(Seq.empty[B])
}
}
def traverseWithStreamsUnordered[B](parallelism: Int)(f: A => Future[B])(implicit
materializer: Materializer): Future[Seq[B]] = {
Source(s.toList).mapAsyncUnordered(parallelism)(f).runWith(Sink.seq)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment