Skip to content

Commit 4ac7592

Browse files
committed
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
1 parent 97ddb01 commit 4ac7592

File tree

12 files changed

+353
-108
lines changed

12 files changed

+353
-108
lines changed

src/main/java/org/maxgamer/quickshop/util/MsgUtil.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,7 @@ private static void updateMessages(int selectedVersion) {
14641464
}
14651465
setAndUpdate("_comment", "Please edit this file after format with json formatter");
14661466
}
1467+
public static final int languageVersion = 61;
14671468

14681469

14691470
private static void setAndUpdate(@NotNull String path, @Nullable Object object) {

src/main/java/org/maxgamer/quickshop/util/language/Formatter.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/main/java/org/maxgamer/quickshop/util/language/formatter/FilledFormatter.java

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/main/java/org/maxgamer/quickshop/util/language/formatter/PAPIFormatter.java

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package org.maxgamer.quickshop.util.language.text;
2+
3+
import com.dumptruckman.bukkit.configuration.json.JsonConfiguration;
4+
import org.bukkit.command.CommandSender;
5+
import org.bukkit.configuration.InvalidConfigurationException;
6+
import org.bukkit.entity.Player;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
import org.maxgamer.quickshop.QuickShop;
10+
import org.maxgamer.quickshop.util.Util;
11+
import org.maxgamer.quickshop.util.language.text.distributions.Distribution;
12+
import org.maxgamer.quickshop.util.language.text.distributions.crowdin.CrowdinOTA;
13+
import org.maxgamer.quickshop.util.language.text.postprocessing.PostProcessor;
14+
import org.maxgamer.quickshop.util.language.text.postprocessing.impl.ColorProcessor;
15+
import org.maxgamer.quickshop.util.language.text.postprocessing.impl.FillerProcessor;
16+
import org.maxgamer.quickshop.util.language.text.postprocessing.impl.PlaceHolderApiProcessor;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.util.ArrayList;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.logging.Level;
28+
29+
public class TextManager {
30+
private final QuickShop plugin;
31+
private final Distribution distribution;
32+
private final File overrideFilesFolder;
33+
// <File <Locale, Section>>
34+
private final Map<String, Map<String, JsonConfiguration>> locale2ContentMapping = new HashMap<>();
35+
private final static String languageFileCrowdin = "/master/src/main/resources/lang/%locale%/messages.json";
36+
public List<PostProcessor> postProcessors = new ArrayList<>();
37+
private JsonConfiguration bundledLang = new JsonConfiguration();
38+
39+
public TextManager(QuickShop plugin) {
40+
this.plugin = plugin;
41+
this.distribution = new CrowdinOTA(plugin);
42+
this.overrideFilesFolder = new File(plugin.getDataFolder(), "lang-override");
43+
load();
44+
}
45+
46+
public void load() {
47+
locale2ContentMapping.clear();
48+
postProcessors.clear();
49+
// Load mapping
50+
//for (String availableFile : distribution.getAvailableFiles()) {
51+
try (InputStream stream = plugin.getResource("lang-original/messages.json")) {
52+
if (stream != null)
53+
bundledLang.loadFromString(new String(Util.inputStream2ByteArray(stream), StandardCharsets.UTF_8));
54+
} catch (IOException | InvalidConfigurationException ignored) {
55+
bundledLang = new JsonConfiguration();
56+
}
57+
for (String availableLanguage : distribution.getAvailableLanguages()) {
58+
try {
59+
// load OTA text
60+
String localeFileContent = distribution.getFile(languageFileCrowdin, availableLanguage);
61+
Map<String, JsonConfiguration> fileLocaleMapping = locale2ContentMapping.computeIfAbsent(languageFileCrowdin, k -> new HashMap<>());
62+
JsonConfiguration configuration = new JsonConfiguration();
63+
fileLocaleMapping.put(availableLanguage, configuration);
64+
configuration.loadFromString(localeFileContent);
65+
// load override text
66+
JsonConfiguration override = new JsonConfiguration();
67+
File localOverrideFile = new File(overrideFilesFolder, availableLanguage + ".json");
68+
if (localOverrideFile.exists()) {
69+
override.loadFromString(Files.readString(localOverrideFile.toPath()));
70+
for (String key : override.getKeys(true)) {
71+
configuration.set(key, override.get(key));
72+
}
73+
}
74+
} catch (Exception e) {
75+
plugin.getLogger().log(Level.WARNING, "Couldn't update the translation for locale " + availableLanguage, e);
76+
}
77+
}
78+
postProcessors.add(new FillerProcessor());
79+
postProcessors.add(new ColorProcessor());
80+
postProcessors.add(new PlaceHolderApiProcessor());
81+
}
82+
83+
public Text of(CommandSender sender, String path, String... args) {
84+
return new Text(this, sender, locale2ContentMapping.get(languageFileCrowdin), path, args);
85+
}
86+
87+
static class Text {
88+
private final TextManager manager;
89+
private final String path;
90+
private final QuickShop plugin;
91+
private final Map<String, JsonConfiguration> mapping;
92+
private final CommandSender sender;
93+
private final String[] args;
94+
95+
private Text(TextManager manager, CommandSender sender, Map<String, JsonConfiguration> mapping, String path, String... args) {
96+
this.plugin = manager.plugin;
97+
this.manager = manager;
98+
this.sender = sender;
99+
this.mapping = mapping;
100+
this.path = path;
101+
this.args = args;
102+
}
103+
104+
@Nullable
105+
private String fallbackLocal() {
106+
return manager.bundledLang.getString(path);
107+
}
108+
109+
@NotNull
110+
private String postProcess(@NotNull String text) {
111+
for (PostProcessor postProcessor : this.manager.postProcessors)
112+
text = postProcessor.process(text, sender, args);
113+
return text;
114+
}
115+
116+
@NotNull
117+
public String forLocale(@NotNull String locale) {
118+
JsonConfiguration index = mapping.get(locale);
119+
if (index == null && locale.equals("en")) {
120+
String str = fallbackLocal();
121+
if (str == null)
122+
return "Fallback Missing Language Key: " + path + ", report to QuickShop!";
123+
}
124+
if (index == null)
125+
return forLocale("en");
126+
String str = index.getString(locale);
127+
if (str == null)
128+
return "Missing Language Key: " + path;
129+
return postProcess(str);
130+
}
131+
132+
@NotNull
133+
public String forLocale(@NotNull Player player){
134+
return forLocale(player.getLocale());
135+
}
136+
}
137+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.maxgamer.quickshop.util.language.text.distributions;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import java.util.List;
6+
7+
public interface Distribution {
8+
@NotNull List<String> getAvailableLanguages();
9+
10+
@NotNull List<String> getAvailableFiles();
11+
12+
@NotNull String getFile(String fileCrowdinPath, String crowdinLocale) throws Exception;
13+
14+
@NotNull String getFile(String fileCrowdinPath, String crowdinLocale, boolean forceFlush) throws Exception;
15+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.maxgamer.quickshop.util.language.text.distributions.crowdin;
2+
3+
import com.google.common.cache.Cache;
4+
import com.google.common.cache.CacheBuilder;
5+
import org.apache.commons.codec.digest.DigestUtils;
6+
import org.bukkit.configuration.file.YamlConfiguration;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
import org.maxgamer.quickshop.QuickShop;
10+
import org.maxgamer.quickshop.nonquickshopstuff.com.sk89q.worldedit.util.net.HttpRequest;
11+
import org.maxgamer.quickshop.util.JsonUtil;
12+
import org.maxgamer.quickshop.util.Util;
13+
import org.maxgamer.quickshop.util.language.text.distributions.Distribution;
14+
import org.maxgamer.quickshop.util.language.text.distributions.crowdin.bean.Manifest;
15+
16+
import java.io.File;
17+
import java.io.IOException;
18+
import java.net.URL;
19+
import java.nio.file.Files;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.concurrent.TimeUnit;
23+
24+
public class CrowdinOTA implements Distribution {
25+
protected static final String CROWDIN_OTA_HOST = "https://distributions.crowdin.net/daf1a8db40f132ce157c457xrm4/";
26+
protected final Cache<String, String> requestCachePool = CacheBuilder.newBuilder()
27+
.expireAfterWrite(7, TimeUnit.DAYS)
28+
.recordStats()
29+
.build();
30+
private QuickShop plugin;
31+
32+
public CrowdinOTA(QuickShop plugin) {
33+
this.plugin = plugin;
34+
Util.getCacheFolder().mkdirs();
35+
}
36+
37+
private @Nullable String requestWithCache(String url) {
38+
String data = requestCachePool.getIfPresent(url);
39+
if (data == null) {
40+
try {
41+
data = HttpRequest.get(new URL(url))
42+
.execute()
43+
.expectResponseCode(200)
44+
.returnContent()
45+
.asString("UTF-8");
46+
} catch (IOException e) {
47+
e.printStackTrace();
48+
return null;
49+
}
50+
if (data != null)
51+
requestCachePool.put(url, data);
52+
}
53+
return data;
54+
}
55+
56+
@Nullable
57+
public Manifest getManifest() {
58+
String url = CROWDIN_OTA_HOST + "manifest.json";
59+
String data = requestWithCache(url);
60+
if (data == null)
61+
return null;
62+
return JsonUtil.getGson().fromJson(data, Manifest.class);
63+
}
64+
65+
@NotNull
66+
public List<String> getAvailableLanguages() {
67+
Manifest manifest = getManifest();
68+
if (manifest == null)
69+
return Collections.emptyList();
70+
return manifest.getLanguages();
71+
}
72+
73+
@NotNull
74+
public List<String> getAvailableFiles() {
75+
Manifest manifest = getManifest();
76+
if (manifest == null)
77+
return Collections.emptyList();
78+
return manifest.getFiles();
79+
}
80+
81+
@Override
82+
public @NotNull String getFile(String fileCrowdinPath, String crowdinLocale) throws Exception {
83+
return getFile(fileCrowdinPath,crowdinLocale,false);
84+
}
85+
86+
@NotNull
87+
public String getFile(String fileCrowdinPath, String crowdinLocale, boolean forceFlush)throws Exception {
88+
Manifest manifest = getManifest();
89+
if (manifest == null)
90+
throw new IllegalStateException("Failed to get project manifest");
91+
if (!manifest.getFiles().contains(fileCrowdinPath))
92+
throw new IllegalArgumentException("The file " + fileCrowdinPath + " not exists on Crowdin");
93+
if (!manifest.getCustomLanguages().contains(crowdinLocale))
94+
throw new IllegalArgumentException("The locale " + crowdinLocale + " not exists on Crowdin");
95+
String postProcessingPath = fileCrowdinPath.replace("%locale%", crowdinLocale);
96+
String pathHash = DigestUtils.sha1Hex(postProcessingPath);
97+
File metadataFile = new File(Util.getCacheFolder(), "i18n.metadata");
98+
YamlConfiguration cacheMetadata = YamlConfiguration.loadConfiguration(metadataFile);
99+
long localeTimestamp = cacheMetadata.getLong(pathHash + ".timestamp");
100+
String data = Files.readString(new File(Util.getCacheFolder(), pathHash).toPath());
101+
// invalidate cache, flush it
102+
if (localeTimestamp != manifest.getTimestamp() || forceFlush) {
103+
String url = CROWDIN_OTA_HOST + "content/" + fileCrowdinPath.replace("%locale%", postProcessingPath);
104+
data = requestWithCache(url);
105+
if (data == null)
106+
throw new IOException("Couldn't download translation from remote server");
107+
// update cache index
108+
cacheMetadata.set(pathHash + ".timestamp", manifest.getTimestamp());
109+
cacheMetadata.save(metadataFile);
110+
}
111+
if (data == null) {
112+
cacheMetadata.set(pathHash, null);
113+
cacheMetadata.save(metadataFile);
114+
throw new IOException("Couldn't read translation from local cache, please try again");
115+
}
116+
return data;
117+
}
118+
}

0 commit comments

Comments
 (0)