better translation system

This commit is contained in:
alexcrea 2026-08-13 13:10:22 +02:00
parent 1e0e8d5f6d
commit 640bd1bfd8
Signed by: alexcrea
GPG key ID: E59DF23EE2A2266C
11 changed files with 257 additions and 83 deletions

View file

@ -2,7 +2,7 @@ package xyz.alexcrea.cuanvil.dependency.util
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
import org.bukkit.inventory.ItemStack import org.bukkit.command.CommandSender
import org.bukkit.inventory.meta.ItemMeta import org.bukkit.inventory.meta.ItemMeta
// Mostly made for paper, spigot and folia support // Mostly made for paper, spigot and folia support
@ -100,4 +100,19 @@ object PlatformUtil {
} }
} }
/**
* Try to send paper component to the player
*
* @param component The used component
* @return true if sent, else otherwise
*/
fun CommandSender.sendPaperMessage(component: Component): Boolean {
if(isPaper) {
this.sendMessage(component)
return true
}
return false
}
} }

View file

@ -16,7 +16,8 @@ import xyz.alexcrea.cuanvil.enchant.CAEnchantmentRegistry
import xyz.alexcrea.cuanvil.gui.config.MainConfigGui import xyz.alexcrea.cuanvil.gui.config.MainConfigGui
import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant import xyz.alexcrea.cuanvil.gui.util.GuiSharedConstant
import xyz.alexcrea.cuanvil.lang.Lang import xyz.alexcrea.cuanvil.lang.Lang
import xyz.alexcrea.cuanvil.lang.Lang.translate import xyz.alexcrea.cuanvil.lang.MsgError
import xyz.alexcrea.cuanvil.lang.MsgWarning
import xyz.alexcrea.cuanvil.listener.AnvilCloseListener import xyz.alexcrea.cuanvil.listener.AnvilCloseListener
import xyz.alexcrea.cuanvil.listener.AnvilResultListener import xyz.alexcrea.cuanvil.listener.AnvilResultListener
import xyz.alexcrea.cuanvil.listener.ChatEventListener import xyz.alexcrea.cuanvil.listener.ChatEventListener
@ -111,12 +112,12 @@ open class CustomAnvil : JavaPlugin() {
/** /**
* Error Logging handler * Error Logging handler
*/ */
@JvmStatic fun logError(message: String, throwable: Throwable? = null, track: Boolean = true) { @JvmStatic fun logError(message: String, throwable: Throwable? = null, track: Boolean = true, level: Level = Level.SEVERE) {
instance.logger.log(Level.SEVERE, message, throwable) instance.logger.log(level, message, throwable)
addToLogQueue(message) addToLogQueue("Error: $message")
if(track && throwable != null) { if(track) {
MetricsUtil.trackError(throwable) MetricsUtil.trackError(message, throwable)
} }
} }
} }
@ -168,7 +169,7 @@ open class CustomAnvil : JavaPlugin() {
try { try {
legacyCheck() legacyCheck()
} catch (e: Exception) { } catch (e: Exception) {
logError("error.load.legacy.failed".translate(), e) MsgError.LOAD_LEGACY_FAILED.log(e)
if(trySafeStart()) return if(trySafeStart()) return
} }
@ -177,7 +178,7 @@ open class CustomAnvil : JavaPlugin() {
try { try {
CustomAnvilCommand(this) CustomAnvilCommand(this)
} catch (e: Exception) { } catch (e: Exception) {
logError("error.load.command-register".translate(), e) MsgError.LOAD_COMMAND_REGISTER.log(e)
if(trySafeStart()) return if(trySafeStart()) return
} }
@ -186,7 +187,7 @@ open class CustomAnvil : JavaPlugin() {
try { try {
DependencyManager.loadDependency() DependencyManager.loadDependency()
} catch (e: Exception) { } catch (e: Exception) {
logError("error.load.compatibility".translate(), e) MsgError.LOAD_COMPATIBILITY.log(e)
if(tryDirtyStart()) return if(tryDirtyStart()) return
} }
@ -194,7 +195,7 @@ open class CustomAnvil : JavaPlugin() {
try { try {
registerListeners() registerListeners()
} catch (e: Exception) { } catch (e: Exception) {
logError("error.load.listeners".translate(), e) MsgError.LOAD_LISTENERS.log(e)
if(tryDirtyStart()) return if(tryDirtyStart()) return
} }
@ -215,18 +216,15 @@ open class CustomAnvil : JavaPlugin() {
val potentialPlugin = Bukkit.getPluginManager().getPlugin("UnsafeEnchantsPlus") val potentialPlugin = Bukkit.getPluginManager().getPlugin("UnsafeEnchantsPlus")
if (potentialPlugin != null) { if (potentialPlugin != null) {
Bukkit.getPluginManager().disablePlugin(potentialPlugin) Bukkit.getPluginManager().disablePlugin(potentialPlugin)
logger.warning("warning.load.legacy.old-name.1".translate()) MsgWarning.LOAD_LEGACY_OLD_NAME.log()
logger.warning("warning.load.legacy.old-name.2".translate())
} }
val isPaper = PlatformUtil.isPaper val isPaper = PlatformUtil.isPaper
if(!isPaper) { if(!isPaper) {
logger.warning("warning.load.legacy.spigot.1".translate()) MsgWarning.LOAD_LEGACY_SPIGOT.log()
logger.warning("warning.load.legacy.spigot.2".translate()) if(MinecraftVersionUtil.isTooNewForSpigot)
if(MinecraftVersionUtil.isTooNewForSpigot) { MsgWarning.LOAD_LEGACY_SPIGOT_OLD.log()
logger.warning("warning.load.legacy.spigot-old.1".translate())
logger.warning("warning.load.legacy.spigot-old.1".translate())
}
} }
val loader = if(isPaper) "paper" else "spigot" val loader = if(isPaper) "paper" else "spigot"
@ -238,13 +236,13 @@ open class CustomAnvil : JavaPlugin() {
UpdateUtils.currentMinecraftVersion().toString()) UpdateUtils.currentMinecraftVersion().toString())
.setFeatured(featured) .setFeatured(featured)
.setOnError { .setOnError {
logger.log(Level.WARNING, "error.load.update.check-fail".translate(), it) MsgError.LOAD_UPDATE_CHECK_FAIL.log(it, level = Level.WARNING, track = false)
} }
.checkVersion { latestVer: String? -> .checkVersion { latestVer: String? ->
CustomAnvil.latestVer = latestVer CustomAnvil.latestVer = latestVer
if(latestVer == null || version.contains(latestVer)) return@checkVersion if(latestVer == null || version.contains(latestVer)) return@checkVersion
logger.warning("warning.load.update.available".translate(Pair("version", latestVer))) MsgWarning.LOAD_UPDATE_AVAILABLE.log(latestVer)
} }
} }
@ -263,7 +261,7 @@ open class CustomAnvil : JavaPlugin() {
try { try {
loadEnchantmentSystem() loadEnchantmentSystem()
} catch (e: Exception) { } catch (e: Exception) {
logError("error.load.enchant-system".translate(), e) MsgError.LOAD_ENCHANT_SYSTEM.log(e)
tryDirtyStart() tryDirtyStart()
} }
} }
@ -278,7 +276,7 @@ open class CustomAnvil : JavaPlugin() {
// Load config // Load config
if (!ConfigHolder.loadNonDefaultConfig()) { if (!ConfigHolder.loadNonDefaultConfig()) {
logError("error.load.non-default-config".translate()) MsgError.LOAD_NON_DEFAULT_CONFIG.log()
server.pluginManager.disablePlugin(this) server.pluginManager.disablePlugin(this)
return return
} }
@ -332,16 +330,15 @@ open class CustomAnvil : JavaPlugin() {
try { try {
val configReader = FileReader(resourceFile) val configReader = FileReader(resourceFile)
yamlConfig.load(configReader) yamlConfig.load(configReader)
} catch (_: Exception) { } catch (e: Exception) {
MsgError.RELOAD_FAIL.log(e, resourceFile.path)
if (hardFailSafe) { if (hardFailSafe) {
// This is important and may impact gameplay if it does not load. // This is important and may impact gameplay if it does not load.
// Failsafe is to stop the plugin // Failsafe is to stop the plugin
logError("error.reload.resource.fail".translate(Pair("path", resourceFile.path))) MsgError.RELOAD_HARD_FAIL.log()
logError("error.reload.resource.hard-fail".translate())
Bukkit.getPluginManager().disablePlugin(this) Bukkit.getPluginManager().disablePlugin(this)
} else {
logError("error.reload.resource.fail".translate(Pair("path", resourceFile.path)))
} }
return null return null
} }
return yamlConfig return yamlConfig

View file

@ -122,8 +122,7 @@ class DiagnosticExecutor : CASubCommand {
if (sender is HumanEntity) { if (sender is HumanEntity) {
if (hasError) if (hasError)
sender.spigot() sender.sendMessage(ChatColor.RED.toString() + "There was an error running the diagnostic")
.sendMessage(TextComponent(ChatColor.RED.toString() + "There was an error running the diagnostic"))
val message = TextComponent(ChatColor.GREEN.toString() + "Click to copy diagnostic data") val message = TextComponent(ChatColor.GREEN.toString() + "Click to copy diagnostic data")
message.clickEvent = ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, stb.toString()) message.clickEvent = ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, stb.toString())

View file

@ -18,60 +18,13 @@ object Lang {
if(default != lang) default.reload() if(default != lang) default.reload()
} }
fun String.translate(): String { fun getTranslated(key: String): String {
val value = lang.get(this) val value = lang.get(key)
if(value != null) return value if(value != null) return value
CustomAnvil.log("Missing language data for ${lang.name} using default") CustomAnvil.log("Missing language data for ${lang.name} using default")
return default.get(this) ?: this return default.get(key) ?: key
}
fun String.translate(vararg params: Pair<String, Any>): String {
return translate(
params.asSequence()
.map { Pair(it.first, it.second.toString()) }
.toMap()
)
}
fun String.translate(vararg params: Pair<Char, Any>): String {
return translate(
params.asSequence()
.map { Pair(it.first.toString(), it.second.toString()) }
.toMap()
)
}
fun String.translate(vararg params: Pair<String, String>): String {
return translate(params.toMap())
}
fun String.translate(vararg params: Pair<Char, String>): String {
return translate(
params.asSequence()
.map { Pair(it.first.toString(), it.second) }
.toMap()
)
}
fun String.translate(params: Map<String, String>): String {
val builder = StringBuilder(translate())
// replace all placeholder thingy %key -> value
for((key, replacement) in params) {
var current = 0
while(true) {
current = builder.indexOf('%', current) + 1
if(current < 0 || current + key.length > builder.length) break // may be able to be removed if bound checked in startsWith ?
if(!builder.startsWith(key, current, false)) continue
builder.replace(current - 1, current + key.length, replacement)
current = current - 1 + replacement.length
}
}
return builder.toString()
} }
/* /*

View file

@ -7,7 +7,6 @@ import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.logging.Level import java.util.logging.Level
class Language(private val id: String, private val default: Boolean = false) { class Language(private val id: String, private val default: Boolean = false) {
private val resourcePath: String private val resourcePath: String

View file

@ -0,0 +1,128 @@
package xyz.alexcrea.cuanvil.lang
import io.delilaheve.CustomAnvil
import net.kyori.adventure.text.Component
import org.bukkit.command.CommandSender
import xyz.alexcrea.cuanvil.dependency.util.PlatformUtil.sendPaperMessage
import xyz.alexcrea.cuanvil.util.MiniMessageUtil
import java.util.logging.Level
import kotlin.math.min
interface MessageLike {
fun log(vararg params: Any)
fun send(destination: CommandSender, vararg params: Any)
}
enum class MessageType {
DEFAULT,
WARNING,
ERROR,
}
open class Message(val key: String, vararg val params: String) : MessageLike {
protected fun replaceParameters(stb: StringBuilder, vararg values: Any) {
// replace all placeholder thingy %key -> value
for(i in 0 until min(params.size, values.size)) {
val key = params[i]
val replacement = values[i].toString()
var current = 0
while(true) {
current = stb.indexOf('%', current) + 1
if(current < 0 || current + key.length > stb.length) break // may be able to be removed if bound checked in startsWith ?
if(!stb.startsWith(key, current, false)) continue
stb.replace(current - 1, current + key.length, replacement)
current = current - 1 + replacement.length
}
}
}
fun unformatted(vararg params: Any): String {
val translated = Lang.getTranslated(key)
if(params.isEmpty()) return translated
val stb = StringBuilder(translated)
replaceParameters(stb, *params)
return stb.toString()
}
fun formatted(vararg params: Any): Component {
val unformatted = unformatted(*params)
return MiniMessageUtil.mm.deserialize(unformatted)
}
override fun log(vararg params: Any) {
val text = unformatted(*params)
CustomAnvil.instance.logger.info(text)
}
override fun send(destination: CommandSender, vararg params: Any) {
val message = formatted(*params)
if(!destination.sendPaperMessage(message))
destination.sendMessage(MiniMessageUtil.legacy_mm.serialize(message))
}
}
class WarningMessage(key: String, vararg params: String) : Message("warning.$key", *params) {
override fun log(vararg params: Any) {
val text = unformatted(*params)
CustomAnvil.instance.logger.warning(text)
}
}
class ErrorMessage(key: String, vararg params: String) : Message("error.$key", *params) {
override fun log(vararg params: Any) {
val text = unformatted(*params)
CustomAnvil.logError(text)
}
fun log(e: Throwable, vararg params: Any, level: Level = Level.SEVERE, track: Boolean = true) {
val text = unformatted(*params)
CustomAnvil.logError(text, e, track, level)
}
}
class MultiLineMessage(type: MessageType, baseKey: String, count: Int, vararg params: String) : MessageLike {
private val messages = ArrayList<MessageLike>()
init {
for(i in 1 until count + 1) {
val message = createNew(type, "$baseKey.$i", *params)
messages.add(message)
}
}
private fun createNew(type: MessageType, key: String, vararg params: String): MessageLike {
return when(type) {
MessageType.DEFAULT -> Message(key, *params)
MessageType.WARNING -> WarningMessage(key, *params)
MessageType.ERROR -> ErrorMessage(key, *params)
}
}
override fun log(vararg params: Any) {
for(message in messages) {
message.log(*params)
}
}
override fun send(destination: CommandSender, vararg params: Any) {
for(message in messages) {
message.send(destination, *params)
}
}
}

View file

@ -0,0 +1,4 @@
package xyz.alexcrea.cuanvil.lang
object Msg {
}

View file

@ -0,0 +1,26 @@
package xyz.alexcrea.cuanvil.lang
object MsgError {
/*
* -----------------
* Load and reload
* -----------------
*/
val LOAD_UPDATE_CHECK_FAIL = ErrorMessage("load.update.check-fail")
val LOAD_LEGACY_FAILED = ErrorMessage("load.legacy.failed")
val LOAD_COMMAND_REGISTER = ErrorMessage("load.command-register")
val LOAD_COMPATIBILITY = ErrorMessage("load.compatibility")
val LOAD_LISTENERS = ErrorMessage("load.listeners")
val LOAD_ENCHANT_SYSTEM = ErrorMessage("load.enchant-system")
val LOAD_NON_DEFAULT_CONFIG = ErrorMessage("load.non-default-config")
val RELOAD_FAIL = ErrorMessage("reload.resource.fail", "path")
val RELOAD_HARD_FAIL = ErrorMessage("reload.resource.hardfail")
/*
* ----------
* Commands
* ----------
*/
}

View file

@ -0,0 +1,17 @@
package xyz.alexcrea.cuanvil.lang
object MsgWarning {
/*
* ---------------
* Load and reload
* ---------------
*/
val LOAD_UPDATE_AVAILABLE = WarningMessage("load.update.available", "version")
val LOAD_LEGACY_OLD_NAME = MultiLineMessage(MessageType.WARNING, "load.legacy.old-name", 2)
val LOAD_LEGACY_SPIGOT = MultiLineMessage(MessageType.WARNING, "load.legacy.spigot", 2)
val LOAD_LEGACY_SPIGOT_OLD = MultiLineMessage(MessageType.WARNING, "load.legacy.spigot", 2)
}

View file

@ -74,8 +74,8 @@ object MetricsUtil {
lastError = e lastError = e
} }
fun trackError(message: String) { fun trackError(message: String, cause: Throwable? = null) {
ERROR_TRACKER?.trackError(message) trackError(RuntimeException(message, cause))
} }
} }

