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 index a7adce902..4e0db7c6b 100644 --- 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 @@ -1,6 +1,33 @@ +/* + * 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.minimessage.Context; @@ -11,29 +38,50 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class ArgumentTag implements TagResolver { +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; - public ArgumentTag(final @NotNull List argumentComponents) { - this.argumentComponents = Objects.requireNonNull(argumentComponents, "argumentComponents"); + 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 NamedTranslationArgument) { + final NamedTranslationArgument namedArgument = (NamedTranslationArgument) argument; + namedArgumentMap.put(namedArgument.name(), namedArgument); + } + } + + this.namedArguments = Collections.unmodifiableMap(namedArgumentMap); } @Override public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { - final int index = arguments.popOr("No argument number provided").asInt().orElseThrow(() -> ctx.newException("Invalid argument number", arguments)); + 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 >= argumentComponents.size()) { - throw 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 NamedTranslationArgument namedArgument = this.namedArguments.get(name); - return Tag.inserting(argumentComponents.get(index)); + 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); + 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 index 627c1ccb3..856bb6e3b 100644 --- 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 @@ -1,7 +1,7 @@ /* * This file is part of adventure, licensed under the MIT License. * - * Copyright (c) 2017-2022 KyoriPowered + * 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 @@ -29,22 +29,70 @@ 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; - 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 @@ -53,8 +101,8 @@ public MiniMessageTranslator(final @NotNull MiniMessage miniMessage) { } @Override - public @Nullable Component translate(final @NotNull TranslatableComponent component, final @NotNull Locale locale) { - final String miniMessageString = getMiniMessageString(component.key(), locale); + 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; @@ -62,10 +110,10 @@ public MiniMessageTranslator(final @NotNull MiniMessage miniMessage) { final Component resultingComponent; - if (component.args().isEmpty()) { - resultingComponent = MiniMessage.miniMessage().deserialize(miniMessageString); + if (component.arguments().isEmpty()) { + resultingComponent = this.miniMessage.deserialize(miniMessageString); } else { - resultingComponent = MiniMessage.miniMessage().deserialize(miniMessageString, new ArgumentTag(component.args())); + resultingComponent = this.miniMessage.deserialize(miniMessageString, new ArgumentTag(component.arguments())); } if (component.children().isEmpty()) { 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..317a8b487 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgument.java @@ -0,0 +1,111 @@ +/* + * 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.ComponentLike; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.TranslationArgument; +import net.kyori.adventure.text.TranslationArgumentLike; +import net.kyori.adventure.text.minimessage.tag.TagPattern; +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.

+ * + * @since 4.19.0 + */ +public interface NamedTranslationArgument extends TranslationArgumentLike { + /** + * Create a named boolean argument. + * + * @param name the name + * @param value the value + * @return the named argument + * @since 4.19.0 + */ + static @NotNull NamedTranslationArgument 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 NamedTranslationArgument 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 NamedTranslationArgument 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 NamedTranslationArgument 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 NamedTranslationArgument argument(final @TagPattern @NotNull String name, final @NotNull TranslationArgument argument) { + return new NamedTranslationArgumentImpl(name, argument); + } + + /** + * The name of this translation argument. + * + * @return the name + * @since 4.19.0 + */ + @TagPattern @NotNull String name(); +} 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..5873e4a15 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/NamedTranslationArgumentImpl.java @@ -0,0 +1,55 @@ +/* + * 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.TranslationArgument; +import net.kyori.adventure.text.minimessage.internal.TagInternals; +import net.kyori.adventure.text.minimessage.tag.TagPattern; +import org.jetbrains.annotations.NotNull; + +final class NamedTranslationArgumentImpl implements NamedTranslationArgument { + + private final @TagPattern @NotNull String name; + private final @NotNull TranslationArgument argument; + + 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.argument = argument; + } + + @Override + public @TagPattern @NotNull String name() { + return this.name; + } + + @Override + public @NotNull TranslationArgument asTranslationArgument() { + return this.argument; + } +}