Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public String getCommit() {

ModMetadata metadata = FabricLoader.getInstance().getModContainer(MeteorClient.MOD_ID).get().getMetadata();

MeteorClient.ADDON.id = metadata.getId();
MeteorClient.ADDON.name = metadata.getName();
MeteorClient.ADDON.authors = new String[metadata.getAuthors().size()];
if (metadata.containsCustomValue(MeteorClient.MOD_ID + ":color")) {
Expand All @@ -72,6 +73,7 @@ public String getCommit() {
throw new RuntimeException("Exception during addon init \"%s\".".formatted(metadata.getName()), throwable);
}

addon.id = metadata.getId();
addon.name = metadata.getName();

if (metadata.getAuthors().isEmpty()) throw new RuntimeException("Addon \"%s\" requires at least 1 author to be defined in it's fabric.mod.json. See https://fabricmc.net/wiki/documentation:fabric_mod_json_spec".formatted(addon.name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import meteordevelopment.meteorclient.utils.render.color.Color;

import java.io.InputStream;

public abstract class MeteorAddon {
/** This field is automatically assigned from fabric.mod.json file.
* @since 1.21.11 */ // todo replace with exact version when released
public String id;

/** This field is automatically assigned from fabric.mod.json file. */
public String name;

Expand All @@ -36,26 +38,4 @@ public GithubRepo getRepo() {
public String getCommit() {
return null;
}

/**
* Example implementation:
* <pre>{@code
* @Override
* public InputStream provideLanguage(String lang) {
* return Addon.class.getResourceAsStream("/assets/addon-name/language/" + lang + ".json")
* }
* }
* </pre><br>
*
* Addons should not store their language files in the /assets/xxx/lang/ path as it opens up users to detection
* by servers via <a href="https://wurst.wiki/sign_translation_vulnerability">the translation exploit</a>.
* Storing them anywhere else should prevent them from getting picked up via the vanilla resource loader.
*
* @param lang A language code in lowercase
* @return An InputStream for the relevant json translation file, or null if the addon doesn't have
* a file for that language.
*/
public InputStream provideLanguage(String lang) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private MutableText getCommandText(Command command) {
}
tooltip.append(aliases.formatted(Formatting.GRAY)).append("\n\n");

tooltip.append(translatable("description")).formatted(Formatting.WHITE);
tooltip.append(command.translatable("description")).formatted(Formatting.WHITE);

// Text
MutableText text = Text.literal(Utils.nameToTitle(command.getName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public class LanguageManagerMixin {
@Inject(method = "setLanguage", at = @At("TAIL"))
private void onSetLanguage(String languageCode, CallbackInfo ci) {
MeteorTranslations.loadLanguage(languageCode);
MeteorTranslations.clearUnusedLanguages(languageCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import com.google.gson.Gson;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import meteordevelopment.meteorclient.MeteorClient;
import meteordevelopment.meteorclient.addons.AddonManager;
import meteordevelopment.meteorclient.addons.MeteorAddon;
import meteordevelopment.meteorclient.utils.PreInit;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.resource.language.LanguageDefinition;
import net.minecraft.client.resource.language.ReorderingUtil;
import net.minecraft.text.OrderedText;
import net.minecraft.text.StringVisitable;
Expand All @@ -20,67 +21,79 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Map;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static meteordevelopment.meteorclient.MeteorClient.mc;

@SuppressWarnings("unused")
public class MeteorTranslations {
private static final String EN_US_CODE = "en_us";
private static final Gson GSON = new Gson();
private static final Map<String, MeteorLanguage> languages = new Object2ObjectOpenHashMap<>();
private static MeteorLanguage defaultLanguage;

@PreInit
public static void preInit() {
List<String> toLoad = new ArrayList<>(2);
toLoad.add("en_us");
if (!mc.options.language.equalsIgnoreCase("en_us")) toLoad.add(mc.options.language);
toLoad.add(EN_US_CODE);
if (!mc.options.language.equals(EN_US_CODE)) toLoad.add(mc.options.language);

for (String language : toLoad) {
loadLanguage(language);
}

defaultLanguage = getLanguage(EN_US_CODE);
}

public static void loadLanguage(String languageCode) {
languageCode = languageCode.toLowerCase();
if (languages.containsKey(languageCode)) return;

LanguageDefinition definition = MinecraftClient.getInstance().getLanguageManager().getLanguage(languageCode);
if (definition == null) return;

Object2ObjectOpenHashMap<String, String> languageMap = new Object2ObjectOpenHashMap<>();

try (InputStream stream = MeteorTranslations.class.getResourceAsStream("/assets/meteor-client/language/" + languageCode + ".json")) {
if (stream == null) {
if (languageCode.equals("en_us")) throw new RuntimeException("Error loading the default language");
if (languageCode.equals(EN_US_CODE)) throw new RuntimeException("Error loading the default language");
else MeteorClient.LOG.info("No language file found for '{}'", languageCode);
}
else {
// noinspection unchecked
Object2ObjectOpenHashMap<String, String> map = GSON.fromJson(new InputStreamReader(stream), Object2ObjectOpenHashMap.class);
languages.put(languageCode, new MeteorLanguage(map));
Object2ObjectOpenHashMap<String, String> map = GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Object2ObjectOpenHashMap.class);
languageMap.putAll(map);

MeteorClient.LOG.info("Loaded language: {}", languageCode);
}
} catch (IOException e) {
if (languageCode.equals("en_us")) throw new RuntimeException(e);
if (languageCode.equals(EN_US_CODE)) throw new RuntimeException("Error loading default language", e);
else MeteorClient.LOG.error("Error loading language: {}", languageCode, e);
}

for (MeteorAddon addon : AddonManager.ADDONS) {
if (addon == MeteorClient.ADDON) continue;

try (InputStream stream = addon.provideLanguage(languageCode)) {
try (InputStream stream = addon.getClass().getResourceAsStream("/assets/" + addon.id + "/language/" + languageCode + ".json")) {
if (stream == null) continue;
MeteorLanguage lang = languages.getOrDefault(languageCode, new MeteorLanguage());

// noinspection unchecked
Object2ObjectOpenHashMap<String, String> map = GSON.fromJson(new InputStreamReader(stream), Object2ObjectOpenHashMap.class);
lang.addCustomTranslation(map);
languages.put(languageCode, lang);
Object2ObjectOpenHashMap<String, String> map = GSON.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), Object2ObjectOpenHashMap.class);
languageMap.putAll(map);

MeteorClient.LOG.info("Loaded language {} from addon {}", languageCode, addon.name);
} catch (IOException e) {
MeteorClient.LOG.error("Error loading language {} from addon {}", languageCode, addon.name, e);
}
}

if (!languageMap.isEmpty()) {
languages.put(languageCode, new MeteorLanguage(definition.rightToLeft(), languageMap));
}
}

public static void clearUnusedLanguages(String currentLanguageCode) {
languages.keySet().removeIf(languageCode -> !languageCode.equals(EN_US_CODE) && !languageCode.equals(currentLanguageCode));
}

public static String translate(String key, Object... args) {
Expand Down Expand Up @@ -110,11 +123,11 @@ public static MeteorLanguage getLanguage(String lang) {
}

public static MeteorLanguage getCurrentLanguage() {
return languages.getOrDefault(mc.options.language.toLowerCase(), getDefaultLanguage());
return languages.getOrDefault(mc.options.language, getDefaultLanguage());
}

public static MeteorLanguage getDefaultLanguage() {
return languages.get("en_us");
return defaultLanguage;
}

/**
Expand All @@ -125,60 +138,42 @@ public static double percentLocalised() {
// translation. Maybe that will change in the future.
if (isEnglish()) return 100;

double currentLangSize = languages.getOrDefault(mc.options.language.toLowerCase(), new MeteorLanguage()).translations.size();
MeteorLanguage currentLang = languages.get(mc.options.language);
double currentLangSize = currentLang != null ? currentLang.translations.size() : 0;
return (currentLangSize / getDefaultLanguage().translations.size()) * 100;
}

public static boolean isEnglish() {
return mc.options.language.toLowerCase().startsWith("en");
return mc.options.language.startsWith("en");
}

public static class MeteorLanguage extends Language {
private final Map<String, String> translations = new Object2ObjectOpenHashMap<>();
private final List<Map<String, String>> customTranslations = new ObjectArrayList<>();
private final boolean rightToLeft;

public MeteorLanguage() {}

public MeteorLanguage(Map<String, String> translations) {
public MeteorLanguage(boolean rightToLeft, Map<String, String> translations) {
this.rightToLeft = rightToLeft;
this.translations.putAll(translations);
}

public void addCustomTranslation(Map<String, String> customTranslation) {
if (customTranslations.contains(customTranslation)) return;

customTranslations.add(customTranslation);
}

@Override
public String get(String key, String fallback) {
if (translations.containsKey(key)) return translations.get(key);

for (Map<String, String> customTranslation : customTranslations) {
if (customTranslation.containsKey(key)) return customTranslation.get(key);
}

return fallback;
return translations.getOrDefault(key, fallback);
}

@Override
public boolean hasTranslation(String key) {
if (translations.containsKey(key)) return true;

for (Map<String, String> customTranslation : customTranslations) {
if (customTranslation.containsKey(key)) return true;
}

return false;
return translations.containsKey(key);
}

@Override
public boolean isRightToLeft() {
return false;
return this.rightToLeft;
}

@Override
public OrderedText reorder(StringVisitable text) {
return ReorderingUtil.reorder(text, false);
return ReorderingUtil.reorder(text, this.rightToLeft);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@
"meteor.command.vclip.description": "Lets you clip through blocks vertically.",
"meteor.command.wasp.description": "Sets the auto wasp target.",
"meteor.command.wasp.exception.cant_wasp_self": "You cannot target yourself!",
"meteor.command.wasp.info.target": "%d set as target.",
"meteor.command.wasp.info.target": "%s set as target.",
"meteor.command.waypoint.description": "Manages waypoints."
}