Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protocolLib = "6845acd89d"

cloudnet = "4.0.0-RC10"
luckperms = "5.4"

kyoriAdventure = "6.2.0"

[libraries]

Expand Down Expand Up @@ -105,6 +105,9 @@ playerAnimator = { group = "dev.kosmx.player-anim", name = "player-animation-lib
geckolib = { group = "software.bernie.geckolib", name = "geckolib-fabric-1.21.4", version.ref = "geckolib" }
emoteLib = { group = "gg.norisk", name = "emote-lib", version = "1.21.4-1.1.20" }

# kyori
kyoriAdventure = { group = "net.kyori", name = "adventure-platform-fabric", version.ref = "kyoriAdventure" }

# cloudnet
cloudnet-driver = { module = "eu.cloudnetservice.cloudnet:driver", version.ref = "cloudnet" }
cloudnet-bridge = { module = "eu.cloudnetservice.cloudnet:bridge", version.ref = "cloudnet" }
Expand Down
1 change: 1 addition & 0 deletions hero-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ dependencies {
modApi(libs.owolib)
modApi(libs.geckolib)
modApi(libs.emoteLib)
modApi(libs.kyoriAdventure)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gg.norisk.heroes.common

import gg.norisk.heroes.common.localization.LocalizationManager
import gg.norisk.heroes.common.registry.SoundRegistry
import gg.norisk.heroes.server.HeroesManagerServer
import net.fabricmc.api.EnvType
Expand Down Expand Up @@ -38,6 +39,7 @@ object HeroesManager : ModInitializer {
logger.info("Init Hero-Api Common...")
SoundRegistry.init()
HeroesManagerServer.initServer()
LocalizationManager.init()

ServerLifecycleEvents.SERVER_STARTING.register {
setBasePath(it.getSavePath(WorldSavePath("heroes")))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package gg.norisk.heroes.common.localization

import gg.norisk.heroes.common.HeroesManager
import gg.norisk.heroes.common.localization.minimessage.MiniMessageUtils
import gg.norisk.heroes.common.utils.createIfNotExists
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.fabricmc.loader.api.FabricLoader
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.client.resource.language.I18n
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.text.Text
import net.silkmc.silk.core.logging.logger
import net.silkmc.silk.core.text.literalText
import java.util.*
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString

object LocalizationManager {
val defaultLocale: Locale = Locale.ENGLISH

fun init() {
loadAllLanguageFiles()
}

private fun loadAllLanguageFiles() {
if (FabricLoader.getInstance().environmentType == EnvType.SERVER) {
FabricLoader.getInstance().getModContainer(HeroesManager.MOD_ID).map { container ->
val modAssets = container.rootPath.toFile().resolve("assets/${HeroesManager.MOD_ID}")
val langDir = modAssets.resolve("lang")

langDir.listFiles()?.forEach { langFile ->
val locale = runCatching { Locale.of(langFile.nameWithoutExtension) }
.onFailure {
print("Localization error: ")
it.printStackTrace()
}.getOrNull() ?: defaultLocale
logger().info("Found lang file for locale $locale")
LocalizationRegistry.register(locale).registerAllFromFile(langFile)
}
}
}
}

@Environment(EnvType.SERVER)
fun getLocalizedTextForPlayerServer(player: ServerPlayerEntity, key: String, vararg args: Any): Text {
val language = player.clientOptions.language
val locale = Locale.of(language)
val translations = LocalizationRegistry.get(locale)
if (translations == null) {
HeroesManager.logger.warn("translationRegistry for locale `${locale}` does not exist. all locales: ${LocalizationRegistry.locales.keys}")
}

val minimessage = translations?.getAsMinimessage(key, *args)
if (minimessage == null) {
HeroesManager.logger.warn("Translation for key `$key` does not exist.")
}
val text = minimessage
?: literalText {
text(key)
if (args.isNotEmpty()) {
text(" ${args.joinToString()}")
}
}
return text
}

@Environment(EnvType.CLIENT)
fun getLocalizedTextOnClient(key: String, vararg args: Any): Text {
val translatedText = I18n.translate(key, *args)
val formatted = MiniMessageUtils.deserialize(translatedText)
return formatted
}
}

@Environment(EnvType.CLIENT)
fun localizedText(key: String, vararg args: Any): Text {
return LocalizationManager.getLocalizedTextOnClient(key, *args)
}

fun PlayerEntity.getLocalized(key: String, vararg args: Any): Text {
return if (this is ClientPlayerEntity) {
println("is client player")
LocalizationManager.getLocalizedTextOnClient(key, *args)
} else {
println("is server player")
LocalizationManager.getLocalizedTextForPlayerServer(this as ServerPlayerEntity, key, *args)
}
}

fun PlayerEntity.sendLocalized(key: String, vararg args: Any) {
sendMessage(this.getLocalized(key, *args), false)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gg.norisk.heroes.common.localization

import java.util.Locale

object LocalizationRegistry {
val locales = hashMapOf<Locale, TranslationRegistry>()

fun register(locale: Locale): TranslationRegistry {
val translationRegistry = TranslationRegistry()
locales[locale] = translationRegistry
return translationRegistry
}

fun register(language: String): TranslationRegistry {
val locale = Locale.of(language)
return register(locale)
}

fun get(locale: Locale): TranslationRegistry? {
return locales[locale] ?: locales[LocalizationManager.defaultLocale]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package gg.norisk.heroes.common.localization

import gg.norisk.heroes.common.localization.minimessage.minimessage
import kotlinx.serialization.json.Json
import net.minecraft.text.Text
import java.io.File

class TranslationRegistry {
val translations: HashMap<String, String> = hashMapOf()

fun register(key: String, value: String) {
translations[key] = value
}

fun register(translation: Pair<String, String>) {
val (key, value) = translation
return register(key, value)
}

fun registerAllFromFile(file: File) {
val fileContent = file.readText()
val fileTranslations = runCatching {
Json.decodeFromString<HashMap<String, String>>(fileContent)
}.getOrNull() ?: hashMapOf()

fileTranslations.forEach { (key, value) ->
register(key, value)
}
}

fun get(key: String): String {
return translations[key] ?: key
}

fun getFormatted(key: String, vararg args: Any): String {
val string = get(key)
val formattedString = string.format(*args)
return formattedString
}

fun getAsText(key: String, vararg args: Any): Text {
val formattedString = getFormatted(key, *args)
return Text.literal(formattedString)
}

fun getAsMinimessage(key: String, vararg args: Any): Text {
val formattedString = getFormatted(key, *args)
return minimessage(formattedString)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package gg.norisk.heroes.common.localization.minimessage

import net.kyori.adventure.text.format.NamedTextColor
import net.kyori.adventure.text.format.Style
import net.kyori.adventure.text.format.TextColor
import net.kyori.adventure.text.minimessage.Context
import net.kyori.adventure.text.minimessage.ParsingException
import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver
import net.kyori.adventure.text.minimessage.internal.serializer.StyleClaim
import net.kyori.adventure.text.minimessage.internal.serializer.TokenEmitter
import net.kyori.adventure.text.minimessage.tag.Tag
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver

internal class LaborColorTagResolver : TagResolver, SerializableResolver.Single {
@Throws(ParsingException::class)
override fun resolve(name: String, args: ArgumentQueue, ctx: Context): Tag? {
if (!this.has(name)) {
return null
}
val colorName = if (isLaborColorOrAbbreviation(name)) {
args.popOr("Expected to find a color parameter: <name>|#RRGGBB").lowerValue()
} else {
name
}

val color = resolveColor(colorName, ctx)
return Tag.styling(color)
}

override fun has(name: String): Boolean {
return isLaborColorOrAbbreviation(name) || COLOR_ALIASES.containsKey(name)
}

override fun claimStyle(): StyleClaim<*>? {
return STYLE
}

companion object {
private const val LABORCOLOR_3 = "lc"
private const val LABORCOLOR_2 = "laborColour"
private const val LABORCOLOR = "laborColor"
val laborColors = hashMapOf<String, Int>()

val INSTANCE: TagResolver = LaborColorTagResolver()
private val STYLE = StyleClaim.claim(
LABORCOLOR,
{ obj: Style -> obj.color() },
{ color: TextColor, emitter: TokenEmitter ->
// TODO: custom aliases
// TODO: compact vs expanded format? COLOR vs color:COLOR vs c:COLOR
if (color is NamedTextColor) {
emitter.tag(NamedTextColor.NAMES.key(color)!!)
} else {
emitter.tag(color.asHexString())
}
})

private val COLOR_ALIASES: MutableMap<String, TextColor> = HashMap()

init {
LaborColors.getAllColorsWithValue().forEach { color, value ->
laborColors[color] = value
}
}

private fun isLaborColorOrAbbreviation(name: String): Boolean {
return name == LABORCOLOR || name == LABORCOLOR_2 || name == LABORCOLOR_3
}

@Throws(ParsingException::class)
fun resolveColor(colorName: String, ctx: Context): TextColor {
val color = if (laborColors.containsKey(colorName.lowercase())) {
TextColor.color(laborColors[colorName]!!)
} else if (COLOR_ALIASES.containsKey(colorName)) {
COLOR_ALIASES[colorName]
} else if (colorName[0] == TextColor.HEX_CHARACTER) {
TextColor.fromHexString(colorName)
} else {
NamedTextColor.NAMES.value(colorName)
}

if (color == null) {
throw ctx.newException(
String.format(
"Unable to parse a color from '%s'. Please use named.",
colorName
)
)
}
return color
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gg.norisk.heroes.common.localization.minimessage

import kotlin.reflect.full.declaredMemberProperties

object LaborColors {
val White = 0xFFFFFF
val Gray = 0xAAAAAA
val DarkGray = 0x555555
val Pink = 0xEF6F82
val LightPink = 0xFFC0CB
val DarkPink = 0x966f76
val DarkPurple = 0x8C5ABB
val Purple = 0xD9C4EC
val Green = 0xBFFFB7
val DarkGreen = 0x0E7C00
val Red = 0xFF9997
val DarkRed = 0xFF4C49
val Blue = 0x3399ff
val LightBlue = 0xA6EDFF
val Yellow = 0xF9E795
val Orange = 0xFFB797
val CornSilk = 0xFFF8DC
val DarkerCornSilk = 0x858276

fun getAllColorsWithValue(): Map<String, Int> {
return this::class.declaredMemberProperties.associate {
val name = it.name.lowercase()
val value = it.getter.call(this) as Int
name to value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gg.norisk.heroes.common.localization.minimessage

import net.fabricmc.api.EnvType
import net.fabricmc.loader.api.FabricLoader
import net.kyori.adventure.platform.modcommon.MinecraftClientAudiences
import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences
import net.kyori.adventure.text.minimessage.MiniMessage
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
import net.kyori.adventure.text.minimessage.tag.standard.*
import net.minecraft.text.Text
import net.silkmc.silk.core.Silk

object MiniMessageUtils {
private val audiences by lazy {
if (FabricLoader.getInstance().environmentType == EnvType.SERVER) {
MinecraftServerAudiences.builder(Silk.serverOrThrow).build()
} else {
MinecraftClientAudiences.builder().build()
}
}

private val miniMessage = MiniMessage.miniMessage()

private val tagResolver = TagResolver.builder().resolvers(
StandardTags.defaults(),
LaborColorTagResolver.INSTANCE
).build()


fun deserialize(string: String): Text {
val miniMessageComponent = miniMessage.deserialize(string, tagResolver)
val text = audiences.asNative(miniMessageComponent)
return text
}
}

fun minimessage(string: String): Text {
return MiniMessageUtils.deserialize(string)
}