refactor text translations, tests

This commit is contained in:
Bixilon 2023-03-19 15:25:04 +01:00
parent a0fe43e296
commit d90ccd3780
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
15 changed files with 359 additions and 87 deletions

View File

@ -31,7 +31,7 @@ FormattedChatMessage(
init {
// ToDo: parent (formatting)
val data = type.chat.formatParameters(parameters)
text = connection.language.forceTranslate(type.chat.translationKey.toResourceLocation(), restrictedMode = true, fallback = type.chat.translationKey, data = data)
text = connection.language.forceTranslate(type.chat.translationKey.toResourceLocation(), restricted = true, fallback = type.chat.translationKey, data = data)
text.setFallbackColor(ChatUtil.DEFAULT_CHAT_COLOR)
}
}

View File

@ -46,7 +46,7 @@ object LanguageUtil {
}
fun loadJsonLanguage(json: JsonObject): LanguageData {
val data: LanguageData = mutableMapOf()
val data: LanguageData = HashMap()
for ((key, value) in json) {
val path = ResourceLocation.of(key).path
@ -57,7 +57,7 @@ object LanguageUtil {
}
fun loadLanguage(lines: Sequence<String>): LanguageData {
val data: LanguageData = mutableMapOf()
val data: LanguageData = HashMap()
for (line in lines) {
if (line.isBlank() || line.startsWith("#")) {

View File

@ -12,9 +12,9 @@
*/
package de.bixilon.minosoft.data.language.lang
import de.bixilon.minosoft.data.language.placeholder.PlaceholderUtil
import de.bixilon.minosoft.data.language.translate.Translator
import de.bixilon.minosoft.data.registries.identified.ResourceLocation
import de.bixilon.minosoft.data.text.BaseComponent
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.data.text.TextComponent
@ -23,62 +23,12 @@ class Language(
private val data: LanguageData,
) : Translator {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restrictedMode: Boolean, vararg data: Any?): ChatComponent? {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restricted: Boolean, vararg data: Any?): ChatComponent? {
val placeholder = this.data[key?.path] ?: return null
return Companion.translate(placeholder, parent, this, restrictedMode, *data)
return PlaceholderUtil.format(placeholder, parent, restricted, *data)
}
override fun toString(): String {
return name
}
companion object {
private val FORMATTER_ORDER_REGEX = "%(\\w+)\\\$[sd]".toRegex() // %1$s fell from a high place
private val FORMATTER_SPLIT_REGEX = "%[ds]".toRegex() // %s fell from a high place
fun translate(placeholder: String, parent: TextComponent? = null, translator: Translator? = null, restrictedMode: Boolean = false, vararg data: Any?): ChatComponent {
val ret = BaseComponent()
val arguments: MutableList<Any?> = mutableListOf()
var splitPlaceholder: List<String> = emptyList()
// Bring arguments in correct oder
FORMATTER_ORDER_REGEX.findAll(placeholder).toList().let {
if (it.isEmpty()) {
// this is not the correct formatter
return@let
}
splitPlaceholder = placeholder.split(FORMATTER_ORDER_REGEX)
for (matchResult in it) {
// 2 groups: Full, index. We don't care about the full value, just skip it
val dataIndex = matchResult.groupValues[1].toInt() - 1
if (dataIndex < 0 || dataIndex > data.size) {
arguments += null
continue
}
arguments += data[dataIndex]
}
}
// check if other splitter already did the job for us
if (splitPlaceholder.isEmpty()) {
placeholder.split(FORMATTER_SPLIT_REGEX).let {
splitPlaceholder = it
arguments.addAll(data.toList())
}
}
// create base component
for ((index, part) in splitPlaceholder.withIndex()) {
ret += ChatComponent.of(part, translator, parent, restrictedMode)
if (index < data.size) {
ret += ChatComponent.of(arguments[index], translator, parent, restrictedMode)
}
}
return ret
}
}
}

View File

@ -22,9 +22,9 @@ class LanguageList(
private val list: MutableList<Language>,
) : Translator {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restrictedMode: Boolean, vararg data: Any?): ChatComponent? {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restricted: Boolean, vararg data: Any?): ChatComponent? {
for (language in list) {
return language.translate(key, parent, restrictedMode, data) ?: continue
return language.translate(key, parent, restricted, data) ?: continue
}
return null
}

View File

@ -24,9 +24,9 @@ class LanguageManager(
) : Translator {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restrictedMode: Boolean, vararg data: Any?): ChatComponent? {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restricted: Boolean, vararg data: Any?): ChatComponent? {
for (language in languages) {
return language.translate(key, parent, restrictedMode, *data) ?: continue
return language.translate(key, parent, restricted, *data) ?: continue
}
return null
}

View File

@ -22,9 +22,9 @@ class MultiLanguageManager(
val translators: MutableMap<String, Translator> = mutableMapOf(),
) : Translator {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restrictedMode: Boolean, vararg data: Any?): ChatComponent? {
override fun translate(key: ResourceLocation?, parent: TextComponent?, restricted: Boolean, vararg data: Any?): ChatComponent? {
if (key == null) return null
return translators[key.namespace]?.translate(key, parent, restrictedMode, *data)
return translators[key.namespace]?.translate(key, parent, restricted, *data)
}
}

View File

@ -0,0 +1,30 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.language.placeholder
import de.bixilon.minosoft.data.text.BaseComponent
import de.bixilon.minosoft.data.text.TextComponent
import java.util.*
class PlaceholderIteratorOptions(
val iterator: PrimitiveIterator.OfInt,
val parent: TextComponent?,
val restricted: Boolean,
val data: Array<out Any?>,
val component: BaseComponent = BaseComponent(),
var previous: TextComponent? = null,
var builder: StringBuilder = StringBuilder(),
var dataIndex: Int = 0,
var escaped: Boolean = false,
)

View File

@ -0,0 +1,121 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.language.placeholder
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.data.text.EmptyComponent
import de.bixilon.minosoft.data.text.TextComponent
object PlaceholderUtil {
private val DIGIT_RANGE = '0'.code..'9'.code
private const val ESCAPE = '%'.code
private const val INDEX = '$'.code
private const val STRING = 's'.code
private const val DIGIT = 'd'.code
private fun PlaceholderIteratorOptions.processEscape() {
if (escaped) {
builder.appendCodePoint(ESCAPE)
} else {
escaped = true
}
}
private fun PlaceholderIteratorOptions.push() {
if (builder.isEmpty()) return
val text = ChatComponent.of(builder.toString(), parent = previous ?: parent, restricted = restricted)
if (text is TextComponent) {
previous = text
}
component += text
builder.clear()
}
private fun PlaceholderIteratorOptions.appendArgument(index: Int) {
val value = if (index >= 0 && index < data.size) data[index] else "<null>" // TODO (kutil 1.21): replace with ArrayUtil::isIndex
component += ChatComponent.of(value, parent = previous ?: parent, restricted = restricted)
}
private fun PlaceholderIteratorOptions.processOrdered() {
push()
appendArgument(dataIndex++)
}
private fun PlaceholderIteratorOptions.processIndexed(char: Int) {
if (char !in DIGIT_RANGE) {
return processChar(char)
}
val indexBuilder = StringBuilder()
indexBuilder.appendCodePoint(char)
var trailing = 0
while (iterator.hasNext()) {
val digit = iterator.nextInt()
if (digit in DIGIT_RANGE) {
indexBuilder.appendCodePoint(digit)
} else {
trailing = digit
break
}
}
if (trailing != INDEX) {
indexBuilder.append(trailing)
}
if (!iterator.hasNext()) {
builder.append(indexBuilder)
return
}
val type = iterator.nextInt()
if (trailing != INDEX || (type != STRING && type != DIGIT)) {
builder.append(indexBuilder)
return
}
push()
appendArgument(Integer.parseInt(indexBuilder.toString()))
}
private fun PlaceholderIteratorOptions.processChar() = processChar(iterator.nextInt())
private fun PlaceholderIteratorOptions.processChar(char: Int) {
if (char == ESCAPE) {
return processEscape()
}
if (!escaped) {
builder.appendCodePoint(char)
return
}
escaped = false
if (char != STRING && char != DIGIT) {
return processIndexed(char)
}
return processOrdered()
}
fun format(placeholder: String, parent: TextComponent? = null, restricted: Boolean = false, vararg data: Any?): ChatComponent {
if (data.isEmpty()) return ChatComponent.of(placeholder, parent = parent, restricted = restricted)
val options = PlaceholderIteratorOptions(placeholder.codePoints().iterator(), parent, restricted, data)
while (options.iterator.hasNext()) {
options.processChar()
}
options.push()
return options.component.trim() ?: EmptyComponent
}
}

View File

@ -14,7 +14,7 @@
package de.bixilon.minosoft.data.language.translate
import de.bixilon.minosoft.data.language.LanguageUtil
import de.bixilon.minosoft.data.language.lang.Language
import de.bixilon.minosoft.data.language.placeholder.PlaceholderUtil
import de.bixilon.minosoft.data.registries.identified.ResourceLocation
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.data.text.TextComponent
@ -25,16 +25,16 @@ interface Translator {
return forceTranslate(key, null, false, null, *data)
}
fun forceTranslate(key: ResourceLocation?, parent: TextComponent? = null, restrictedMode: Boolean = false, fallback: String? = null, vararg data: Any?): ChatComponent {
translate(key, parent, restrictedMode, *data)?.let { return it }
fun forceTranslate(key: ResourceLocation?, parent: TextComponent? = null, restricted: Boolean = false, fallback: String? = null, vararg data: Any?): ChatComponent {
translate(key, parent, restricted, *data)?.let { return it }
if (fallback != null) {
return Language.translate(fallback, parent, null, restrictedMode)
return PlaceholderUtil.format(fallback, parent, restricted, *data)
}
return LanguageUtil.getFallbackTranslation(key, parent, restrictedMode, data)
return LanguageUtil.getFallbackTranslation(key, parent, restricted, *data)
}
fun translate(key: ResourceLocation?, parent: TextComponent? = null, vararg data: Any?): ChatComponent? = translate(key, parent, false, *data)
fun translate(key: ResourceLocation?, parent: TextComponent? = null, restrictedMode: Boolean = false, vararg data: Any?): ChatComponent?
fun translate(key: ResourceLocation?, parent: TextComponent? = null, restricted: Boolean = false, vararg data: Any?): ChatComponent?
fun translate(translatable: Any?): ChatComponent {

View File

@ -93,7 +93,7 @@ interface ChatComponent {
val EMPTY = EmptyComponent
@JvmOverloads
fun of(raw: Any? = null, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false, restrictedMode: Boolean = false): ChatComponent {
fun of(raw: Any? = null, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false, restricted: Boolean = false): ChatComponent {
if (raw == null) {
return EMPTY
}
@ -101,15 +101,15 @@ interface ChatComponent {
return raw
}
if (raw is Translatable && raw !is ResourceLocation) {
return (translator ?: Minosoft.LANGUAGE_MANAGER).forceTranslate(raw.translationKey, parent, restrictedMode = restrictedMode)
return (translator ?: Minosoft.LANGUAGE_MANAGER).forceTranslate(raw.translationKey, parent, restricted = restricted)
}
when (raw) {
is Map<*, *> -> return BaseComponent(translator, parent, raw.unsafeCast(), restrictedMode).trim() ?: EmptyComponent
is Map<*, *> -> return BaseComponent(translator, parent, raw.unsafeCast(), restricted).trim() ?: EmptyComponent
is List<*> -> {
val component = BaseComponent()
for (part in raw) {
component += of(part, translator, parent, restrictedMode = restrictedMode).trim() ?: continue
component += of(part, translator, parent, restricted = restricted).trim() ?: continue
}
return component.trim() ?: EmptyComponent
}
@ -126,7 +126,7 @@ interface ChatComponent {
if (codePoint == '{'.code || codePoint == '['.code) {
try {
val read: Any = Jackson.MAPPER.readValue(string, Any::class.java)
return of(read, translator, parent, ignoreJson = true, restrictedMode).trim() ?: EmptyComponent
return of(read, translator, parent, ignoreJson = true, restricted).trim() ?: EmptyComponent
} catch (ignored: JacksonException) {
break
}
@ -135,7 +135,7 @@ interface ChatComponent {
}
}
return LegacyComponentReader.parse(parent, string, restrictedMode).trim() ?: EmptyComponent
return LegacyComponentReader.parse(parent, string, restricted).trim() ?: EmptyComponent
}
fun String.chat(): ChatComponent {

View File

@ -56,7 +56,7 @@ open class InByteBuffer : de.bixilon.kutil.buffer.bytes.`in`.InByteBuffer {
}
open fun readChatComponent(): ChatComponent {
return ChatComponent.of(readString(), restrictedMode = true)
return ChatComponent.of(readString(), restricted = true)
}
fun readDirection(): Directions {

View File

@ -20,7 +20,7 @@ minosoft:server_info.remote_brand=Remote brand
minosoft:server_info.active_connections=Connected
minosoft:server_info.players_online=Players online
minosoft:server_info.ping=Latency
minosoft:server_info.delete.dialog.description=Do you really want to delete the server %1$s (%2$s)?
minosoft:server_info.delete.dialog.description=Do you really want to delete the server %0$s (%1$s)?
minosoft:connection.dialog.verify_assets.title=Verifying assets... - Minosoft
@ -107,7 +107,7 @@ minosoft:main.account.checking_dialog.title=Checking account... - Minosoft
minosoft:main.account.checking_dialog.header=Checking account... Please wait
minosoft:main.account.card.connection_count=%1$s connections
minosoft:main.account.card.connection_count=%0$s connections
minosoft:main.account.account_info.id=Id
minosoft:main.account.account_info.state=State
@ -135,20 +135,20 @@ minosoft:main.account.add.mojang.cancel_button=Cancel
minosoft:main.account.add.microsoft.please_wait.device_code=Obtaining device code...Please wait!
minosoft:main.account.add.microsoft.title=Add microsoft account - Minosoft
minosoft:main.account.add.microsoft.header=Please use a web browser to open the page %1$s and enter the following code in order to proceed with the login
minosoft:main.account.add.microsoft.header=Please use a web browser to open the page %0$s and enter the following code in order to proceed with the login
minosoft:main.account.add.microsoft.cancel=Cancel
minosoft:connection.kick.title=Kicked from server
minosoft:connection.kick.header=You got kicked
minosoft:connection.kick.description=You got kicked from %1$s (connected with: %2$s)
minosoft:connection.kick.description=You got kicked from %0$s (connected with: %1$s)
minosoft:connection.kick.reconnect_button=Reconnect
minosoft:connection.kick.close_button=Close
minosoft:connection.login_kick.title=Kicked from server
minosoft:connection.login_kick.header=You got kicked
minosoft:connection.login_kick.description=You got kicked while logging in from %1$s (connected with: %2$s)
minosoft:connection.login_kick.description=You got kicked while logging in from %0$s (connected with: %0$s)
minosoft:error.title=%1$s - Minosoft
minosoft:error.title=%0$s - Minosoft
minosoft:error.header=An error occurred!
minosoft:error.description=An error in minosoft occurred. You can continue like before, but the behavior might not be the expected one. If this error persists, feel free to open an issue here: https://gitlab.bixilon.de/bixilon/minosoft/-/issues/
minosoft:error.fatal_crash=Fatal crash

View File

@ -19,7 +19,7 @@ minosoft:server_info.active_connections=Conexiones activas
minosoft:server_info.players_online=Jugadores conectados
minosoft:server_info.ping=Latencia
minosoft:server_info.delete.dialog.description=Estas seguro de que quieres eliminar el servidor: %1$s (%2$s)?
minosoft:server_info.delete.dialog.description=Estas seguro de que quieres eliminar el servidor: %0$s (%1$s)?
minosoft:connection.status.state.waiting=Esperando...
@ -68,7 +68,7 @@ minosoft:main.account.type.microsoft=Microsoft
minosoft:main.account.type.offline=Offline
minosoft:main.account.card.connection_count=%1$s conexiones
minosoft:main.account.card.connection_count=%0$s conexiones
minosoft:main.account.account_info.id=Id
minosoft:main.account.account_info.email=E-Mail
@ -97,15 +97,15 @@ minosoft:main.account.add.microsoft.title=Añadir cuenta de Microsoft
minosoft:connection.kick.title=Expulsado del servidor
minosoft:connection.kick.header=Te han expulsado
minosoft:connection.kick.description=Te han expulsado de %1$s (conectado con: %2$s)
minosoft:connection.kick.description=Te han expulsado de %0$s (conectado con: %1$s)
minosoft:connection.kick.reconnect_button=Reconectarse
minosoft:connection.kick.close_button=Cerrar
minosoft:connection.login_kick.title=Expulsado del servidor
minosoft:connection.login_kick.header=Te han expulsado
minosoft:connection.login_kick.description=Te han expulsado mientras iniciabas sesion de %1$s (conectado con: %2$s)
minosoft:connection.login_kick.description=Te han expulsado mientras iniciabas sesion de %0$s (conectado con: %1$s)
minosoft:error.title=%1$s - Minosoft
minosoft:error.title=%0$s - Minosoft
minosoft:error.header=¡Ha ocurrido un error!
minosoft:error.description=Ha ocurrido un error en Minosoft. Puedes continuar como antes, pero el comportamiento puede ser inesperado. Si el error continua, sientete libre de abrir un ticket aquí: https://gitlab.bixilon.de/bixilon/minosoft/-/issues/
minosoft:error.fatal_crash=Cierre inesperado

View File

@ -0,0 +1,171 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.language.lang
import de.bixilon.minosoft.data.language.translate.Translator
import de.bixilon.minosoft.data.text.BaseComponent
import de.bixilon.minosoft.data.text.TextComponent
import de.bixilon.minosoft.data.text.formatting.color.ChatColors
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class LanguageTest {
private fun create(placeholder: String): Translator {
val data: LanguageData = mutableMapOf(
KEY.path to placeholder,
)
return Language("test", data)
}
@Test
fun none() {
val language = create("Hello world!")
assertEquals(language.translate(KEY)?.message, "Hello world!")
}
@Test
fun args() {
val language = create("%s %s")
assertEquals(language.translate(KEY, data = arrayOf("hello", "world"))?.message, "hello world")
}
@Test
fun numberArgs() {
val language = create("%s %d")
assertEquals(language.translate(KEY, data = arrayOf("hello", "world"))?.message, "hello world")
}
@Test
fun textArgs() {
val language = create("Hi %s, my name is %s and I like %s!")
assertEquals(language.translate(KEY, data = arrayOf("Gustaf", "Moritz", "sleeping"))?.message, "Hi Gustaf, my name is Moritz and I like sleeping!")
}
@Test
fun ordered() {
val language = create("Hi %2\$s, my name is %1\$s and I like %0\$s!")
assertEquals(language.translate(KEY, data = arrayOf("sleeping", "Moritz", "Gustaf"))?.message, "Hi Gustaf, my name is Moritz and I like sleeping!")
}
@Test
fun invalid() {
val language = create("hi %")
assertEquals(language.translate(KEY)?.message, "hi %")
}
@Test
fun invalid2() {
val language = create("hi % s")
assertEquals(language.translate(KEY)?.message, "hi % s")
}
@Test
fun invalid3() {
val language = create("hi %2$ s")
assertEquals(language.translate(KEY)?.message, "hi %2$ s")
}
@Test
fun escape() {
val language = create("%%s %%%s %%%%s %%%%%s")
assertEquals(language.translate(KEY)?.message, "%%s %%%s %%%%s %%%%%s")
}
@Test
fun complex() {
val language = create("Prefix, %s%2\$s again %s and %1\$s lastly %s and also %1\$s again!")
assertEquals(language.translate(KEY, data = arrayOf("aaa", "bbb", "ccc"))?.message, "Prefix, aaaccc again bbb and bbb lastly ccc and also bbb again!")
}
@Test
fun formatting() {
val language = create("§eHi %s, welcome!")
assertEquals(language.translate(KEY, data = arrayOf("§aMoritz"))?.legacyText, "§eHi §r§aMoritz§r§e, welcome!§r")
}
@Test
fun formatting2() {
val language = create("§eHi %s, welcome!")
assertEquals(language.translate(KEY, data = arrayOf("§aMoritz")), BaseComponent(TextComponent("Hi ").color(ChatColors.YELLOW), TextComponent("Moritz").color(ChatColors.GREEN), TextComponent(", welcome!").color(ChatColors.YELLOW)))
}
@Test
fun parent() {
val language = create("Hi %s, welcome!")
assertEquals(language.translate(KEY, parent = TextComponent("").color(ChatColors.YELLOW), data = arrayOf("§aMoritz"))?.legacyText, "§eHi §r§aMoritz§r§e, welcome!§r")
}
@Test
fun unavailableEmpty() {
val language = Language("test", mutableMapOf())
assertNull(language.translate(KEY)?.message)
}
@Test
fun unavailableForceEmpty() {
val language = Language("test", mutableMapOf())
assertEquals(language.forceTranslate(KEY).message, "minecraft:key")
}
@Test
fun unavailableData() {
val language = Language("test", mutableMapOf())
assertEquals(language.forceTranslate(KEY, data = arrayOf("data2")).message, "minecraft:key->[data2]")
}
@Test
fun fallbackData() {
val language = Language("test", mutableMapOf())
assertEquals(language.forceTranslate(KEY, fallback = "falling back %s!", data = arrayOf("data2")).message, "falling back data2!")
}
@Test
fun trailingData() {
val language = create("Hi %s!")
assertEquals(language.translate(KEY, data = arrayOf("Moritz", "trail me off"))?.message, "Hi Moritz!")
}
@Test
fun missingData() {
val language = create("Hi %s %s!")
assertEquals(language.translate(KEY, data = arrayOf("Moritz"))?.message, "Hi Moritz <null>!")
}
@Test
fun tailingIndex() {
val language = create("Hi %0\$s!!!")
assertEquals(language.translate(KEY, data = arrayOf(null, "not me"))?.message, "Hi !!!")
}
@Test
fun invalidIndex() {
val language = create("Hi %213\$s!!!")
assertEquals(language.translate(KEY, data = arrayOf("i am index one"))?.message, "Hi <null>!!!")
}
@Test
fun recursion() {
val language = create("Hi %s!")
assertEquals(language.translate(KEY, data = arrayOf("hah %0\$s"))?.message, "Hi hah %0\$s!")
}
companion object {
val KEY = "key".toResourceLocation()
}
}

View File

@ -108,7 +108,7 @@ internal class ChatComponentTest {
TextComponent("Test ").color(ChatColors.RED),
TextComponent("file:/home/moritz").color(ChatColors.GREEN),
)
val actual = ChatComponent.of("§cTest §afile:/home/moritz", restrictedMode = true)
val actual = ChatComponent.of("§cTest §afile:/home/moritz", restricted = true)
assertEquals(expected, actual)
}