From 2cbc75a2cab718a9326ef710f7e0829a1f427c90 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 19 Apr 2024 14:14:09 +0800 Subject: [PATCH] Language Tree and Translations Def, scala & sbt version update. --- build.sbt | 2 +- project/build.properties | 2 +- .../resources/assets_morny/langs/_index.hyl | 30 +++ .../resources/assets_morny/langs/en_us.hyt | 53 +++++ .../resources/assets_morny/langs/zh_cn.hyt | 11 + .../sukazyo/cono/morny/core/MornyAbout.scala | 2 +- .../sukazyo/cono/morny/core/MornyCoeur.scala | 2 +- .../morny/{core => data}/MornyAssets.scala | 2 +- .../cono/morny/data/TelegramImages.scala | 3 +- .../cono/morny/util/hytrans/Definitions.scala | 14 ++ .../cono/morny/util/hytrans/LangTag.scala | 33 +++ .../morny/util/hytrans/LanguageTree.scala | 188 ++++++++++++++++++ .../cono/morny/util/hytrans/Parser.scala | 41 ++++ .../morny/util/hytrans/Translations.scala | 45 +++++ 14 files changed, 421 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/assets_morny/langs/_index.hyl create mode 100644 src/main/resources/assets_morny/langs/en_us.hyt create mode 100644 src/main/resources/assets_morny/langs/zh_cn.hyt rename src/main/scala/cc/sukazyo/cono/morny/{core => data}/MornyAssets.scala (88%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Definitions.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LangTag.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LanguageTree.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Parser.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Translations.scala diff --git a/build.sbt b/build.sbt index 87ec014..1fe9e47 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ aether.AetherKeys.aetherOldVersionMethod := true ThisBuild / organization := "cc.sukazyo" ThisBuild / organizationName := "A.C. Sukazyo Eyre" -ThisBuild / scalaVersion := "3.4.0-RC4" +ThisBuild / scalaVersion := "3.4.1" resolvers ++= Seq( "-ws-releases" at "https://mvn.sukazyo.cc/releases" diff --git a/project/build.properties b/project/build.properties index 3040987..04267b1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.4 +sbt.version=1.9.9 diff --git a/src/main/resources/assets_morny/langs/_index.hyl b/src/main/resources/assets_morny/langs/_index.hyl new file mode 100644 index 0000000..5eb07b6 --- /dev/null +++ b/src/main/resources/assets_morny/langs/_index.hyl @@ -0,0 +1,30 @@ +en, 2 000 000 000 + en_us, 328 000 000 + en_gb, 66 300 000 + en_sg, 5 000 000 + en_mt, 1 + en_ph, 100 000 000 + en_nz, 5 000 000 + en_za, 5 000 000 + en_au, 25 000 000 + en_ie, 5 000 000 + en_ca, 25 000 000 + en_in, 125 000 000 +zh, 1 000 000 000 + zh_hans, 1 400 000 000 + zh_cn, 1 300 000 000 + zh_sg, 5 000 000 + zh_mo, 2 000 000 + zh_hant, 50 000 000 + zh_tw, 23 000 000 + zh_hk, 7 000 000 + zh_ancient, 10 +ja, 125 000 000 + ja_jp, 10 000 000 +fr, 338 000 000 + fr_fr, 67 000 000 + fr_ch, 8 000 000 + fr_be, 11 000 000 + fr_ca, 7 000 000 + fr_lu, 1 +mt_mt, 520 000 diff --git a/src/main/resources/assets_morny/langs/en_us.hyt b/src/main/resources/assets_morny/langs/en_us.hyt new file mode 100644 index 0000000..3382989 --- /dev/null +++ b/src/main/resources/assets_morny/langs/en_us.hyt @@ -0,0 +1,53 @@ + Morny Translations File + +%1.0 +&encoding=utf8 +&indent=1 + +morny.command.info.message.about +| Morny Cono +| An assistant rat(micromys minutus) from Annie。 +| ———————————————— +| {about_links} + +morny.command.info.version +| version: +| - Morny {version_codename} +| - {version_base}{version_delta_html}{version_git_suffix} +| coeur md5_hash: +| - {md5} +| coding timestamp: +| - {time_millis} +| - {time_utc} [UTC] + +morny.command.info.message.runtime +| system: +| - {hostname} +| - {os.name} {os.arch} {os.version} +| java runtime: +| - {java.vm.vendor}.{java.vm.name} +| - {java.vm.version} +| vm memory: +| - {memory_used_mb} / {memory_available_mb} MB +| - {cpu_cores} cores +| coeur version: +| - {version_full} +| - {coeur_md5} +| - {compile_time_utc} [UTC] +| - [{compile_time_millis}] +| continuous: +| - {running_duration} +| - [{running_duration_ms}] +| - {startup_time_utc} +| - [{startup_time_millis}] + +morny.command.info.message.tasks +| Coeur Task Scheduler: +| - scheduled tasks: {coeur.tasks.amount} +| - scheduler status: {coeur.tasks.state} +| - current runner status: {coeur.tasks.runnerState} + +morny.command.info.message.event +| Event Statistics : +| in today +| {event_statistics}""".stripMargin diff --git a/src/main/resources/assets_morny/langs/zh_cn.hyt b/src/main/resources/assets_morny/langs/zh_cn.hyt new file mode 100644 index 0000000..6a08f79 --- /dev/null +++ b/src/main/resources/assets_morny/langs/zh_cn.hyt @@ -0,0 +1,11 @@ + Morny Translations File + +%1.0 +&encoding=utf8 +&indent=1 + +morny.command.info.about +| Morny Cono +| 来自安妮的侍从小鼠。 +| ———————————————— +| {about_links} diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyAbout.scala b/src/main/scala/cc/sukazyo/cono/morny/core/MornyAbout.scala index 22002d0..52113dd 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyAbout.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyAbout.scala @@ -1,6 +1,6 @@ package cc.sukazyo.cono.morny.core -import cc.sukazyo.cono.morny.core.MornyAssets +import cc.sukazyo.cono.morny.data.MornyAssets import java.io.IOException diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala index f1b0f06..a0deba0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala @@ -397,7 +397,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes break(Some(LoginResult(account, remote.username, remote.id))) } catch case r: boundary.Break[Option[LoginResult]] => throw r - case e => + case e: Throwable => logger `error` s"""${e.toLogString} |login failed""" diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyAssets.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyAssets.scala similarity index 88% rename from src/main/scala/cc/sukazyo/cono/morny/core/MornyAssets.scala rename to src/main/scala/cc/sukazyo/cono/morny/data/MornyAssets.scala index 76990b2..c5e4c92 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyAssets.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyAssets.scala @@ -1,4 +1,4 @@ -package cc.sukazyo.cono.morny.core +package cc.sukazyo.cono.morny.data import cc.sukazyo.restools.ResourcesPackage diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala index b3a1081..e2e4c68 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -1,7 +1,6 @@ package cc.sukazyo.cono.morny.data -import cc.sukazyo.cono.morny.core.MornyAssets -import cc.sukazyo.cono.morny.core.MornyAssets.AssetsException +import cc.sukazyo.cono.morny.data.MornyAssets.AssetsException import java.io.IOException import scala.language.postfixOps diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Definitions.scala b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Definitions.scala new file mode 100644 index 0000000..75f9250 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Definitions.scala @@ -0,0 +1,14 @@ +package cc.sukazyo.cono.morny.util.hytrans + +//opaque type Definitions = Map[String, String] + +class Definitions ( + innerData: Map[String, String] +) extends Map[String, String] { + + def iterator: Iterator[(String, String)] = innerData.iterator + def removed (key: String): Map[String, String] = innerData.removed(key) + def updated[V1 >: String] (key: String, value: V1): Map[String, V1] = innerData.updated(key, value) + def get (key: String): Option[String] = innerData.get(key) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LangTag.scala b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LangTag.scala new file mode 100644 index 0000000..c6b7a70 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LangTag.scala @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.util.hytrans + +case class LangTag ( + lang: String, + priority: Long +) extends Comparable[LangTag] { + + override def compareTo(o: LangTag): Int = + this.priority `compareTo` o.priority + +} + +object LangTag { + + class IllegalLangTagException (message: String, val original: String) + extends IllegalArgumentException(message) + + def normalizeLangTag(tag: String): String = + tag.replaceAll("-", "_").toLowerCase + + @throws[IllegalLangTagException] + def ensureLangTag (tag: String): String = + tag.foreach { + case c if c.isLetter => + case '_' => + case ' ' | '\t' | '\r' | '\n' => + throw IllegalLangTagException("Lang Tag cannot contains space", tag) + case ill => + throw IllegalLangTagException(s"Illegal character '$ill' in Lang Tag \"$tag\"", tag) + } + tag + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LanguageTree.scala b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LanguageTree.scala new file mode 100644 index 0000000..6d288af --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/LanguageTree.scala @@ -0,0 +1,188 @@ +package cc.sukazyo.cono.morny.util.hytrans + +import cc.sukazyo.cono.morny.util.hytrans.LangTag.IllegalLangTagException + +import java.io.{PrintWriter, StringWriter} +import scala.collection.mutable +import scala.util.boundary + +class LanguageTree { + + class Node (val langTag: LangTag) extends Comparable[Node] { + private object LangTagOrdering extends Ordering[Node]: + override def compare (x: Node, y: Node): Int = y.langTag `compareTo` x.langTag + + override def compareTo (o: Node): Int = this.langTag `compareTo` o.langTag + + private type ChildCol = mutable.SortedSet[Node] + + private var _parent: Option[Node] = None + private val _children: ChildCol = mutable.SortedSet.empty(using LangTagOrdering) + + def parent: Option[Node] = _parent + def children: ChildCol = _children + + def traverseParent (f: Node => Unit): Unit = + _parent.foreach { parent => + f(parent) + parent.traverseParent(f) + } + + private def traversingTree (f: Node => Unit)(using visited: mutable.Set[Node]): Unit = + _children.foreach { child => + if !(visited contains child) then + child.traversingTree(f) + } + if !(visited contains this) then + f(this) + visited += this + this.parent.foreach { parent => + if !(visited contains parent) then + parent.traversingTree(f) + } + + def traverseTree (f: Node=>Unit): Unit = + val visited = mutable.Set.empty[Node] + f(this) + visited += this + traversingTree(f)(using visited) + + @throws[IllegalArgumentException] + private infix def setParent (parent: Node): Unit = + this._parent = Some(parent) + parent.traverseParent { p => + if p == parent then + throw new IllegalArgumentException( + s"failed set parent ${parent.langTag.lang} to ${langTag.lang}: " + + s"Cannot set parent to a child of itself" + ) + this._parent = None + } + + def detachParent (): Unit = + this._parent.foreach(_.removeChild(this)) + + def removeChild (child: Node): Unit = + child._parent = None + _children -= child + + @throws[IllegalArgumentException] + infix def addChild (child: Node): Unit = + val child_old_parent = child._parent + if child._parent.nonEmpty then + child.detachParent() + try child setParent this + catch case e: IllegalArgumentException => + child_old_parent.foreach(_ addChild child) + throw e + this._children += child + + @throws[IllegalArgumentException] + infix def addChild (child: LangTag): Node = + val node = new Node(child) + addChild(node) + node + + private def printTree (node: Node, prefix: String = "", printer: PrintWriter): Unit = { + printer.println(s"$prefix${node.langTag}") + node.children.foreach { child => + printTree(child, prefix + " ", printer) + } + } + + def printTree: String = + val s = StringWriter() + printTree(this, "", PrintWriter(s)) + s.toString + + override def toString: String = + printTree + + } + object Node: + def defaultRoot: Node = Node(LangTag("root", 0)) + + val root: Node = Node.defaultRoot + + def search (langTag: String): Option[Node] = + val _langTag = LangTag.normalizeLangTag(langTag) + boundary { + root.traverseTree { node => + if node.langTag.lang == _langTag then + boundary.break(Some(node)) + } + None + } + +} + +object LanguageTree { + + @throws[IllegalArgumentException] + def parseTreeDocument (document: String): LanguageTree = { + + val lines = document.replaceAll("\\r", "").split('\n') + val tree = new LanguageTree + val root = tree.root + import tree.Node + var currentLevel = mutable.ListBuffer[Node](root) + + def countHeadingWhitespaceLevel (line: String)(using whitespaceSize: Int): Option[(Int, String)] = + val whitespace = line.takeWhile(_.isWhitespace) + if whitespace.length % whitespaceSize == 0 then + Some(whitespace.length / whitespaceSize -> line.drop(whitespace.length)) + else None + + for (i <- lines.indices) { + val line = lines(i) + val line_number = i+1 + countHeadingWhitespaceLevel(line)(using 2) match + case Some((level, content)) => + val langTag: LangTag = content.split(",", 2) match + case Array(lang) => + try LangTag(LangTag.ensureLangTag(LangTag.normalizeLangTag(lang)), 0) + catch case e: IllegalLangTagException => + throw new IllegalArgumentException( + s"illegal lang name at line $line_number: ${e.getMessage}" + ).initCause(e) + case Array(lang, priority) => + try LangTag( + LangTag.ensureLangTag(LangTag.normalizeLangTag(lang)), + priority.filterNot(List(' ', ',', '-', '_', '\'').contains(_)).toInt + ) + catch + case e: NumberFormatException => + throw new IllegalArgumentException( + s"failed parse lang's priority at line $line_number: ${e.getMessage}" + ).initCause(e) + case e: IllegalLangTagException => + throw new IllegalArgumentException( + s"illegal lang name at line $line_number: ${e.getMessage}" + ).initCause(e) + case _ => + throw new IllegalArgumentException( + s"failed parse at line $line_number: line with invalid format." + ) + val node = Node(langTag) + if level < (currentLevel.length + 1) then + currentLevel = currentLevel take (level + 1) + currentLevel.last addChild node + currentLevel += node + else if level == (currentLevel.length + 1) then + currentLevel.last addChild node + currentLevel += node + else + throw new IllegalArgumentException( + s"failed parse at line $line_number: line with invalid indentation." + ) + case None => + throw new IllegalArgumentException( + s"failed parse at line $line_number: line with invalid indentation." + ) + } + + tree + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Parser.scala b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Parser.scala new file mode 100644 index 0000000..ec29786 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Parser.scala @@ -0,0 +1,41 @@ +package cc.sukazyo.cono.morny.util.hytrans + +object Parser { + + def parse (document: String): Definitions = { + + val lines = document.replaceAll("\\r", "").split('\n') + + val keyValues = collection.mutable.Map.empty[String, String] + var keyDef: String | Null = null + def newValue = StringBuilder() + var valueDef: StringBuilder = newValue + def addLine (line: String) = + valueDef ++= line += '\n' + //noinspection TypeAnnotation + def saveThis() = + if keyDef != null then + keyValues += (keyDef -> valueDef.toString.stripSuffix("\n")) + keyDef = null + valueDef = newValue + + lines.foreach { line => + line.headOption match { + case Some(' ') | Some('\t') | None => // empty lines, will be ignored + case Some('#') => // comment line, will be ignored + case Some('%') => // document meta definition line, currently not supported + case Some('|') => // content line + addLine(line drop 2) + case Some(_) => // a key definition line + saveThis() + keyDef = line + } + } + + saveThis() + + Definitions(keyValues.toMap) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Translations.scala b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Translations.scala new file mode 100644 index 0000000..906fcef --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/hytrans/Translations.scala @@ -0,0 +1,45 @@ +package cc.sukazyo.cono.morny.util.hytrans + +import cc.sukazyo.cono.morny.util.var_text.{Var, VarText} + +import scala.util.boundary + +class Translations ( + langs: LanguageTree, + translations: Map[String, Definitions] +) { + + def traverse (using lang: String)(f: (LangTag, Definitions) => Unit): Unit = { + langs.search(lang) + .getOrElse(langs.root) + .traverseTree(node => + translations.get(node.langTag.lang) + .map( + f(node.langTag, _) + ) + ) + } + + def traverseWithKey (key: String)(using lang: String)(f: (LangTag, Option[String]) => Unit): Unit = { + this.traverse (using lang) { (langTag, definitions) => + f(langTag, definitions.get(key)) + } + } + + def get (key: String)(using lang: String): Option[String] = { + boundary { + traverseWithKey(key) { (_, value) => + if value.nonEmpty then + boundary.break(Some(value.get)) + } + None + } + } + + def trans (key: String)(using lang: String): VarText = + VarText(get(key).getOrElse(s"#[$key@$lang]")) + + def trans (key: String, args: Var*)(using lang: String): String = + trans(key).render(args*) + +}