diff --git a/src/main/java/meteordevelopment/meteorclient/addons/AddonManager.java b/src/main/java/meteordevelopment/meteorclient/addons/AddonManager.java index 54af1c4d00..2e1a10668e 100644 --- a/src/main/java/meteordevelopment/meteorclient/addons/AddonManager.java +++ b/src/main/java/meteordevelopment/meteorclient/addons/AddonManager.java @@ -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")) { @@ -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)); diff --git a/src/main/java/meteordevelopment/meteorclient/addons/MeteorAddon.java b/src/main/java/meteordevelopment/meteorclient/addons/MeteorAddon.java index 7ae5918d6f..5c2eb55fff 100644 --- a/src/main/java/meteordevelopment/meteorclient/addons/MeteorAddon.java +++ b/src/main/java/meteordevelopment/meteorclient/addons/MeteorAddon.java @@ -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; @@ -36,26 +38,4 @@ public GithubRepo getRepo() { public String getCommit() { return null; } - - /** - * Example implementation: - *
{@code
-     *  @Override
-     *  public InputStream provideLanguage(String lang) {
-     *      return Addon.class.getResourceAsStream("/assets/addon-name/language/" + lang + ".json")
-     *  }
-     * }
-     * 

- * - * Addons should not store their language files in the /assets/xxx/lang/ path as it opens up users to detection - * by servers via the translation exploit. - * 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; - } } diff --git a/src/main/java/meteordevelopment/meteorclient/commands/commands/CommandsCommand.java b/src/main/java/meteordevelopment/meteorclient/commands/commands/CommandsCommand.java index ca08d56e89..7f63c4cd26 100644 --- a/src/main/java/meteordevelopment/meteorclient/commands/commands/CommandsCommand.java +++ b/src/main/java/meteordevelopment/meteorclient/commands/commands/CommandsCommand.java @@ -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())); diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/LanguageManagerMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/LanguageManagerMixin.java index 228143cef8..4a1868909c 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixin/LanguageManagerMixin.java +++ b/src/main/java/meteordevelopment/meteorclient/mixin/LanguageManagerMixin.java @@ -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); } } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorTranslations.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorTranslations.java index 3b449ac3c7..c726bb5d4e 100644 --- a/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorTranslations.java +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/MeteorTranslations.java @@ -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; @@ -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 languages = new Object2ObjectOpenHashMap<>(); + private static MeteorLanguage defaultLanguage; @PreInit public static void preInit() { List 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 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 map = GSON.fromJson(new InputStreamReader(stream), Object2ObjectOpenHashMap.class); - languages.put(languageCode, new MeteorLanguage(map)); + Object2ObjectOpenHashMap 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 map = GSON.fromJson(new InputStreamReader(stream), Object2ObjectOpenHashMap.class); - lang.addCustomTranslation(map); - languages.put(languageCode, lang); + Object2ObjectOpenHashMap 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) { @@ -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; } /** @@ -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 translations = new Object2ObjectOpenHashMap<>(); - private final List> customTranslations = new ObjectArrayList<>(); + private final boolean rightToLeft; - public MeteorLanguage() {} - - public MeteorLanguage(Map translations) { + public MeteorLanguage(boolean rightToLeft, Map translations) { + this.rightToLeft = rightToLeft; this.translations.putAll(translations); } - public void addCustomTranslation(Map 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 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 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); } } } diff --git a/src/main/resources/assets/meteor-client/language/en_us.json b/src/main/resources/assets/meteor-client/language/en_us.json index 35aa0b8e0c..c27502e5fe 100644 --- a/src/main/resources/assets/meteor-client/language/en_us.json +++ b/src/main/resources/assets/meteor-client/language/en_us.json @@ -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." }