Language.java 7.41 KB
Newer Older
1
2
3
4
5
package emu.grasscutter.utils;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import emu.grasscutter.Grasscutter;
Secretboy's avatar
Secretboy committed
6
import emu.grasscutter.game.player.Player;
7
8

import javax.annotation.Nullable;
9
10
11

import static emu.grasscutter.config.Configuration.*;

12
import java.io.InputStream;
Secretboy-SMR's avatar
Secretboy-SMR committed
13
import java.util.concurrent.ConcurrentHashMap;
14
15
16
import java.util.Map;

public final class Language {
17
    private static final Map<String, Language> cachedLanguages = new ConcurrentHashMap<>();
github-actions's avatar
github-actions committed
18

19
    private final JsonObject languageData;
Secretboy's avatar
Secretboy committed
20
    private final String languageCode;
Secretboy-SMR's avatar
Secretboy-SMR committed
21
    private final Map<String, String> cachedTranslations = new ConcurrentHashMap<>();
22
23
24
25
26
27
28

    /**
     * Creates a language instance from a code.
     * @param langCode The language code.
     * @return A language instance.
     */
    public static Language getLanguage(String langCode) {
Secretboy-SMR's avatar
Secretboy-SMR committed
29
30
31
32
        if (cachedLanguages.containsKey(langCode)) {
            return cachedLanguages.get(langCode);
        }

33
34
        var fallbackLanguageCode = Utils.getLanguageCode(FALLBACK_LANGUAGE);
        var description = getLanguageFileDescription(langCode, fallbackLanguageCode);
35
        var actualLanguageCode = description.getLanguageCode();
Secretboy's avatar
Secretboy committed
36

37
38
39
        Language languageInst;
        if (description.getLanguageFile() != null) {
            languageInst = new Language(description);
Secretboy's avatar
Secretboy committed
40
            cachedLanguages.put(actualLanguageCode, languageInst);
41
        } else {
Secretboy's avatar
Secretboy committed
42
43
44
45
            languageInst = cachedLanguages.get(actualLanguageCode);
            cachedLanguages.put(langCode, languageInst);
        }

Secretboy-SMR's avatar
Secretboy-SMR committed
46
        return languageInst;
47
48
49
50
51
52
53
54
55
    }

    /**
     * Returns the translated value from the key while substituting arguments.
     * @param key The key of the translated value to return.
     * @param args The arguments to substitute.
     * @return A translated value with arguments substituted.
     */
    public static String translate(String key, Object... args) {
KingRainbow44's avatar
KingRainbow44 committed
56
        String translated = Grasscutter.getLanguage().get(key);
github-actions's avatar
github-actions committed
57

KingRainbow44's avatar
KingRainbow44 committed
58
59
60
61
62
63
        try {
            return translated.formatted(args);
        } catch (Exception exception) {
            Grasscutter.getLogger().error("Failed to format string: " + key, exception);
            return translated;
        }
64
65
    }

Secretboy's avatar
Secretboy committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    /**
     * Returns the translated value from the key while substituting arguments.
     * @param player Target player
     * @param key The key of the translated value to return.
     * @param args The arguments to substitute.
     * @return A translated value with arguments substituted.
     */
    public static String translate(Player player, String key, Object... args) {
        if (player == null) {
            return translate(key, args);
        }

        var langCode = Utils.getLanguageCode(player.getAccount().getLocale());
        String translated = Grasscutter.getLanguage(langCode).get(key);
github-actions's avatar
github-actions committed
80

Secretboy's avatar
Secretboy committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
        try {
            return translated.formatted(args);
        } catch (Exception exception) {
            Grasscutter.getLogger().error("Failed to format string: " + key, exception);
            return translated;
        }
    }

    /**
     * get language code
     */
    public String getLanguageCode() {
        return languageCode;
    }

Secretboy's avatar
Secretboy committed
96
97
98
    /**
     * Reads a file and creates a language instance.
     */
99
    private Language(LanguageStreamDescription description) {
Secretboy's avatar
Secretboy committed
100
        @Nullable JsonObject languageData = null;
101
        languageCode = description.getLanguageCode();
github-actions's avatar
github-actions committed
102

Secretboy's avatar
Secretboy committed
103
        try {
104
            languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class);
Secretboy's avatar
Secretboy committed
105
        } catch (Exception exception) {
106
            Grasscutter.getLogger().warn("Failed to load language file: " + description.getLanguageCode(), exception);
Secretboy's avatar
Secretboy committed
107
        }
github-actions's avatar
github-actions committed
108

Secretboy's avatar
Secretboy committed
109
110
111
112
        this.languageData = languageData;
    }

    /**
113
     * create a LanguageStreamDescription
Secretboy's avatar
Secretboy committed
114
115
116
     * @param languageCode The name of the language code.
     * @param fallbackLanguageCode The name of the fallback language code.
     */
