Last active
May 11, 2021 14:45
-
-
Save tg44/a7829d3877fab0a059e6675c6231f494 to your computer and use it in GitHub Desktop.
Crowdin CDN from Play framework
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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