From 4ac75920d31044eef80988ea5ec00ecc109103ee Mon Sep 17 00:00:00 2001 From: Ghost_chu <2908803755@qq.com> Date: Fri, 24 Sep 2021 21:04:42 +0800 Subject: [PATCH] rewrite language system' new language system offer better performance, and more stable. also new system allow us update translation from Crowdin OTA platform and auto adapt player client language --- .../org/maxgamer/quickshop/util/MsgUtil.java | 1 + .../quickshop/util/language/Formatter.java | 36 ----- .../language/formatter/FilledFormatter.java | 34 ----- .../language/formatter/PAPIFormatter.java | 38 ----- .../util/language/text/TextManager.java | 137 ++++++++++++++++++ .../text/distributions/Distribution.java | 15 ++ .../distributions/crowdin/CrowdinOTA.java | 118 +++++++++++++++ .../distributions/crowdin/bean/Manifest.java | 23 +++ .../text/postprocessing/PostProcessor.java | 10 ++ .../postprocessing/impl/ColorProcessor.java | 14 ++ .../postprocessing/impl/FillerProcessor.java | 14 ++ .../impl/PlaceHolderApiProcessor.java | 21 +++ 12 files changed, 353 insertions(+), 108 deletions(-) delete mode 100644 src/main/java/org/maxgamer/quickshop/util/language/Formatter.java delete mode 100644 src/main/java/org/maxgamer/quickshop/util/language/formatter/FilledFormatter.java delete mode 100644 src/main/java/org/maxgamer/quickshop/util/language/formatter/PAPIFormatter.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/TextManager.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/distributions/Distribution.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/CrowdinOTA.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/bean/Manifest.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/PostProcessor.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/ColorProcessor.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/FillerProcessor.java create mode 100644 src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/PlaceHolderApiProcessor.java diff --git a/src/main/java/org/maxgamer/quickshop/util/MsgUtil.java b/src/main/java/org/maxgamer/quickshop/util/MsgUtil.java index 1bf152f44..a8adc3d28 100644 --- a/src/main/java/org/maxgamer/quickshop/util/MsgUtil.java +++ b/src/main/java/org/maxgamer/quickshop/util/MsgUtil.java @@ -1464,6 +1464,7 @@ public class MsgUtil { } setAndUpdate("_comment", "Please edit this file after format with json formatter"); } + public static final int languageVersion = 61; private static void setAndUpdate(@NotNull String path, @Nullable Object object) { diff --git a/src/main/java/org/maxgamer/quickshop/util/language/Formatter.java b/src/main/java/org/maxgamer/quickshop/util/language/Formatter.java deleted file mode 100644 index f57824a67..000000000 --- a/src/main/java/org/maxgamer/quickshop/util/language/Formatter.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is a part of project QuickShop, the name is Formatter.java - * Copyright (C) PotatoCraft Studio and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.maxgamer.quickshop.util.language; - -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public interface Formatter { - /** - * Calling while a string need to be formatted - * - * @param raw Raw string (but it might formatted by other formatter) - * @param sender The command sender - * @param args The args you can use it to fill - * @return Formatted string (and it will transfer to other formatters) - */ - @NotNull String format(@NotNull String raw, @Nullable CommandSender sender, @Nullable String... args); -} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/formatter/FilledFormatter.java b/src/main/java/org/maxgamer/quickshop/util/language/formatter/FilledFormatter.java deleted file mode 100644 index f29e664a3..000000000 --- a/src/main/java/org/maxgamer/quickshop/util/language/formatter/FilledFormatter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This file is a part of project QuickShop, the name is FilledFormatter.java - * Copyright (C) PotatoCraft Studio and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.maxgamer.quickshop.util.language.formatter; - -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.maxgamer.quickshop.util.MsgUtil; -import org.maxgamer.quickshop.util.language.Formatter; - -public class FilledFormatter implements Formatter { - - @Override - public @NotNull String format(@NotNull String raw, @Nullable CommandSender sender, @Nullable String... args) { - return MsgUtil.fillArgs(raw, args); - } -} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/formatter/PAPIFormatter.java b/src/main/java/org/maxgamer/quickshop/util/language/formatter/PAPIFormatter.java deleted file mode 100644 index f146a64f1..000000000 --- a/src/main/java/org/maxgamer/quickshop/util/language/formatter/PAPIFormatter.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is a part of project QuickShop, the name is PAPIFormatter.java - * Copyright (C) PotatoCraft Studio and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.maxgamer.quickshop.util.language.formatter; - -import me.clip.placeholderapi.PlaceholderAPI; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.maxgamer.quickshop.util.language.Formatter; - -public class PAPIFormatter implements Formatter { - - @Override - public @NotNull String format(@NotNull String raw, @Nullable CommandSender sender, @Nullable String... args) { - if (sender instanceof OfflinePlayer) { - return PlaceholderAPI.setPlaceholders((OfflinePlayer) sender, raw); - } - return raw; - } -} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/TextManager.java b/src/main/java/org/maxgamer/quickshop/util/language/text/TextManager.java new file mode 100644 index 000000000..975c09de5 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/TextManager.java @@ -0,0 +1,137 @@ +package org.maxgamer.quickshop.util.language.text; + +import com.dumptruckman.bukkit.configuration.json.JsonConfiguration; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.util.Util; +import org.maxgamer.quickshop.util.language.text.distributions.Distribution; +import org.maxgamer.quickshop.util.language.text.distributions.crowdin.CrowdinOTA; +import org.maxgamer.quickshop.util.language.text.postprocessing.PostProcessor; +import org.maxgamer.quickshop.util.language.text.postprocessing.impl.ColorProcessor; +import org.maxgamer.quickshop.util.language.text.postprocessing.impl.FillerProcessor; +import org.maxgamer.quickshop.util.language.text.postprocessing.impl.PlaceHolderApiProcessor; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +public class TextManager { + private final QuickShop plugin; + private final Distribution distribution; + private final File overrideFilesFolder; + // > + private final Map> locale2ContentMapping = new HashMap<>(); + private final static String languageFileCrowdin = "/master/src/main/resources/lang/%locale%/messages.json"; + public List postProcessors = new ArrayList<>(); + private JsonConfiguration bundledLang = new JsonConfiguration(); + + public TextManager(QuickShop plugin) { + this.plugin = plugin; + this.distribution = new CrowdinOTA(plugin); + this.overrideFilesFolder = new File(plugin.getDataFolder(), "lang-override"); + load(); + } + + public void load() { + locale2ContentMapping.clear(); + postProcessors.clear(); + // Load mapping + //for (String availableFile : distribution.getAvailableFiles()) { + try (InputStream stream = plugin.getResource("lang-original/messages.json")) { + if (stream != null) + bundledLang.loadFromString(new String(Util.inputStream2ByteArray(stream), StandardCharsets.UTF_8)); + } catch (IOException | InvalidConfigurationException ignored) { + bundledLang = new JsonConfiguration(); + } + for (String availableLanguage : distribution.getAvailableLanguages()) { + try { + // load OTA text + String localeFileContent = distribution.getFile(languageFileCrowdin, availableLanguage); + Map fileLocaleMapping = locale2ContentMapping.computeIfAbsent(languageFileCrowdin, k -> new HashMap<>()); + JsonConfiguration configuration = new JsonConfiguration(); + fileLocaleMapping.put(availableLanguage, configuration); + configuration.loadFromString(localeFileContent); + // load override text + JsonConfiguration override = new JsonConfiguration(); + File localOverrideFile = new File(overrideFilesFolder, availableLanguage + ".json"); + if (localOverrideFile.exists()) { + override.loadFromString(Files.readString(localOverrideFile.toPath())); + for (String key : override.getKeys(true)) { + configuration.set(key, override.get(key)); + } + } + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Couldn't update the translation for locale " + availableLanguage, e); + } + } + postProcessors.add(new FillerProcessor()); + postProcessors.add(new ColorProcessor()); + postProcessors.add(new PlaceHolderApiProcessor()); + } + + public Text of(CommandSender sender, String path, String... args) { + return new Text(this, sender, locale2ContentMapping.get(languageFileCrowdin), path, args); + } + + static class Text { + private final TextManager manager; + private final String path; + private final QuickShop plugin; + private final Map mapping; + private final CommandSender sender; + private final String[] args; + + private Text(TextManager manager, CommandSender sender, Map mapping, String path, String... args) { + this.plugin = manager.plugin; + this.manager = manager; + this.sender = sender; + this.mapping = mapping; + this.path = path; + this.args = args; + } + + @Nullable + private String fallbackLocal() { + return manager.bundledLang.getString(path); + } + + @NotNull + private String postProcess(@NotNull String text) { + for (PostProcessor postProcessor : this.manager.postProcessors) + text = postProcessor.process(text, sender, args); + return text; + } + + @NotNull + public String forLocale(@NotNull String locale) { + JsonConfiguration index = mapping.get(locale); + if (index == null && locale.equals("en")) { + String str = fallbackLocal(); + if (str == null) + return "Fallback Missing Language Key: " + path + ", report to QuickShop!"; + } + if (index == null) + return forLocale("en"); + String str = index.getString(locale); + if (str == null) + return "Missing Language Key: " + path; + return postProcess(str); + } + + @NotNull + public String forLocale(@NotNull Player player){ + return forLocale(player.getLocale()); + } + } +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/Distribution.java b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/Distribution.java new file mode 100644 index 000000000..30ccac181 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/Distribution.java @@ -0,0 +1,15 @@ +package org.maxgamer.quickshop.util.language.text.distributions; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface Distribution { + @NotNull List getAvailableLanguages(); + + @NotNull List getAvailableFiles(); + + @NotNull String getFile(String fileCrowdinPath, String crowdinLocale) throws Exception; + + @NotNull String getFile(String fileCrowdinPath, String crowdinLocale, boolean forceFlush) throws Exception; +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/CrowdinOTA.java b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/CrowdinOTA.java new file mode 100644 index 000000000..85a74c782 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/CrowdinOTA.java @@ -0,0 +1,118 @@ +package org.maxgamer.quickshop.util.language.text.distributions.crowdin; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.apache.commons.codec.digest.DigestUtils; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.maxgamer.quickshop.QuickShop; +import org.maxgamer.quickshop.nonquickshopstuff.com.sk89q.worldedit.util.net.HttpRequest; +import org.maxgamer.quickshop.util.JsonUtil; +import org.maxgamer.quickshop.util.Util; +import org.maxgamer.quickshop.util.language.text.distributions.Distribution; +import org.maxgamer.quickshop.util.language.text.distributions.crowdin.bean.Manifest; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class CrowdinOTA implements Distribution { + protected static final String CROWDIN_OTA_HOST = "https://distributions.crowdin.net/daf1a8db40f132ce157c457xrm4/"; + protected final Cache requestCachePool = CacheBuilder.newBuilder() + .expireAfterWrite(7, TimeUnit.DAYS) + .recordStats() + .build(); + private QuickShop plugin; + + public CrowdinOTA(QuickShop plugin) { + this.plugin = plugin; + Util.getCacheFolder().mkdirs(); + } + + private @Nullable String requestWithCache(String url) { + String data = requestCachePool.getIfPresent(url); + if (data == null) { + try { + data = HttpRequest.get(new URL(url)) + .execute() + .expectResponseCode(200) + .returnContent() + .asString("UTF-8"); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + if (data != null) + requestCachePool.put(url, data); + } + return data; + } + + @Nullable + public Manifest getManifest() { + String url = CROWDIN_OTA_HOST + "manifest.json"; + String data = requestWithCache(url); + if (data == null) + return null; + return JsonUtil.getGson().fromJson(data, Manifest.class); + } + + @NotNull + public List getAvailableLanguages() { + Manifest manifest = getManifest(); + if (manifest == null) + return Collections.emptyList(); + return manifest.getLanguages(); + } + + @NotNull + public List getAvailableFiles() { + Manifest manifest = getManifest(); + if (manifest == null) + return Collections.emptyList(); + return manifest.getFiles(); + } + + @Override + public @NotNull String getFile(String fileCrowdinPath, String crowdinLocale) throws Exception { + return getFile(fileCrowdinPath,crowdinLocale,false); + } + + @NotNull + public String getFile(String fileCrowdinPath, String crowdinLocale, boolean forceFlush)throws Exception { + Manifest manifest = getManifest(); + if (manifest == null) + throw new IllegalStateException("Failed to get project manifest"); + if (!manifest.getFiles().contains(fileCrowdinPath)) + throw new IllegalArgumentException("The file " + fileCrowdinPath + " not exists on Crowdin"); + if (!manifest.getCustomLanguages().contains(crowdinLocale)) + throw new IllegalArgumentException("The locale " + crowdinLocale + " not exists on Crowdin"); + String postProcessingPath = fileCrowdinPath.replace("%locale%", crowdinLocale); + String pathHash = DigestUtils.sha1Hex(postProcessingPath); + File metadataFile = new File(Util.getCacheFolder(), "i18n.metadata"); + YamlConfiguration cacheMetadata = YamlConfiguration.loadConfiguration(metadataFile); + long localeTimestamp = cacheMetadata.getLong(pathHash + ".timestamp"); + String data = Files.readString(new File(Util.getCacheFolder(), pathHash).toPath()); + // invalidate cache, flush it + if (localeTimestamp != manifest.getTimestamp() || forceFlush) { + String url = CROWDIN_OTA_HOST + "content/" + fileCrowdinPath.replace("%locale%", postProcessingPath); + data = requestWithCache(url); + if (data == null) + throw new IOException("Couldn't download translation from remote server"); + // update cache index + cacheMetadata.set(pathHash + ".timestamp", manifest.getTimestamp()); + cacheMetadata.save(metadataFile); + } + if (data == null) { + cacheMetadata.set(pathHash, null); + cacheMetadata.save(metadataFile); + throw new IOException("Couldn't read translation from local cache, please try again"); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/bean/Manifest.java b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/bean/Manifest.java new file mode 100644 index 000000000..8371c99d5 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/distributions/crowdin/bean/Manifest.java @@ -0,0 +1,23 @@ +package org.maxgamer.quickshop.util.language.text.distributions.crowdin.bean; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor +@Data +public class Manifest { + + @JsonProperty("files") + private List files; + @JsonProperty("languages") + private List languages; + @JsonProperty("language_mapping") + private List languageMapping; + @JsonProperty("custom_languages") + private List customLanguages; + @JsonProperty("timestamp") + private Integer timestamp; +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/PostProcessor.java b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/PostProcessor.java new file mode 100644 index 000000000..ddc97aa91 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/PostProcessor.java @@ -0,0 +1,10 @@ +package org.maxgamer.quickshop.util.language.text.postprocessing; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface PostProcessor { + @NotNull + String process(@NotNull String text, @Nullable CommandSender sender, String... args); +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/ColorProcessor.java b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/ColorProcessor.java new file mode 100644 index 000000000..a79425369 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/ColorProcessor.java @@ -0,0 +1,14 @@ +package org.maxgamer.quickshop.util.language.text.postprocessing.impl; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.maxgamer.quickshop.util.Util; +import org.maxgamer.quickshop.util.language.text.postprocessing.PostProcessor; + +public class ColorProcessor implements PostProcessor { + @Override + public @NotNull String process(@NotNull String text, @Nullable CommandSender sender, String... args) { + return Util.parseColours(text); + } +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/FillerProcessor.java b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/FillerProcessor.java new file mode 100644 index 000000000..2fcd80564 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/FillerProcessor.java @@ -0,0 +1,14 @@ +package org.maxgamer.quickshop.util.language.text.postprocessing.impl; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.maxgamer.quickshop.util.MsgUtil; +import org.maxgamer.quickshop.util.language.text.postprocessing.PostProcessor; + +public class FillerProcessor implements PostProcessor { + @Override + public @NotNull String process(@NotNull String text, @Nullable CommandSender sender, String... args) { + return MsgUtil.fillArgs(text,args); + } +} diff --git a/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/PlaceHolderApiProcessor.java b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/PlaceHolderApiProcessor.java new file mode 100644 index 000000000..6f9bf3853 --- /dev/null +++ b/src/main/java/org/maxgamer/quickshop/util/language/text/postprocessing/impl/PlaceHolderApiProcessor.java @@ -0,0 +1,21 @@ +package org.maxgamer.quickshop.util.language.text.postprocessing.impl; + +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.maxgamer.quickshop.util.language.text.postprocessing.PostProcessor; + +public class PlaceHolderApiProcessor implements PostProcessor { + @Override + public @NotNull String process(@NotNull String text, @Nullable CommandSender sender, String... args) { + if(sender instanceof OfflinePlayer player) { + if (Bukkit.getPluginManager().isPluginEnabled("PlaceHolderAPI")) { + return PlaceholderAPI.setPlaceholders(player,text); + } + } + return text; + } +}