commit 63659703b57ac2153e9c75f72827542aa3a42323 Author: JFronny Date: Fri Feb 17 14:16:47 2023 +0100 Add code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c37caf --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..659abc7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 JFronny + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..876edbf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,15 @@ +import io.gitlab.jfronny.scripts.* + +plugins { + id("jfmod") version "1.3-SNAPSHOT" +} + +dependencies { + modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v1:${prop("libjf_version")}") + modImplementation("io.gitlab.jfronny.libjf:libjf-translate-v1:${prop("libjf_version")}") + + // Dev env + modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny-v1:${prop("libjf_version")}") + modLocalRuntime("io.gitlab.jfronny.libjf:libjf-devutil:${prop("libjf_version")}") + modLocalRuntime("com.terraformersmc:modmenu:5.0.2") +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..33a3c98 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,14 @@ +# https://fabricmc.net/develop/ +minecraft_version=1.19.3 +yarn_mappings=build.5 +loader_version=0.14.12 + +maven_group=io.gitlab.jfronny +archives_base_name=globalization + +modrinth_id=globalization +modrinth_required_dependencies=libjf +modrinth_optional_dependencies=modmenu + +libjf_version=3.5.0 +fabric_version=0.70.0+1.19.3 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2d073a3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") // FabricMC + maven("https://maven.frohnmeyer-wds.de/artifacts") // scripts + gradlePluginPortal() + } +} + +rootProject.name = "globalization" diff --git a/src/client/java/io/gitlab/jfronny/globalization/mixin/TranslationStorageMixin.java b/src/client/java/io/gitlab/jfronny/globalization/mixin/TranslationStorageMixin.java new file mode 100644 index 0000000..4d957db --- /dev/null +++ b/src/client/java/io/gitlab/jfronny/globalization/mixin/TranslationStorageMixin.java @@ -0,0 +1,37 @@ +package io.gitlab.jfronny.globalization.mixin; + +import com.google.common.collect.ImmutableMap; +import io.gitlab.jfronny.globalization.DelegateHashMap; +import io.gitlab.jfronny.globalization.GlobalizationMap; +import net.minecraft.client.resource.language.LanguageDefinition; +import net.minecraft.client.resource.language.TranslationStorage; +import net.minecraft.resource.ResourceManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.*; + +@Mixin(TranslationStorage.class) +public class TranslationStorageMixin { + @ModifyVariable(method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;)Lnet/minecraft/client/resource/language/TranslationStorage;", at = @At(value = "INVOKE_ASSIGN", target = "Lcom/google/common/collect/Maps;newHashMap()Ljava/util/HashMap;"), index = 2) + private static Map globalization$createCustomMap(Map original) { + if (!original.isEmpty()) throw new IllegalStateException("Non-empty original"); + return new GlobalizationMap(); + } + + @Inject(method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;)Lnet/minecraft/client/resource/language/TranslationStorage;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resource/language/LanguageDefinition;isRightToLeft()Z"), locals = LocalCapture.CAPTURE_FAILHARD) + private static void globalization$forkCustomMap(ResourceManager resourceManager, List definitions, CallbackInfoReturnable cir, Map map) { + globalization$getMap(map).fork(); + } + + @Redirect(method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;)Lnet/minecraft/client/resource/language/TranslationStorage;", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/ImmutableMap;copyOf(Ljava/util/Map;)Lcom/google/common/collect/ImmutableMap;")) + private static ImmutableMap globalization$generateMissing(Map source) { + return ImmutableMap.copyOf(globalization$getMap(source).generateMissing()); + } + + private static GlobalizationMap globalization$getMap(Map map) { + return (GlobalizationMap) map; + } +} diff --git a/src/client/resources/assets/globalization/icon.png b/src/client/resources/assets/globalization/icon.png new file mode 100644 index 0000000..9af5991 Binary files /dev/null and b/src/client/resources/assets/globalization/icon.png differ diff --git a/src/client/resources/assets/globalization/lang/en_us.json b/src/client/resources/assets/globalization/lang/en_us.json new file mode 100644 index 0000000..3f2eb1e --- /dev/null +++ b/src/client/resources/assets/globalization/lang/en_us.json @@ -0,0 +1,5 @@ +{ + "globalization.jfconfig.title": "LibJF Translate v1", + "globalization.jfconfig.targetCode": "Target Code", + "globalization.jfconfig.targetCode.tooltip": "The language code to translate missing translations to.\nShould be the same as your Minecraft language. Source language is detected automatically." +} diff --git a/src/client/resources/globalization.client.mixins.json b/src/client/resources/globalization.client.mixins.json new file mode 100644 index 0000000..410a720 --- /dev/null +++ b/src/client/resources/globalization.client.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "io.gitlab.jfronny.globalization.mixin", + "compatibilityLevel": "JAVA_17", + "client": [ + "TranslationStorageMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/java/io/gitlab/jfronny/globalization/DelegateHashMap.java b/src/main/java/io/gitlab/jfronny/globalization/DelegateHashMap.java new file mode 100644 index 0000000..f16e36f --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalization/DelegateHashMap.java @@ -0,0 +1,147 @@ +package io.gitlab.jfronny.globalization; + +import java.util.*; +import java.util.function.*; + +public class DelegateHashMap, K, V> extends HashMap { + private final M delegate; + + public DelegateHashMap(M delegate) { + this.delegate = delegate; + } + + public M getDelegate() { + if (!super.isEmpty()) throw new IllegalStateException("Something accesses this maps internal state"); + return delegate; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public V put(K key, V value) { + return delegate.put(key, value); + } + + @Override + public void putAll(Map m) { + delegate.putAll(m); + } + + @Override + public V remove(Object key) { + return delegate.remove(key); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } + + @Override + public V getOrDefault(Object key, V defaultValue) { + return delegate.getOrDefault(key, defaultValue); + } + + @Override + public V putIfAbsent(K key, V value) { + return delegate.putIfAbsent(key, value); + } + + @Override + public boolean remove(Object key, Object value) { + return delegate.remove(key, value); + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + return delegate.replace(key, oldValue, newValue); + } + + @Override + public V replace(K key, V value) { + return delegate.replace(key, value); + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + return delegate.computeIfAbsent(key, mappingFunction); + } + + @Override + public V computeIfPresent(K key, BiFunction remappingFunction) { + return delegate.computeIfPresent(key, remappingFunction); + } + + @Override + public V compute(K key, BiFunction remappingFunction) { + return delegate.compute(key, remappingFunction); + } + + @Override + public V merge(K key, V value, BiFunction remappingFunction) { + return delegate.merge(key, value, remappingFunction); + } + + @Override + public void forEach(BiConsumer action) { + delegate.forEach(action); + } + + @Override + public void replaceAll(BiFunction function) { + delegate.replaceAll(function); + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/src/main/java/io/gitlab/jfronny/globalization/Globalization.java b/src/main/java/io/gitlab/jfronny/globalization/Globalization.java new file mode 100644 index 0000000..b0d9a84 --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalization/Globalization.java @@ -0,0 +1,12 @@ +package io.gitlab.jfronny.globalization; + +import io.gitlab.jfronny.commons.log.Logger; +import net.fabricmc.api.ModInitializer; + +public class Globalization implements ModInitializer { + public static final Logger LOG = Logger.forName("globalization"); + @Override + public void onInitialize() { + + } +} diff --git a/src/main/java/io/gitlab/jfronny/globalization/GlobalizationConfig.java b/src/main/java/io/gitlab/jfronny/globalization/GlobalizationConfig.java new file mode 100644 index 0000000..9368ba8 --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalization/GlobalizationConfig.java @@ -0,0 +1,13 @@ +package io.gitlab.jfronny.globalization; + +import io.gitlab.jfronny.libjf.config.api.v1.Entry; +import io.gitlab.jfronny.libjf.config.api.v1.JfConfig; + +@JfConfig(referencedConfigs = "libjf-translate-v1") +public class GlobalizationConfig { + @Entry public static String targetCode = "en"; + + static { + JFC_GlobalizationConfig.ensureInitialized(); + } +} diff --git a/src/main/java/io/gitlab/jfronny/globalization/GlobalizationMap.java b/src/main/java/io/gitlab/jfronny/globalization/GlobalizationMap.java new file mode 100644 index 0000000..aa2c90e --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalization/GlobalizationMap.java @@ -0,0 +1,138 @@ +package io.gitlab.jfronny.globalization; + +import io.gitlab.jfronny.libjf.translate.api.*; +import net.fabricmc.loader.api.FabricLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +public class GlobalizationMap extends AbstractMap { + private static final Path targetPath = FabricLoader.getInstance().getConfigDir().resolve("globalization"); + private List> pools = new LinkedList<>(); + + public void fork() { + if (pools.isEmpty() || !getCurrentPool().isEmpty()) pools.add(new LinkedHashMap<>()); + } + + public Map generateMissing() { + if (pools.isEmpty()) return Map.of(); + if (pools.size() == 1) return pools.get(0); + return generateMissing(pools.get(pools.size() - 1), pools.subList(0, pools.size() - 1), TranslateService.getConfigured()); + } + + private static > Map generateMissing(Map origin, List> other, S service) { + Map target = new LinkedHashMap<>(origin); + Map toAdd = other + .stream() + .map(Map::entrySet) + .flatMap(Collection::stream) + .filter(s -> !target.containsKey(s.getKey())) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + Properties cache = new Properties(); + T targetLang = service.parseLang(GlobalizationConfig.targetCode); + Path cacheFile = targetPath.resolve(targetLang.getIdentifier() + ".cache"); + try { + Files.createDirectories(targetPath); + if (Files.exists(cacheFile)) { + try (Reader r = Files.newBufferedReader(cacheFile)) { + cache.load(r); + } + } + } catch (IOException e) { + Globalization.LOG.error("Could not read cache", e); + } + toAdd.forEach((k, v) -> { + try { + if (cache.containsKey(v)) target.put(k, cache.get(v).toString()); + else { + String s = service.translate(v, service.detect(v), targetLang); + target.put(k, s); + cache.put(v, s); + Globalization.LOG.info("Translated " + v + " to " + s); + } + } catch (TranslateException e) { + Globalization.LOG.error("Could not translate " + v, e); + } + }); + try { + Files.createDirectories(targetPath); + try (Writer w = Files.newBufferedWriter(cacheFile)) { + cache.store(w, "Cache for translations to " + targetLang.getDisplayName()); + } + } catch (IOException e) { + Globalization.LOG.error("Could not write cache", e); + } + return target; + } + + private Map getCurrentPool() { + if (pools.isEmpty()) throw new IllegalStateException("Called method on non-forked globalization map"); + return pools.get(pools.size() - 1); + } + + @Nullable + @Override + public String put(String s, String s2) { + String s3 = getCurrentPool().put(s, s2); + if (s3 == null) { + for (Map map : pools.subList(0, pools.size() - 1)) { + s3 = map.getOrDefault(s, s3); + } + } + return s3; + } + + @NotNull + @Override + public Set> entrySet() { + return pools.stream() + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.groupingBy(Entry::getKey, Collectors.toList())) + .entrySet() + .stream() + .map(s -> new MapEntry<>(s.getKey(), s.getValue().get(s.getValue().size() - 1).getValue())) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public boolean isEmpty() { + return pools.isEmpty() || (pools.size() == 1 && pools.get(0).isEmpty()); + } + + @Override + public boolean containsKey(Object o) { + return pools.stream().anyMatch(s -> s.containsKey(o)); + } + + @Override + public boolean containsValue(Object o) { + return pools.stream().anyMatch(s -> s.containsValue(o)); + } + + @Override + public String get(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public String remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set keySet() { + return pools.stream().flatMap(s -> s.keySet().stream()).collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/src/main/java/io/gitlab/jfronny/globalization/MapEntry.java b/src/main/java/io/gitlab/jfronny/globalization/MapEntry.java new file mode 100644 index 0000000..c103ff2 --- /dev/null +++ b/src/main/java/io/gitlab/jfronny/globalization/MapEntry.java @@ -0,0 +1,20 @@ +package io.gitlab.jfronny.globalization; + +import java.util.Map; + +public record MapEntry(K key, V value) implements Map.Entry { + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V v) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..4385403 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "globalization", + "name": "Globalization", + "version": "${version}", + "description": "Generate missing translations", + "authors": ["JFronny"], + "contact": { + "email": "projects.contact@frohnmeyer-wds.de", + "homepage": "https://jfronny.gitlab.io", + "issues": "https://git.frohnmeyer-wds.de/Johannes/Globalization/issues", + "sources": "https://git.frohnmeyer-wds.de/Johannes/Globalization" + }, + "license": "MIT", + "icon": "assets/globalization/icon.png", + "environment": "client", + "entrypoints": { + "libjf:config": ["io.gitlab.jfronny.globalization.JFC_GlobalizationConfig"], + "main": ["io.gitlab.jfronny.globalization.Globalization"] + }, + "mixins": [ + { + "config": "globalization.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.14.12", + "minecraft": "*", + "libjf-config-core-v1": "*", + "libjf-translate-v1": "*" + } +}