117
    private static LanguageStreamDescription getLanguageFileDescription(String languageCode, String fallbackLanguageCode) {
Secretboy's avatar
Secretboy committed
118
119
        var fileName = languageCode + ".json";
        var fallback = fallbackLanguageCode + ".json";
github-actions's avatar
github-actions committed
120

121
        String actualLanguageCode = languageCode;
122
        InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
Secretboy's avatar
Secretboy committed
123

124
        if (file == null) { // Provided fallback language.
125
            Grasscutter.getLogger().warn("Failed to load language file: " + fileName + ", falling back to: " + fallback);
Secretboy's avatar
Secretboy committed
126
127
            actualLanguageCode = fallbackLanguageCode;
            if (cachedLanguages.containsKey(actualLanguageCode)) {
128
                return new LanguageStreamDescription(actualLanguageCode, null);
Secretboy's avatar
Secretboy committed
129
            }
github-actions's avatar
github-actions committed
130

131
            file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
132
        }
Secretboy's avatar
Secretboy committed
133

github-actions's avatar
github-actions committed
134
        if (file == null) { // Fallback the fallback language.
135
            Grasscutter.getLogger().warn("Failed to load language file: " + fallback + ", falling back to: en-US.json");
Secretboy's avatar
Secretboy committed
136
137
            actualLanguageCode = "en-US";
            if (cachedLanguages.containsKey(actualLanguageCode)) {
138
                return new LanguageStreamDescription(actualLanguageCode, null);
Secretboy's avatar
Secretboy committed
139
            }
github-actions's avatar
github-actions committed
140

141
            file = Grasscutter.class.getResourceAsStream("/languages/en-US.json");
142
        }
Secretboy's avatar
Secretboy committed
143

github-actions's avatar
github-actions committed
144
        if (file == null)
145
            throw new RuntimeException("Unable to load the primary, fallback, and 'en-US' language files.");
Secretboy's avatar
Secretboy committed
146

147
        return new LanguageStreamDescription(actualLanguageCode, file);
148
149
150
151
152
153
154
155
    }

    /**
     * Returns the value (as a string) from a nested key.
     * @param key The key to look for.
     * @return The value (as a string) from a nested key.
     */
    public String get(String key) {
github-actions's avatar
github-actions committed
156
        if (this.cachedTranslations.containsKey(key)) {
157
158
            return this.cachedTranslations.get(key);
        }
github-actions's avatar
github-actions committed
159

160
161
162
163
        String[] keys = key.split("\\.");
        JsonObject object = this.languageData;

        int index = 0;
164
165
166
        String valueNotFoundPattern = "This value does not exist. Please report this to the Discord: ";
        String result = valueNotFoundPattern + key;
        boolean isValueFound = false;
167
168

        while (true) {
github-actions's avatar
github-actions committed
169
170
            if (index == keys.length) break;

171
            String currentKey = keys[index++];
github-actions's avatar
github-actions committed
172
            if (object.has(currentKey)) {
173
                JsonElement element = object.get(currentKey);
github-actions's avatar
github-actions committed
174
                if (element.isJsonObject())
175
176
                    object = element.getAsJsonObject();
                else {
177
                    isValueFound = true;
178
179
180
181
                    result = element.getAsString(); break;
                }
            } else break;
        }
182
183
184
185
186
187
188

        if (!isValueFound && !languageCode.equals("en-US")) {
            var englishValue = Grasscutter.getLanguage("en-US").get(key);
            if (!englishValue.contains(valueNotFoundPattern)) {
                result += "\nhere is english version:\n" + englishValue;
            }
        }
github-actions's avatar
github-actions committed
189

190
191
        this.cachedTranslations.put(key, result); return result;
    }
Secretboy's avatar
Secretboy committed
192

193
194
195
    private static class LanguageStreamDescription {
        private final String languageCode;
        private final InputStream languageFile;
Secretboy's avatar
Secretboy committed
196

197
        public LanguageStreamDescription(String languageCode, InputStream languageFile) {
Secretboy's avatar
Secretboy committed
198
199
200
201
202
203
204
205
206
207
208
209
            this.languageCode = languageCode;
            this.languageFile = languageFile;
        }

        public String getLanguageCode() {
            return languageCode;
        }

        public InputStream getLanguageFile() {
            return languageFile;
        }
    }
210
}