Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add support for translatable component fallbacks (#863) #868

Merged
merged 10 commits into from
Mar 14, 2023
390 changes: 386 additions & 4 deletions api/src/main/java/net/kyori/adventure/text/Component.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -119,12 +120,35 @@ public interface TranslatableComponent extends BuildableComponent<TranslatableCo
@Contract(pure = true)
@NotNull TranslatableComponent args(final @NotNull List<? extends ComponentLike> 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
*/
@Nullable String fallback();
NoahvdAa marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
*/
@Contract(pure = true)
@NotNull TranslatableComponent fallback(final @NotNull String fallback);
zml2008 marked this conversation as resolved.
Show resolved Hide resolved

@Override
default @NotNull Stream<? extends ExaminableProperty> 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()
);
Expand Down Expand Up @@ -208,5 +232,17 @@ interface Builder extends ComponentBuilder<TranslatableComponent, Builder> {
*/
@Contract("_ -> this")
@NotNull Builder args(final @NotNull List<? extends ComponentLike> 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
*/
@Contract("_ -> this")
@NotNull Builder fallback(final @Nullable String fallback);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,29 @@
import static java.util.Objects.requireNonNull;

final class TranslatableComponentImpl extends AbstractComponent implements TranslatableComponent {
static TranslatableComponent create(final @NotNull List<Component> children, final @NotNull Style style, final @NotNull String key, final @NotNull ComponentLike@NotNull[] args) {
static TranslatableComponent create(final @NotNull List<Component> 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<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String key, final @NotNull List<? extends ComponentLike> args) {
static TranslatableComponent create(final @NotNull List<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String key, final @Nullable String fallback, final @NotNull List<? extends ComponentLike> 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<Component> args;

TranslatableComponentImpl(final @NotNull List<Component> children, final @NotNull Style style, final @NotNull String key, final @NotNull List<Component> args) {
TranslatableComponentImpl(final @NotNull List<Component> children, final @NotNull Style style, final @NotNull String key, final @Nullable String fallback, final @NotNull List<Component> args) {
super(children, style);
this.key = key;
this.fallback = fallback;
this.args = args;
}

Expand All @@ -68,7 +71,7 @@ static TranslatableComponent create(final @NotNull List<? extends ComponentLike>
@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
Expand All @@ -78,22 +81,32 @@ static TranslatableComponent create(final @NotNull List<? extends ComponentLike>

@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<? extends ComponentLike> 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<? extends ComponentLike> 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
Expand All @@ -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;
}
Expand All @@ -125,6 +139,7 @@ public String toString() {

static final class BuilderImpl extends AbstractComponentBuilder<TranslatableComponent, Builder> implements TranslatableComponent.Builder {
private @Nullable String key;
private @Nullable String fallback;
private List<? extends Component> args = Collections.emptyList();

BuilderImpl() {
Expand All @@ -134,6 +149,7 @@ static final class BuilderImpl extends AbstractComponentBuilder<TranslatableComp
super(component);
this.key = component.key();
this.args = component.args();
this.fallback = component.fallback();
}

@Override
Expand Down Expand Up @@ -173,10 +189,16 @@ static final class BuilderImpl extends AbstractComponentBuilder<TranslatableComp
return this;
}

@Override
public @NotNull Builder fallback(final @Nullable String fallback) {
this.fallback = fallback;
return this;
}

@Override
public @NotNull TranslatableComponent build() {
if (this.key == null) throw new IllegalStateException("key must be set");
return create(this.children, this.buildStyle(), this.key, this.args);
return create(this.children, this.buildStyle(), this.key, this.fallback, this.args);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ final class ComponentTypeSerializer implements TypeSerializer<Component> {
static final String TEXT = "text";
static final String TRANSLATE = "translate";
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";
Expand Down Expand Up @@ -207,6 +208,7 @@ public void serialize(final @NotNull TypeToken<?> type, final @Nullable Componen
with.appendListNode().setValue(TYPE, arg);
}
}
value.getNode(FALLBACK).setValue(tc.fallback());
} else if (src instanceof ScoreComponent) {
final ScoreComponent sc = (ScoreComponent) src;
final ConfigurationNode score = value.getNode(SCORE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ final class ComponentTypeSerializer implements TypeSerializer<Component> {
static final String TEXT = "text";
static final String TRANSLATE = "translate";
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";
Expand Down Expand Up @@ -205,6 +206,7 @@ public void serialize(final @NotNull Type type, final @Nullable Component src, f
with.appendListNode().set(Component.class, arg);
}
}
value.node(FALLBACK).set(tc.fallback());
} else if (src instanceof ScoreComponent) {
final ScoreComponent sc = (ScoreComponent) src;
final ConfigurationNode score = value.node(SCORE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private StandardTags() {
ColorTagResolver.INSTANCE,
KeybindTag.RESOLVER,
TranslatableTag.RESOLVER,
TranslatableFallbackTag.RESOLVER,
InsertionTag.RESOLVER,
FontTag.RESOLVER,
DecorationTag.RESOLVER,
Expand Down Expand Up @@ -145,6 +146,18 @@ private StandardTags() {
return TranslatableTag.RESOLVER;
}

/**
* Get a resolver for the {@value TranslatableFallbackTag#TRANSLATE_OR} tag.
*
* <p>This tag also responds to {@value TranslatableFallbackTag#LANG_OR} and {@value TranslatableFallbackTag#TR_OR}.</p>
*
* @return a resolver for the {@value TranslatableFallbackTag#TRANSLATE_OR} tag
* @since 4.10.0
zml2008 marked this conversation as resolved.
Show resolved Hide resolved
*/
public static @NotNull TagResolver translatableFallback() {
return TranslatableFallbackTag.RESOLVER;
}

/**
* Get a resolver for the {@value InsertionTag#INSERTION} tag.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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
*/
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<Component> 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;
NoahvdAa marked this conversation as resolved.
Show resolved Hide resolved

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);
}
};
}
}
Loading