diff --git a/api/src/main/java/net/kyori/adventure/text/Component.java b/api/src/main/java/net/kyori/adventure/text/Component.java index f45d996db..6727b0b36 100644 --- a/api/src/main/java/net/kyori/adventure/text/Component.java +++ b/api/src/main/java/net/kyori/adventure/text/Component.java @@ -1278,6 +1278,34 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex return translatable(requireNonNull(translatable, "translatable").translationKey(), Style.empty()); } + /** + * Creates a translatable component with a translation key and an optional fallback string. + * + * @param key the translation key + * @param fallback the fallback string + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback) { + return translatable(key, fallback, Style.empty()); + } + + /** + * Creates a translatable component with a translation key and an optional fallback string. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, Style.empty()); + } + /** * Creates a translatable component with a translation key and styling. * @@ -1288,7 +1316,7 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex */ @Contract(value = "_, _ -> new", pure = true) static @NotNull TranslatableComponent translatable(final @NotNull String key, final @NotNull Style style) { - return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, Collections.emptyList()); + return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, null, Collections.emptyList()); } /** @@ -1304,6 +1332,224 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex return translatable(requireNonNull(translatable, "translatable").translationKey(), style); } + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param key the translation key + * @param fallback the fallback string + * @param style the style + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull Style style) { + return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, fallback, Collections.emptyList()); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param style the style + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull Style style) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, style); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param key the translation key + * @param fallback the fallback string + * @param style the style + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull StyleBuilderApplicable... style) { + return translatable(requireNonNull(key, "key"), fallback, Style.style(style)); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param style the style + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull Iterable style) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, Style.style(style)); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param key the translation key + * @param fallback the fallback string + * @param args the translation arguments + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull ComponentLike@NotNull... args) { + return translatable(key, fallback, Style.empty(), args); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param args the translation arguments + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull ComponentLike@NotNull... args) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, args); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param key the translation key + * @param fallback the fallback string + * @param style the style + * @param args the translation arguments + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull Style style, final @NotNull ComponentLike@NotNull... args) { + return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, fallback, requireNonNull(args, "args")); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and styling. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param style the style + * @param args the translation arguments + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull Style style, final @NotNull ComponentLike@NotNull... args) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, style, args); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param key the translation key + * @param fallback the fallback string + * @param style the style + * @param args the translation arguments + * @return a translatable component + * @since 4.0.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull Style style, final @NotNull List args) { + return TranslatableComponentImpl.create(Collections.emptyList(), style, key, fallback, requireNonNull(args, "args")); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param style the style + * @param args the translation arguments + * @return a translatable component + * @since 4.8.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull Style style, final @NotNull List args) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, style, args); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param key the translation key + * @param fallback the fallback string + * @param args the translation arguments + * @param style the style + * @return a translatable component + * @since 4.0.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull List args, final @NotNull Iterable style) { + return TranslatableComponentImpl.create(Collections.emptyList(), Style.style(style), key, fallback, requireNonNull(args, "args")); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param args the translation arguments + * @param style the style + * @return a translatable component + * @since 4.8.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull List args, final @NotNull Iterable style) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, args, style); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param key the translation key + * @param fallback the fallback string + * @param args the translation arguments + * @param style the style + * @return a translatable component + * @since 4.0.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull String key, final @Nullable String fallback, final @NotNull List args, final @NotNull StyleBuilderApplicable... style) { + return TranslatableComponentImpl.create(Collections.emptyList(), Style.style(style), key, fallback, requireNonNull(args, "args")); + } + + /** + * Creates a translatable component with a translation key, optional fallback string, and arguments. + * + * @param translatable the translatable object to get the key from + * @param fallback the fallback string + * @param args the translation arguments + * @param style the style + * @return a translatable component + * @since 4.8.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + static @NotNull TranslatableComponent translatable(final @NotNull Translatable translatable, final @Nullable String fallback, final @NotNull List args, final @NotNull StyleBuilderApplicable... style) { + return translatable(requireNonNull(translatable, "translatable").translationKey(), fallback, args, style); + } + /** * Creates a translatable component with a translation key, and optional color. * @@ -1423,7 +1669,7 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex */ @Contract(value = "_, _, _ -> new", pure = true) static @NotNull TranslatableComponent translatable(final @NotNull String key, final @NotNull Style style, final @NotNull ComponentLike@NotNull... args) { - return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, requireNonNull(args, "args")); + return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, null, requireNonNull(args, "args")); } /** @@ -1508,7 +1754,7 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex */ @Contract(value = "_, _ -> new", pure = true) static @NotNull TranslatableComponent translatable(final @NotNull String key, final @NotNull List args) { - return TranslatableComponentImpl.create(Collections.emptyList(), Style.empty(), key, requireNonNull(args, "args")); + return TranslatableComponentImpl.create(Collections.emptyList(), Style.empty(), key, null, requireNonNull(args, "args")); } /** @@ -1535,7 +1781,7 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex */ @Contract(value = "_, _, _ -> new", pure = true) static @NotNull TranslatableComponent translatable(final @NotNull String key, final @NotNull Style style, final @NotNull List args) { - return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, requireNonNull(args, "args")); + return TranslatableComponentImpl.create(Collections.emptyList(), requireNonNull(style, "style"), key, null, requireNonNull(args, "args")); } /** diff --git a/api/src/main/java/net/kyori/adventure/text/TranslatableComponent.java b/api/src/main/java/net/kyori/adventure/text/TranslatableComponent.java index a360bfbd5..809fd728d 100644 --- a/api/src/main/java/net/kyori/adventure/text/TranslatableComponent.java +++ b/api/src/main/java/net/kyori/adventure/text/TranslatableComponent.java @@ -34,6 +34,7 @@ import net.kyori.examination.ExaminableProperty; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * A component that can display translated text. @@ -119,12 +120,37 @@ public interface TranslatableComponent extends BuildableComponent args); + /** + * Gets the translation fallback text for this component. + * The fallback text will be shown when the client doesn't know the + * translation key used in the translatable component. + * + * @return the fallback string + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Nullable String fallback(); + + /** + * Sets the translation fallback text for this component. + * The fallback text will be shown when the client doesn't know the + * translation key used in the translatable component. + * + * @param fallback the fallback string + * @return a translatable component + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract(pure = true) + @NotNull TranslatableComponent fallback(final @Nullable String fallback); + @Override default @NotNull Stream examinableProperties() { return Stream.concat( Stream.of( ExaminableProperty.of("key", this.key()), - ExaminableProperty.of("args", this.args()) + ExaminableProperty.of("args", this.args()), + ExaminableProperty.of("fallback", this.fallback()) ), BuildableComponent.super.examinableProperties() ); @@ -208,5 +234,18 @@ interface Builder extends ComponentBuilder { */ @Contract("_ -> this") @NotNull Builder args(final @NotNull List args); + + /** + * Sets the translation fallback text. + * The fallback text will be shown when the client doesn't know the + * translation key used in the translatable component. + * + * @param fallback the fallback string + * @return this builder + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ + @Contract("_ -> this") + @NotNull Builder fallback(final @Nullable String fallback); } } diff --git a/api/src/main/java/net/kyori/adventure/text/TranslatableComponentImpl.java b/api/src/main/java/net/kyori/adventure/text/TranslatableComponentImpl.java index b0418e558..df7f1c735 100644 --- a/api/src/main/java/net/kyori/adventure/text/TranslatableComponentImpl.java +++ b/api/src/main/java/net/kyori/adventure/text/TranslatableComponentImpl.java @@ -37,26 +37,29 @@ import static java.util.Objects.requireNonNull; final class TranslatableComponentImpl extends AbstractComponent implements TranslatableComponent { - static TranslatableComponent create(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @NotNull ComponentLike@NotNull[] args) { + static TranslatableComponent create(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @Nullable String fallback, final @NotNull ComponentLike@NotNull[] args) { requireNonNull(args, "args"); - return create(children, style, key, Arrays.asList(args)); + return create(children, style, key, fallback, Arrays.asList(args)); } - static TranslatableComponent create(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @NotNull List args) { + static TranslatableComponent create(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @Nullable String fallback, final @NotNull List args) { return new TranslatableComponentImpl( ComponentLike.asComponents(children, IS_NOT_EMPTY), requireNonNull(style, "style"), requireNonNull(key, "key"), + fallback, ComponentLike.asComponents(args) // Since translation arguments can be indexed, empty components are also included. ); } private final String key; + private final @Nullable String fallback; private final List args; - TranslatableComponentImpl(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @NotNull List args) { + TranslatableComponentImpl(final @NotNull List children, final @NotNull Style style, final @NotNull String key, final @Nullable String fallback, final @NotNull List args) { super(children, style); this.key = key; + this.fallback = fallback; this.args = args; } @@ -68,7 +71,7 @@ static TranslatableComponent create(final @NotNull List @Override public @NotNull TranslatableComponent key(final @NotNull String key) { if (Objects.equals(this.key, key)) return this; - return create(this.children, this.style, key, this.args); + return create(this.children, this.style, key, this.fallback, this.args); } @Override @@ -78,22 +81,32 @@ static TranslatableComponent create(final @NotNull List @Override public @NotNull TranslatableComponent args(final @NotNull ComponentLike@NotNull... args) { - return create(this.children, this.style, this.key, args); + return create(this.children, this.style, this.key, this.fallback, args); } @Override public @NotNull TranslatableComponent args(final @NotNull List args) { - return create(this.children, this.style, this.key, args); + return create(this.children, this.style, this.key, this.fallback, args); + } + + @Override + public @Nullable String fallback() { + return this.fallback; + } + + @Override + public @NotNull TranslatableComponent fallback(final @Nullable String fallback) { + return create(this.children, this.style, this.key, fallback, this.args); } @Override public @NotNull TranslatableComponent children(final @NotNull List children) { - return create(children, this.style, this.key, this.args); + return create(children, this.style, this.key, this.fallback, this.args); } @Override public @NotNull TranslatableComponent style(final @NotNull Style style) { - return create(this.children, style, this.key, this.args); + return create(this.children, style, this.key, this.fallback, this.args); } @Override @@ -102,13 +115,14 @@ public boolean equals(final @Nullable Object other) { if (!(other instanceof TranslatableComponent)) return false; if (!super.equals(other)) return false; final TranslatableComponent that = (TranslatableComponent) other; - return Objects.equals(this.key, that.key()) && Objects.equals(this.args, that.args()); + return Objects.equals(this.key, that.key()) && Objects.equals(this.fallback, that.fallback()) && Objects.equals(this.args, that.args()); } @Override public int hashCode() { int result = super.hashCode(); result = (31 * result) + this.key.hashCode(); + result = (31 * result) + Objects.hashCode(this.fallback); result = (31 * result) + this.args.hashCode(); return result; } @@ -125,6 +139,7 @@ public String toString() { static final class BuilderImpl extends AbstractComponentBuilder implements TranslatableComponent.Builder { private @Nullable String key; + private @Nullable String fallback; private List args = Collections.emptyList(); BuilderImpl() { @@ -134,6 +149,7 @@ static final class BuilderImpl extends AbstractComponentBuilder { static final String TEXT = "text"; static final String TRANSLATE = "translate"; static final String TRANSLATE_WITH = "with"; + static final String TRANSLATE_FALLBACK = "fallback"; static final String SCORE = "score"; static final String SCORE_NAME = "name"; static final String SCORE_OBJECTIVE = "objective"; @@ -119,17 +120,22 @@ final class ComponentTypeSerializer implements TypeSerializer { if (children.containsKey(TEXT)) { component = Component.text().content(children.get(TEXT).getString()); } else if (children.containsKey(TRANSLATE)) { + final TranslatableComponent.Builder builder; final String key = children.get(TRANSLATE).getString(); if (!children.containsKey(TRANSLATE_WITH)) { - component = Component.translatable().key(key); + builder = Component.translatable().key(key); } else { final ConfigurationNode with = children.get(TRANSLATE_WITH); if (!with.isList()) { throw new ObjectMappingException("Expected " + TRANSLATE_WITH + " to be a list"); } final List args = with.getValue(LIST_TYPE); - component = Component.translatable().key(key).args(args); + builder = Component.translatable().key(key).args(args); } + if (children.containsKey(TRANSLATE_FALLBACK)) { + builder.fallback(children.get(TRANSLATE_FALLBACK).getString()); + } + component = builder; } else if (children.containsKey(SCORE)) { final ConfigurationNode score = children.get(SCORE); final ConfigurationNode name = score.getNode(SCORE_NAME); @@ -207,6 +213,7 @@ public void serialize(final @NotNull TypeToken type, final @Nullable Componen with.appendListNode().setValue(TYPE, arg); } } + value.getNode(TRANSLATE_FALLBACK).setValue(tc.fallback()); } else if (src instanceof ScoreComponent) { final ScoreComponent sc = (ScoreComponent) src; final ConfigurationNode score = value.getNode(SCORE); diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ComponentTypeSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ComponentTypeSerializer.java index afa1b5701..3d62d0bbb 100644 --- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ComponentTypeSerializer.java +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ComponentTypeSerializer.java @@ -53,6 +53,7 @@ final class ComponentTypeSerializer implements TypeSerializer { static final String TEXT = "text"; static final String TRANSLATE = "translate"; static final String TRANSLATE_WITH = "with"; + static final String TRANSLATE_FALLBACK = "fallback"; static final String SCORE = "score"; static final String SCORE_NAME = "name"; static final String SCORE_OBJECTIVE = "objective"; @@ -117,17 +118,22 @@ final class ComponentTypeSerializer implements TypeSerializer { if (children.containsKey(TEXT)) { component = Component.text().content(children.get(TEXT).getString()); } else if (children.containsKey(TRANSLATE)) { - final String key = children.get(TRANSLATE).getString(); - if (!children.containsKey(TRANSLATE_WITH)) { - component = Component.translatable().key(key); - } else { + final TranslatableComponent.Builder builder = Component.translatable() + .key(children.get(TRANSLATE).getString()); + + if (children.containsKey(TRANSLATE_WITH)) { final ConfigurationNode with = children.get(TRANSLATE_WITH); if (!with.isList()) { throw new SerializationException("Expected " + TRANSLATE_WITH + " to be a list"); } final List args = with.get(LIST_TYPE); - component = Component.translatable().key(key).args(args); + builder.args(args); + } + + if (children.containsKey(TRANSLATE_FALLBACK)) { + builder.fallback(children.get(TRANSLATE_FALLBACK).getString()); } + component = builder; } else if (children.containsKey(SCORE)) { final ConfigurationNode score = children.get(SCORE); final ConfigurationNode name = score.node(SCORE_NAME); @@ -205,6 +211,7 @@ public void serialize(final @NotNull Type type, final @Nullable Component src, f with.appendListNode().set(Component.class, arg); } } + value.node(TRANSLATE_FALLBACK).set(tc.fallback()); } else if (src instanceof ScoreComponent) { final ScoreComponent sc = (ScoreComponent) src; final ConfigurationNode score = value.node(SCORE); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java index c27991d77..72ad26a1a 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java @@ -52,6 +52,7 @@ private StandardTags() { ColorTagResolver.INSTANCE, KeybindTag.RESOLVER, TranslatableTag.RESOLVER, + TranslatableFallbackTag.RESOLVER, InsertionTag.RESOLVER, FontTag.RESOLVER, DecorationTag.RESOLVER, @@ -145,6 +146,18 @@ private StandardTags() { return TranslatableTag.RESOLVER; } + /** + * Get a resolver for the {@value TranslatableFallbackTag#TRANSLATE_OR} tag. + * + *

This tag also responds to {@value TranslatableFallbackTag#LANG_OR} and {@value TranslatableFallbackTag#TR_OR}.

+ * + * @return a resolver for the {@value TranslatableFallbackTag#TRANSLATE_OR} tag + * @since 4.13.0 + */ + public static @NotNull TagResolver translatableFallback() { + return TranslatableFallbackTag.RESOLVER; + } + /** * Get a resolver for the {@value InsertionTag#INSERTION} tag. * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTag.java new file mode 100644 index 000000000..42bf0faa9 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTag.java @@ -0,0 +1,89 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 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.tag.standard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.internal.serializer.Emitable; +import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; +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.Nullable; + +/** + * Insert a translation component into the result, with a fallback string. + * + * @since 4.13.0 + * @sinceMinecraft 1.19.4 + */ +final class TranslatableFallbackTag { + private static final String TR_OR = "tr_or"; + private static final String TRANSLATE_OR = "translate_or"; + private static final String LANG_OR = "lang_or"; + + static final TagResolver RESOLVER = SerializableResolver.claimingComponent( + StandardTags.names(LANG_OR, TRANSLATE_OR, TR_OR), + TranslatableFallbackTag::create, + TranslatableFallbackTag::claim + ); + + private TranslatableFallbackTag() { + } + + static Tag create(final ArgumentQueue args, final Context ctx) throws ParsingException { + final String key = args.popOr("A translation key is required").value(); + final String fallback = args.popOr("A fallback messages is required").value(); + final List with; + if (args.hasNext()) { + with = new ArrayList<>(); + while (args.hasNext()) { + with.add(ctx.deserialize(args.pop().value())); + } + } else { + with = Collections.emptyList(); + } + + return Tag.inserting(Component.translatable(key, fallback, with)); + } + + static @Nullable Emitable claim(final Component input) { + if (!(input instanceof TranslatableComponent) || ((TranslatableComponent) input).fallback() == null) return null; + + final TranslatableComponent tr = (TranslatableComponent) input; + return emit -> { + emit.tag(LANG_OR); + emit.argument(tr.key()); + emit.argument(tr.fallback()); + for (final Component with : tr.args()) { + emit.argument(with); + } + }; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTag.java index 3340deb9d..7dd4b654d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTag.java @@ -72,7 +72,7 @@ static Tag create(final ArgumentQueue args, final Context ctx) throws ParsingExc } static @Nullable Emitable claim(final Component input) { - if (!(input instanceof TranslatableComponent)) return null; + if (!(input instanceof TranslatableComponent) || ((TranslatableComponent) input).fallback() != null) return null; final TranslatableComponent tr = (TranslatableComponent) input; return emit -> { diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTagTest.java new file mode 100644 index 000000000..f99fa8919 --- /dev/null +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableFallbackTagTest.java @@ -0,0 +1,70 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 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.tag.standard; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.AbstractTest; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; + +class TranslatableFallbackTagTest extends AbstractTest { + @Test + void testSerializeTranslatable() { + final String expected = "You should get a !"; + + final TextComponent.Builder builder = Component.text() + .content("You should get a ") + .append(Component.translatable("block.minecraft.diamond_block").fallback("Fallback :(")) + .append(Component.text("!")); + + this.assertSerializedEquals(expected, builder); + } + + @Test + void testSerializeTranslatableWithArgs() { + final String expected = ":arg' 1\":'arg 2'>"; + + final Component translatable = Component.translatable() + .key("some_key") + .fallback("fallback") + .args(text(":arg' 1", NamedTextColor.RED), text("arg 2", NamedTextColor.BLUE)) + .build(); + + this.assertSerializedEquals(expected, translatable); + } + + @Test + void testTranslatable() { + final String input = "You should get a !"; + final Component expected = text("You should get a ") + .append(translatable("block.minecraft.diamond_block").fallback("Diamond Block") + .append(text("!"))); + + this.assertParsedEquals(expected, input); + } +} diff --git a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ComponentSerializerImpl.java b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ComponentSerializerImpl.java index 64be4675b..e938a1a3e 100644 --- a/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ComponentSerializerImpl.java +++ b/text-serializer-gson/src/main/java/net/kyori/adventure/text/serializer/gson/ComponentSerializerImpl.java @@ -56,7 +56,9 @@ final class ComponentSerializerImpl extends TypeAdapter { static final String TEXT = "text"; static final String TRANSLATE = "translate"; + static final String TRANSLATE_FALLBACK = "fallback"; static final String TRANSLATE_WITH = "with"; + static final String FALLBACK = "fallback"; static final String SCORE = "score"; static final String SCORE_NAME = "name"; static final String SCORE_OBJECTIVE = "objective"; @@ -115,6 +117,7 @@ private ComponentSerializerImpl(final Gson gson) { // type specific String text = null; String translate = null; + String translateFallback = null; List translateWith = null; String scoreName = null; String scoreObjective = null; @@ -135,6 +138,8 @@ private ComponentSerializerImpl(final Gson gson) { text = readString(in); } else if (fieldName.equals(TRANSLATE)) { translate = in.nextString(); + } else if (fieldName.equals(TRANSLATE_FALLBACK)) { + translateFallback = in.nextString(); } else if (fieldName.equals(TRANSLATE_WITH)) { translateWith = this.gson.fromJson(in, COMPONENT_LIST_TYPE); } else if (fieldName.equals(SCORE)) { @@ -183,9 +188,9 @@ private ComponentSerializerImpl(final Gson gson) { builder = Component.text().content(text); } else if (translate != null) { if (translateWith != null) { - builder = Component.translatable().key(translate).args(translateWith); + builder = Component.translatable().key(translate).fallback(translateFallback).args(translateWith); } else { - builder = Component.translatable().key(translate); + builder = Component.translatable().key(translate).fallback(translateFallback); } } else if (scoreName != null && scoreObjective != null) { if (scoreValue == null) { @@ -261,10 +266,19 @@ public void write(final JsonWriter out, final Component value) throws IOExceptio final TranslatableComponent translatable = (TranslatableComponent) value; out.name(TRANSLATE); out.value(translatable.key()); + final @Nullable String fallback = translatable.fallback(); + if (fallback != null) { + out.name(TRANSLATE_FALLBACK); + out.value(fallback); + } if (!translatable.args().isEmpty()) { out.name(TRANSLATE_WITH); this.gson.toJson(translatable.args(), COMPONENT_LIST_TYPE, out); } + if (translatable.fallback() != null) { + out.name(FALLBACK); + out.value(translatable.fallback()); + } } else if (value instanceof ScoreComponent) { final ScoreComponent score = (ScoreComponent) value; out.name(SCORE); diff --git a/text-serializer-gson/src/test/java/net/kyori/adventure/text/serializer/gson/TranslatableComponentTest.java b/text-serializer-gson/src/test/java/net/kyori/adventure/text/serializer/gson/TranslatableComponentTest.java index 7f86afce4..089087e6d 100644 --- a/text-serializer-gson/src/test/java/net/kyori/adventure/text/serializer/gson/TranslatableComponentTest.java +++ b/text-serializer-gson/src/test/java/net/kyori/adventure/text/serializer/gson/TranslatableComponentTest.java @@ -41,6 +41,20 @@ void testNoArgs() { this.test(Component.translatable(KEY), object(json -> json.addProperty(ComponentSerializerImpl.TRANSLATE, KEY))); } + @Test + void testFallback() { + this.test( + Component.translatable() + .key("thisIsA") + .fallback("This is a test.") + .build(), + object(json -> { + json.addProperty(ComponentSerializerImpl.TRANSLATE, "thisIsA"); + json.addProperty(ComponentSerializerImpl.TRANSLATE_FALLBACK, "This is a test."); + }) + ); + } + @Test void testSingleArgWithEvents() { final UUID id = UUID.fromString("eb121687-8b1a-4944-bd4d-e0a818d9dfe2");