View file

@ -1,4 +1,5 @@
name: English name: English
last-updated: 2.1.0
warning: warning:
load: load:
@ -27,3 +28,38 @@ error:
resource: resource:
fail: "Resource %path Could not be loaded or reloaded." fail: "Resource %path Could not be loaded or reloaded."
hard-fail: "Disabling plugin." hard-fail: "Disabling plugin."
command:
shared:
no-diag-permission: "<red>You do not have permission to diagnostic this server"
# I try to avoid using & for color but this is for a bungee text component
hover-copy: "§7Click to copy"
warning:
missing-subcmd: "Need to specify a subcommand. for example %example1 or %example2"
root:
warning:
unknown-sub: "<red>Invalid subcommand. run <yellow>`%command help` <red>to see available commands"
error:
generic: "<red>Error running this command"
debug:
description: "Used to toggle debug logs and retrieve them"
log-cleared: "Log Cleared"
toggled: "Debug toggled to %type"
# I try to avoid using & for color but this is for a bungee text component
copy: "§aClick to copy log data"
data:
header: "Debug Log data:"
line-count: "Found %count lines"
warning:
unspecified-type: "Need to specify which type of debug to toggle: \"default\" or \"verbose\""
no-log: "No log to show ? make sure you tried with debug log toggled (%command)"
diagnostic:
description: "Basic diagnostic of this plugin"
had-error: "<red>There was an error running the diagnostic"
# I try to avoid using & for color but this is for a bungee text component
copy: "§aClick to copy diagnostic data"
config:
description: "Used to edit the configuration of the plugin"
warning:
legacy-name: "<red>/ca gui has been moved to /ca config"