package net.minecraft.network.chat.contents; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.minecraft.commands.CommandSourceStack; import net.minecraft.locale.Language; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentContents; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.entity.Entity; import org.jetbrains.annotations.Nullable; public class TranslatableContents implements ComponentContents { public static final Object[] NO_ARGS = new Object[0]; private static final Codec PRIMITIVE_ARG_CODEC = ExtraCodecs.JAVA.validate(TranslatableContents::filterAllowedArguments); private static final Codec ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, ComponentSerialization.CODEC) .xmap( either -> either.map(object -> object, component -> Objects.requireNonNullElse(component.tryCollapseToString(), component)), object -> object instanceof Component component ? Either.right(component) : Either.left(object) ); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( instance -> instance.group( Codec.STRING.fieldOf("translate").forGetter(translatableContents -> translatableContents.key), Codec.STRING.lenientOptionalFieldOf("fallback").forGetter(translatableContents -> Optional.ofNullable(translatableContents.fallback)), ARG_CODEC.listOf().optionalFieldOf("with").forGetter(translatableContents -> adjustArgs(translatableContents.args)) ) .apply(instance, TranslatableContents::create) ); public static final ComponentContents.Type TYPE = new ComponentContents.Type<>(CODEC, "translatable"); private static final FormattedText TEXT_PERCENT = FormattedText.of("%"); private static final FormattedText TEXT_NULL = FormattedText.of("null"); private final String key; @Nullable private final String fallback; private final Object[] args; @Nullable private Language decomposedWith; /** * The discrete elements that make up this component. For example, this would be ["Prefix, ", "FirstArg", "SecondArg", " again ", "SecondArg", " and ", "FirstArg", " lastly ", "ThirdArg", " and also ", "FirstArg", " again!"] for "translation.test.complex" (see en_us.json) */ private List decomposedParts = ImmutableList.of(); private static final Pattern FORMAT_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?([A-Za-z%]|$)"); private static DataResult filterAllowedArguments(@Nullable Object input) { return !isAllowedPrimitiveArgument(input) ? DataResult.error(() -> "This value needs to be parsed as component") : DataResult.success(input); } public static boolean isAllowedPrimitiveArgument(@Nullable Object input) { return input instanceof Number || input instanceof Boolean || input instanceof String; } private static Optional> adjustArgs(Object[] args) { return args.length == 0 ? Optional.empty() : Optional.of(Arrays.asList(args)); } private static Object[] adjustArgs(Optional> args) { return (Object[])args.map(list -> list.isEmpty() ? NO_ARGS : list.toArray()).orElse(NO_ARGS); } private static TranslatableContents create(String key, Optional fallback, Optional> args) { return new TranslatableContents(key, (String)fallback.orElse(null), adjustArgs(args)); } public TranslatableContents(String key, @Nullable String fallback, Object[] args) { this.key = key; this.fallback = fallback; this.args = args; } @Override public ComponentContents.Type type() { return TYPE; } /** * Ensures that all the children are up to date with the most recent translation mapping. */ private void decompose() { Language language = Language.getInstance(); if (language != this.decomposedWith) { this.decomposedWith = language; String string = this.fallback != null ? language.getOrDefault(this.key, this.fallback) : language.getOrDefault(this.key); try { Builder builder = ImmutableList.builder(); this.decomposeTemplate(string, builder::add); this.decomposedParts = builder.build(); } catch (TranslatableFormatException var4) { this.decomposedParts = ImmutableList.of(FormattedText.of(string)); } } } private void decomposeTemplate(String formatTemplate, Consumer consumer) { Matcher matcher = FORMAT_PATTERN.matcher(formatTemplate); try { int i = 0; int j = 0; while (matcher.find(j)) { int k = matcher.start(); int l = matcher.end(); if (k > j) { String string = formatTemplate.substring(j, k); if (string.indexOf(37) != -1) { throw new IllegalArgumentException(); } consumer.accept(FormattedText.of(string)); } String string = matcher.group(2); String string2 = formatTemplate.substring(k, l); if ("%".equals(string) && "%%".equals(string2)) { consumer.accept(TEXT_PERCENT); } else { if (!"s".equals(string)) { throw new TranslatableFormatException(this, "Unsupported format: '" + string2 + "'"); } String string3 = matcher.group(1); int m = string3 != null ? Integer.parseInt(string3) - 1 : i++; consumer.accept(this.getArgument(m)); } j = l; } if (j < formatTemplate.length()) { String string4 = formatTemplate.substring(j); if (string4.indexOf(37) != -1) { throw new IllegalArgumentException(); } consumer.accept(FormattedText.of(string4)); } } catch (IllegalArgumentException var12) { throw new TranslatableFormatException(this, var12); } } private FormattedText getArgument(int index) { if (index >= 0 && index < this.args.length) { Object object = this.args[index]; if (object instanceof Component component) { return component; } else { return object == null ? TEXT_NULL : FormattedText.of(object.toString()); } } else { throw new TranslatableFormatException(this, index); } } @Override public Optional visit(FormattedText.StyledContentConsumer styledContentConsumer, Style style) { this.decompose(); for (FormattedText formattedText : this.decomposedParts) { Optional optional = formattedText.visit(styledContentConsumer, style); if (optional.isPresent()) { return optional; } } return Optional.empty(); } @Override public Optional visit(FormattedText.ContentConsumer contentConsumer) { this.decompose(); for (FormattedText formattedText : this.decomposedParts) { Optional optional = formattedText.visit(contentConsumer); if (optional.isPresent()) { return optional; } } return Optional.empty(); } @Override public MutableComponent resolve(@Nullable CommandSourceStack nbtPathPattern, @Nullable Entity entity, int recursionDepth) throws CommandSyntaxException { Object[] objects = new Object[this.args.length]; for (int i = 0; i < objects.length; i++) { Object object = this.args[i]; if (object instanceof Component component) { objects[i] = ComponentUtils.updateForEntity(nbtPathPattern, component, entity, recursionDepth); } else { objects[i] = object; } } return MutableComponent.create(new TranslatableContents(this.key, this.fallback, objects)); } public boolean equals(Object object) { return this == object ? true : object instanceof TranslatableContents translatableContents && Objects.equals(this.key, translatableContents.key) && Objects.equals(this.fallback, translatableContents.fallback) && Arrays.equals(this.args, translatableContents.args); } public int hashCode() { int i = Objects.hashCode(this.key); i = 31 * i + Objects.hashCode(this.fallback); return 31 * i + Arrays.hashCode(this.args); } public String toString() { return "translation{key='" + this.key + "'" + (this.fallback != null ? ", fallback='" + this.fallback + "'" : "") + ", args=" + Arrays.toString(this.args) + "}"; } public String getKey() { return this.key; } @Nullable public String getFallback() { return this.fallback; } public Object[] getArgs() { return this.args; } }