Skip to content

Commit bd9ae18

Browse files
authored
优化本地化支持 (#4525)
1 parent 867a04d commit bd9ae18

File tree

5 files changed

+158
-93
lines changed

5 files changed

+158
-93
lines changed

HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ public String getDisplayName(SupportedLocale inLocale) {
142142
}
143143

144144
Locale inJavaLocale = inLocale.getLocale();
145-
if (LocaleUtils.isISO3Language(inJavaLocale.getLanguage())) {
145+
if (inJavaLocale.getLanguage().length() > 2) {
146146
String iso1 = LocaleUtils.getISO1Language(inJavaLocale);
147-
if (LocaleUtils.isISO1Language(iso1)) {
147+
if (iso1.length() <= 2) {
148148
Locale.Builder builder = new Locale.Builder()
149149
.setLocale(inJavaLocale)
150150
.setLanguage(iso1);

HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package org.jackhuang.hmcl.util.i18n;
1919

20-
import java.util.ArrayList;
2120
import java.util.List;
2221
import java.util.Locale;
2322
import java.util.ResourceBundle;
@@ -31,7 +30,7 @@
3130
/// - For all Chinese locales, `zh-CN` is always added to the candidate list. If `zh-Hans` already exists in the candidate list,
3231
/// `zh-CN` is inserted before `zh`; otherwise, it is inserted after `zh`.
3332
/// - For all Traditional Chinese locales, `zh-TW` is always added to the candidate list (before `zh`).
34-
/// - For all [supported][LocaleUtils#toISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
33+
/// - For all [supported][LocaleUtils#mapToISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
3534
/// a candidate list with the language code replaced by the ISO 639-1 (Macro)language code is added to the end of the candidate list.
3635
///
3736
/// @author Glavo
@@ -42,67 +41,8 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
4241
public DefaultResourceBundleControl() {
4342
}
4443

45-
private static List<Locale> ensureEditable(List<Locale> list) {
46-
return list instanceof ArrayList<?>
47-
? list
48-
: new ArrayList<>(list);
49-
}
50-
5144
@Override
5245
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
53-
if (locale.getLanguage().isEmpty())
54-
return getCandidateLocales(baseName, Locale.ENGLISH);
55-
else if (LocaleUtils.isChinese(locale)) {
56-
String script = locale.getScript();
57-
58-
if (script.isEmpty()) {
59-
script = LocaleUtils.getScript(locale);
60-
if (!script.isEmpty())
61-
return getCandidateLocales(baseName, new Locale.Builder()
62-
.setLocale(locale)
63-
.setScript(script)
64-
.build());
65-
}
66-
}
67-
68-
String language = locale.getLanguage();
69-
70-
List<Locale> locales = super.getCandidateLocales(baseName, locale);
71-
72-
// Is ISO 639-3 language tag
73-
if (language.length() == 3) {
74-
String iso1 = LocaleUtils.toISO1Language(locale.getLanguage());
75-
76-
if (iso1.length() == 2) {
77-
locales = ensureEditable(locales);
78-
locales.removeIf(it -> !it.getLanguage().equals(language));
79-
80-
locales.addAll(getCandidateLocales(baseName, new Locale.Builder()
81-
.setLocale(locale)
82-
.setLanguage(iso1)
83-
.build()));
84-
}
85-
} else if (language.equals("zh")) {
86-
if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) {
87-
locales = ensureEditable(locales);
88-
int chineseIdx = locales.indexOf(Locale.CHINESE);
89-
if (chineseIdx >= 0)
90-
locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
91-
}
92-
93-
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
94-
int chineseIdx = locales.indexOf(Locale.CHINESE);
95-
96-
if (chineseIdx >= 0) {
97-
locales = ensureEditable(locales);
98-
if (locales.contains(LocaleUtils.LOCALE_ZH_HANS))
99-
locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
100-
else
101-
locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
102-
}
103-
}
104-
}
105-
106-
return locales;
46+
return LocaleUtils.getCandidateLocales(locale);
10747
}
10848
}

HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java

Lines changed: 139 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.nio.file.Files;
2727
import java.nio.file.Path;
2828
import java.util.*;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.ConcurrentMap;
2931
import java.util.stream.Stream;
3032

3133
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -42,6 +44,16 @@ public final class LocaleUtils {
4244

4345
public static final String DEFAULT_LANGUAGE_KEY = "default";
4446

47+
private static Locale getInstance(String language, String script, String region,
48+
String variant) {
49+
Locale.Builder builder = new Locale.Builder();
50+
if (!language.isEmpty()) builder.setLanguage(language);
51+
if (!script.isEmpty()) builder.setScript(script);
52+
if (!region.isEmpty()) builder.setRegion(region);
53+
if (!variant.isEmpty()) builder.setVariant(variant);
54+
return builder.build();
55+
}
56+
4557
/// Convert a locale to the language key.
4658
///
4759
/// The language key is in the format of BCP 47 language tag.
@@ -52,18 +64,24 @@ public static String toLanguageKey(Locale locale) {
5264
: locale.stripExtensions().toLanguageTag();
5365
}
5466

55-
public static boolean isISO1Language(String language) {
56-
return language.length() == 2;
57-
}
58-
59-
public static boolean isISO3Language(String language) {
60-
return language.length() == 3;
61-
}
62-
6367
public static @NotNull String getISO1Language(Locale locale) {
6468
String language = locale.getLanguage();
6569
if (language.isEmpty()) return "en";
66-
return isISO3Language(language) ? toISO1Language(language) : language;
70+
if (language.length() <= 2)
71+
return language;
72+
73+
String lang = language;
74+
while (lang != null) {
75+
if (lang.length() <= 2)
76+
return lang;
77+
else {
78+
String iso1 = mapToISO1Language(lang);
79+
if (iso1 != null)
80+
return iso1;
81+
}
82+
lang = getParentLanguage(lang);
83+
}
84+
return language;
6785
}
6886

6987
/// Get the script of the locale. If the script is empty and the language is Chinese,
@@ -83,10 +101,108 @@ public static boolean isISO3Language(String language) {
83101
return locale.getScript();
84102
}
85103

104+
private static final ConcurrentMap<Locale, List<Locale>> CANDIDATE_LOCALES = new ConcurrentHashMap<>();
105+
86106
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
87-
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
107+
return CANDIDATE_LOCALES.computeIfAbsent(locale, LocaleUtils::createCandidateLocaleList);
108+
}
109+
110+
// -------------
111+
112+
private static List<Locale> createCandidateLocaleList(Locale locale) {
113+
String language = locale.getLanguage();
114+
if (language.isEmpty())
115+
return List.of(Locale.ENGLISH, Locale.ROOT);
116+
117+
String script = getScript(locale);
118+
String region = locale.getCountry();
119+
List<String> variants = locale.getVariant().isEmpty()
120+
? List.of()
121+
: List.of(locale.getVariant().split("[_\\-]"));
122+
123+
ArrayList<Locale> result = new ArrayList<>();
124+
do {
125+
List<String> languages;
126+
127+
if (language.isEmpty()) {
128+
result.add(Locale.ROOT);
129+
break;
130+
} else if (language.length() <= 2) {
131+
languages = List.of(language);
132+
} else {
133+
String iso1Language = mapToISO1Language(language);
134+
languages = iso1Language != null
135+
? List.of(language, iso1Language)
136+
: List.of(language);
137+
}
138+
139+
addCandidateLocales(result, languages, script, region, variants);
140+
} while ((language = getParentLanguage(language)) != null);
141+
142+
return List.copyOf(result);
88143
}
89144

145+
private static void addCandidateLocales(ArrayList<Locale> list,
146+
List<String> languages,
147+
String script,
148+
String region,
149+
List<String> variants) {
150+
if (!variants.isEmpty()) {
151+
for (String v : variants) {
152+
for (String language : languages) {
153+
list.add(getInstance(language, script, region, v));
154+
}
155+
}
156+
}
157+
if (!region.isEmpty()) {
158+
for (String language : languages) {
159+
list.add(getInstance(language, script, region, ""));
160+
}
161+
}
162+
if (!script.isEmpty()) {
163+
for (String language : languages) {
164+
list.add(getInstance(language, script, "", ""));
165+
}
166+
if (!variants.isEmpty()) {
167+
for (String v : variants) {
168+
for (String language : languages) {
169+
list.add(getInstance(language, "", region, v));
170+
}
171+
}
172+
}
173+
if (!region.isEmpty()) {
174+
for (String language : languages) {
175+
list.add(getInstance(language, "", region, ""));
176+
}
177+
}
178+
}
179+
180+
for (String language : languages) {
181+
list.add(getInstance(language, "", "", ""));
182+
}
183+
184+
if (languages.contains("zh")) {
185+
if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) {
186+
int chineseIdx = list.indexOf(Locale.CHINESE);
187+
if (chineseIdx >= 0)
188+
list.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
189+
}
190+
191+
if (!list.contains(Locale.SIMPLIFIED_CHINESE)) {
192+
int chineseIdx = list.indexOf(Locale.CHINESE);
193+
194+
if (chineseIdx >= 0) {
195+
if (list.contains(LocaleUtils.LOCALE_ZH_HANS))
196+
list.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
197+
else
198+
list.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
199+
}
200+
}
201+
}
202+
}
203+
204+
// -------------
205+
90206
public static <T> @Nullable T getByCandidateLocales(Map<String, T> map, List<Locale> candidateLocales) {
91207
for (Locale locale : candidateLocales) {
92208
String key = toLanguageKey(locale);
@@ -178,17 +294,25 @@ else if (fileName.length() > defaultFileNameLength + 1 && fileName.charAt(baseNa
178294

179295
// ---
180296

181-
/// Try to convert ISO 639-3 language codes to ISO 639-1 language codes.
182-
public static String toISO1Language(String languageTag) {
183-
return switch (languageTag) {
297+
/// Map ISO 639-3 language codes to ISO 639-1 language codes.
298+
public static @Nullable String mapToISO1Language(String iso3Language) {
299+
return switch (iso3Language) {
184300
case "eng" -> "en";
185301
case "spa" -> "es";
186302
case "jpa" -> "ja";
187303
case "rus" -> "ru";
188304
case "ukr" -> "uk";
189-
case "zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
305+
case "zho" -> "zh";
306+
default -> null;
307+
};
308+
}
309+
310+
public static @Nullable String getParentLanguage(String language) {
311+
return switch (language) {
312+
case "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
190313
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
191-
default -> languageTag;
314+
case "" -> null;
315+
default -> "";
192316
};
193317
}
194318

HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public void testGetCandidateLocales() {
6262
assertCandidateLocales("zh-Latn", List.of("zh-Latn", "zh", "zh-CN", "und"));
6363
assertCandidateLocales("zh-Latn-CN", List.of("zh-Latn-CN", "zh-Latn", "zh-CN", "zh", "und"));
6464
assertCandidateLocales("zh-pinyin", List.of("zh-Latn-pinyin", "zh-Latn", "zh-pinyin", "zh", "zh-CN", "und"));
65+
assertCandidateLocales("zho", List.of("zho-Hans", "zh-Hans", "zho", "zh-CN", "zh", "und"));
6566
assertCandidateLocales("lzh", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
6667
assertCandidateLocales("lzh-Hant", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
6768
assertCandidateLocales("lzh-Hans", List.of("lzh-Hans", "lzh", "zh-Hans", "zh-CN", "zh", "und"));
@@ -72,12 +73,12 @@ public void testGetCandidateLocales() {
7273
assertCandidateLocales("ja", List.of("ja", "und"));
7374
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
7475
assertCandidateLocales("jpa", List.of("jpa", "ja", "und"));
75-
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "jpa", "ja-JP", "ja", "und"));
76+
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "ja-JP", "jpa", "ja", "und"));
7677

7778
assertCandidateLocales("en", List.of("en", "und"));
7879
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
7980
assertCandidateLocales("eng", List.of("eng", "en", "und"));
80-
assertCandidateLocales("eng-US", List.of("eng-US", "eng", "en-US", "en", "und"));
81+
assertCandidateLocales("eng-US", List.of("eng-US", "en-US", "eng", "en", "und"));
8182

8283
assertCandidateLocales("es", List.of("es", "und"));
8384
assertCandidateLocales("spa", List.of("spa", "es", "und"));

docs/Localization_zh.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ HMCL 为多种语言提供本地化支持。
1010

1111
目前,HMCL 为这些语言提供支持:
1212

13-
| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | 支持状态 | 志愿者 |
14-
|---------|-----------|------------|-----------|--------|-------------------------------------------|
15-
| 英语 | `en` | (空) | `default` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
16-
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
17-
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
18-
| 中文 (文言) | `lzh` | `_lzh` | `lzh` | 次要 | |
19-
| 日语 | `ja` | `_ja` | `ja` | 次要 | |
20-
| 西班牙语 | `es` | `_es` | `es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
21-
| 俄语 | `ru` | `_ru` | `ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
22-
| 乌克兰语 | `uk` | `_uk` | `uk` | 次要 | |
13+
| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | [游戏语言文件](https://minecraft.wiki/w/Language) | 支持状态 | 志愿者 |
14+
|---------|-----------|------------|-----------|---------------------------------------------|--------|-------------------------------------------|
15+
| 英语 | `en` | (空) | `default` | `en_us` | **主要** | [Glavo](https://github.com/Glavo) |
16+
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) |
17+
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | `zh_tw` <br/> `zh_hk` | **主要** | [Glavo](https://github.com/Glavo) |
18+
| 中文 (文言) | `lzh` | `_lzh` | `lzh` | `lzh` | 次要 | |
19+
| 日语 | `ja` | `_ja` | `ja` | `ja_jp` | 次要 | |
20+
| 西班牙语 | `es` | `_es` | `es` | `es_es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
21+
| 俄语 | `ru` | `_ru` | `ru` | `ru_ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
22+
| 乌克兰语 | `uk` | `_uk` | `uk` | `uk_ua` | 次要 | |
2323

2424
HMCL 会要求所有 Pull Request 在更新文档和本地化资源时同步更新所有**主要**支持的语言对应的资源。
2525
如果 PR 作者对相关语言并不了解,那么可以直接在评论中提出翻译请求,
@@ -136,8 +136,8 @@ HMCL 的维护者会替你完成其他步骤。
136136
例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会根据以下列表的顺序搜索对应的本地化资源:
137137

138138
1. `eng-US`
139-
2. `eng`
140-
3. `en-US`
139+
2. `en-US`
140+
3. `eng`
141141
4. `en`
142142
5. `und`
143143

0 commit comments

Comments
 (0)