diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java new file mode 100644 index 000000000..9f9eebb88 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java @@ -0,0 +1,93 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.translation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.VirtualComponent; +import net.kyori.adventure.text.VirtualComponentRenderer; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.ParsingException; +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; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class ArgumentTag implements TagResolver { + private static final String NAME = "argument"; + private static final String NAME_1 = "arg"; + + private final List argumentComponents; + private final Map namedArguments; + + ArgumentTag(final @NotNull List argumentComponents) { + this.argumentComponents = new ArrayList<>(Objects.requireNonNull(argumentComponents, "argumentComponents")); + + final Map namedArgumentMap = new HashMap<>(this.argumentComponents.size()); + for (final ComponentLike argument : this.argumentComponents) { + if (argument instanceof VirtualComponent) { + final VirtualComponentRenderer renderer = ((VirtualComponent) argument).renderer(); + + if (renderer instanceof NamedTranslationArgument) { + final NamedTranslationArgument namedArgument = (NamedTranslationArgument) argument; + namedArgumentMap.put(namedArgument.name(), namedArgument.translationArgument()); + } + } + } + + this.namedArguments = Collections.unmodifiableMap(namedArgumentMap); + } + + @Override + public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { + if (name.equals(NAME) || name.equals(NAME_1)) { + final int index = arguments.popOr("No argument number provided").asInt().orElseThrow(() -> ctx.newException("Invalid argument number", arguments)); + + if (index < 0 || index >= this.argumentComponents.size()) { + throw ctx.newException("Invalid argument number", arguments); + } + + return Tag.inserting(this.argumentComponents.get(index)); + } else { + final ComponentLike namedArgument = this.namedArguments.get(name); + + if (namedArgument != null) { + return Tag.inserting(namedArgument); + } else { + return null; + } + } + } + + @Override + public boolean has(final @NotNull String name) { + return name.equals(NAME) || name.equals(NAME_1) || this.namedArguments.containsKey(name); + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/MiniMessageTranslator.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/MiniMessageTranslator.java new file mode 100644 index 000000000..856bb6e3b --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/MiniMessageTranslator.java @@ -0,0 +1,125 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.translation; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.Objects; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.translation.Translator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link Translator} implementation that translates strings using MiniMessage. + * + *

To use this feature, you should extend this class, implementing the + * {@link #getMiniMessageString(String, Locale)} method to return the MiniMessage string + * for a given key and locale. + * After that, you can use the translator as-is using + * {@link #translate(TranslatableComponent, Locale)}, or automatically (depending on the + * implementing platform) using the {@link GlobalTranslator}.

+ * + *

This system supports arguments using {@code } tags (or {@code argument}, + * where {@code 0} is the index of the argument to use). + * Alternatively, you can use named arguments by creating the translatable component + * with {@link NamedTranslationArgument} as the arguments. + * The provided {@link NamedTranslationArgument#name() name} will be available for use in + * a tag as {@code }, in addition to the index-based {@code arg} tag.

+ * + * @see Translator + * @see GlobalTranslator + * @see NamedTranslationArgument + * @since 4.19.0 + */ +public abstract class MiniMessageTranslator implements Translator { + private final MiniMessage miniMessage; + + /** + * Constructor for a MiniMessageTranslator using the default MiniMessage instance. + * + * @see MiniMessage#miniMessage() + * @since 4.19.0 + */ + public MiniMessageTranslator() { + this(MiniMessage.miniMessage()); + } + + /** + * Constructor for a MiniMessageTranslator using a specific MiniMessage instance. + * + * @param miniMessage the MiniMessage instance + * @see MiniMessage#miniMessage() + * @since 4.19.0 + */ + public MiniMessageTranslator(final @NotNull MiniMessage miniMessage) { + this.miniMessage = Objects.requireNonNull(miniMessage, "miniMessage"); + } + + /** + * Returns a raw MiniMessage string for the given key. + * + *

If no string is found for the given key, returning {@code null} will use the + * {@link TranslatableComponent#fallback() translatable component's fallback} (or the + * key itself).

+ * + * @param key the key + * @param locale the locale + * @return the resulting MiniMessage string + * @since 4.19.0 + */ + @SuppressWarnings("checkstyle:MethodName") + protected abstract @Nullable String getMiniMessageString(final @NotNull String key, final @NotNull Locale locale); + + @Override + public final @Nullable MessageFormat translate(final @NotNull String key, final @NotNull Locale locale) { + return null; + } + + @Override + public final @Nullable Component translate(final @NotNull TranslatableComponent component, final @NotNull Locale locale) { + final String miniMessageString = this.getMiniMessageString(component.key(), locale); + + if (miniMessageString == null) { + return null; + } + + final Component resultingComponent; + + if (component.arguments().isEmpty()) { + resultingComponent = this.miniMessage.deserialize(miniMessageString); + } else { + resultingComponent = this.miniMessage.deserialize(miniMessageString, new ArgumentTag(component.arguments())); + } + + if (component.children().isEmpty()) { + return resultingComponent; + } else { + return resultingComponent.children(component.children()); + } + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgument.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgument.java new file mode 100644 index 000000000..c5d98ba0f --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgument.java @@ -0,0 +1,131 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.translation; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.TranslationArgument; +import net.kyori.adventure.text.TranslationArgumentLike; +import net.kyori.adventure.text.VirtualComponent; +import net.kyori.adventure.text.minimessage.tag.TagPattern; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link TranslationArgument} with an associated string name. + * + *

This is intended for use with {@link TranslatableComponent translatable components} + * used with a {@link MiniMessageTranslator} instance to allow {@code } tags.

+ * + *

Static methods on this class work by creating + * {@link VirtualComponent virtual components} that store instances of this class. + * The MiniMessage translator instance detects these virtual components to use the name + * provided as tag names to replace the {@code } tag.

+ * + *

As the names provided to all static methods in this class are used to create tags, + * they must be valid tag names.

+ * + * @since 4.19.0 + */ +@ApiStatus.NonExtendable +public interface NamedTranslationArgument { + /** + * Create a named boolean argument. + * + * @param name the name + * @param value the value + * @return the named argument + * @since 4.19.0 + */ + static @NotNull ComponentLike bool(final @TagPattern @NotNull String name, final boolean value) { + return argument(name, TranslationArgument.bool(value)); + } + + /** + * Create a named numeric argument. + * + * @param name the name + * @param value the value + * @return the named argument + * @since 4.19.0 + */ + static @NotNull ComponentLike numeric(final @TagPattern @NotNull String name, final @NotNull Number value) { + return argument(name, TranslationArgument.numeric(value)); + } + + /** + * Create a named component argument. + * + * @param name the name + * @param value the value + * @return the named argument + * @since 4.19.0 + */ + static @NotNull ComponentLike component(final @TagPattern @NotNull String name, final @NotNull ComponentLike value) { + return argument(name, TranslationArgument.component(value)); + } + + /** + * Create a named translation argument. + * + * @param name the name + * @param argument the translation argument + * @return the named argument + * @since 4.19.0 + */ + static @NotNull ComponentLike argument(final @TagPattern @NotNull String name, final @NotNull TranslationArgumentLike argument) { + return argument(name, requireNonNull(argument, "argument").asTranslationArgument()); + } + + /** + * Create a named translation argument. + * + * @param name the name + * @param argument the translation argument + * @return the named argument + * @since 4.19.0 + */ + static @NotNull ComponentLike argument(final @TagPattern @NotNull String name, final @NotNull TranslationArgument argument) { + return Component.virtual(Void.class, new NamedTranslationArgumentImpl(name, argument)); + } + + /** + * The name of this translation argument. + * + * @return the name + * @since 4.19.0 + */ + @TagPattern @NotNull String name(); + + /** + * The backing translation argument. + * + * @return the translation argument + * @since 4.19.0 + **/ + @NotNull TranslationArgument translationArgument(); +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgumentImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgumentImpl.java new file mode 100644 index 000000000..6607cf261 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgumentImpl.java @@ -0,0 +1,68 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.minimessage.translation; + +import java.util.Objects; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TranslationArgument; +import net.kyori.adventure.text.VirtualComponentRenderer; +import net.kyori.adventure.text.minimessage.internal.TagInternals; +import net.kyori.adventure.text.minimessage.tag.TagPattern; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; + +final class NamedTranslationArgumentImpl implements NamedTranslationArgument, VirtualComponentRenderer { + + private final @TagPattern @NotNull String name; + private final @NotNull TranslationArgument translationArgument; + + NamedTranslationArgumentImpl(final @TagPattern @NotNull String name, final @NotNull TranslationArgument argument) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(argument, "argument"); + TagInternals.assertValidTagName(name); + + this.name = name; + this.translationArgument = argument; + } + + @Override + public @TagPattern @NotNull String name() { + return this.name; + } + + @Override + public @NotNull TranslationArgument translationArgument() { + return this.translationArgument; + } + + @Override + public @UnknownNullability ComponentLike apply(final @NotNull Void context) { + return this.translationArgument; + } + + @Override + public @NotNull String fallbackString() { + return ""; // Not for display purposes. + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/package-info.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/package-info.java new file mode 100644 index 000000000..0a6f9874c --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/package-info.java @@ -0,0 +1,27 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * Tools for working with Adventure's translation API. + */ +package net.kyori.adventure.text.minimessage.translation;