mirror of
https://github.com/EarthMC/QuickShop-Reremake.git
synced 2026-11-06 17:32:42 -05:00
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
This commit is contained in:
1 parent
97ddb01d93
commit
4ac75920d3
12 files changed
+353
-108
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// <File <Locale, Section>>
|
||||
private final Map<String, Map<String, JsonConfiguration>> locale2ContentMapping = new HashMap<>();
|
||||
private final static String languageFileCrowdin = "/master/src/main/resources/lang/%locale%/messages.json";
|
||||
public List<PostProcessor> 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<String, JsonConfiguration> 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<String, JsonConfiguration> mapping;
|
||||
private final CommandSender sender;
|
||||
private final String[] args;
|
||||
|
||||
private Text(TextManager manager, CommandSender sender, Map<String, JsonConfiguration> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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<String> getAvailableLanguages();
|
||||
|
||||
@NotNull List<String> getAvailableFiles();
|
||||
|
||||
@NotNull String getFile(String fileCrowdinPath, String crowdinLocale) throws Exception;
|
||||
|
||||
@NotNull String getFile(String fileCrowdinPath, String crowdinLocale, boolean forceFlush) throws Exception;
|
||||
}
|
||||
+118
@@ -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<String, String> 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<String> getAvailableLanguages() {
|
||||
Manifest manifest = getManifest();
|
||||
if (manifest == null)
|
||||
return Collections.emptyList();
|
||||
return manifest.getLanguages();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> 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;
|
||||
}
|
||||
}
|
||||
+23
@@ -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<String> files;
|
||||
@JsonProperty("languages")
|
||||
private List<String> languages;
|
||||
@JsonProperty("language_mapping")
|
||||
private List<?> languageMapping;
|
||||
@JsonProperty("custom_languages")
|
||||
private List<?> customLanguages;
|
||||
@JsonProperty("timestamp")
|
||||
private Integer timestamp;
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user