This commit is contained in:
Glavo 2025-09-20 22:38:19 +08:00
parent dd7703c7a9
commit 454bb8bb7d
4 changed files with 158 additions and 78 deletions

View File

@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.util.i18n;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@ -42,67 +41,9 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
public DefaultResourceBundleControl() {
}
private static List<Locale> ensureEditable(List<Locale> list) {
return list instanceof ArrayList<?>
? list
: new ArrayList<>(list);
}
@Override
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
if (locale.getLanguage().isEmpty())
return getCandidateLocales(baseName, Locale.ENGLISH);
else if (LocaleUtils.isChinese(locale)) {
String script = locale.getScript();
if (script.isEmpty()) {
script = LocaleUtils.getScript(locale);
if (!script.isEmpty())
return getCandidateLocales(baseName, new Locale.Builder()
.setLocale(locale)
.setScript(script)
.build());
}
}
String language = locale.getLanguage();
List<Locale> locales = super.getCandidateLocales(baseName, locale);
// Is ISO 639-3 language tag
if (language.length() == 3) {
String iso1 = LocaleUtils.toISO1Language(locale.getLanguage());
if (iso1.length() == 2) {
locales = ensureEditable(locales);
locales.removeIf(it -> !it.getLanguage().equals(language));
locales.addAll(getCandidateLocales(baseName, new Locale.Builder()
.setLocale(locale)
.setLanguage(iso1)
.build()));
}
} else if (language.equals("zh")) {
if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) {
locales = ensureEditable(locales);
int chineseIdx = locales.indexOf(Locale.CHINESE);
if (chineseIdx >= 0)
locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
}
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
int chineseIdx = locales.indexOf(Locale.CHINESE);
if (chineseIdx >= 0) {
locales = ensureEditable(locales);
if (locales.contains(LocaleUtils.LOCALE_ZH_HANS))
locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
else
locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
}
}
}
return locales;
return LocaleUtils.getCandidateLocales(locale);
}
}

View File

@ -26,6 +26,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -42,6 +44,16 @@ public final class LocaleUtils {
public static final String DEFAULT_LANGUAGE_KEY = "default";
private static Locale getInstance(String language, String script, String region,
String variant) {
Locale.Builder builder = new Locale.Builder();
if (!language.isEmpty()) builder.setLanguage(language);
if (!script.isEmpty()) builder.setScript(script);
if (!region.isEmpty()) builder.setRegion(region);
if (!variant.isEmpty()) builder.setVariant(variant);
return builder.build();
}
/// Convert a locale to the language key.
///
/// The language key is in the format of BCP 47 language tag.
@ -83,10 +95,108 @@ public final class LocaleUtils {
return locale.getScript();
}
private static final ConcurrentMap<Locale, List<Locale>> CANDIDATE_LOCALES = new ConcurrentHashMap<>();
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
return CANDIDATE_LOCALES.computeIfAbsent(locale, LocaleUtils::createCandidateLocaleList);
}
// -------------
private static List<Locale> createCandidateLocaleList(Locale locale) {
String language = locale.getLanguage();
if (language.isEmpty())
return List.of(Locale.ENGLISH, Locale.ROOT);
String script = getScript(locale);
String region = locale.getCountry();
List<String> variants = locale.getVariant().isEmpty()
? List.of()
: List.of(locale.getVariant().split("[_\\-]"));
ArrayList<Locale> result = new ArrayList<>();
do {
List<String> languages;
if (language.isEmpty()) {
result.add(Locale.ROOT);
break;
} else if (language.length() <= 2) {
languages = List.of(language);
} else {
String iso1Language = mapToISO1Language(language);
languages = iso1Language != null
? List.of(language, iso1Language)
: List.of(language);
}
addCandidateLocales(result, languages, script, region, variants);
} while ((language = getParentLanguage(language)) != null);
return List.copyOf(result);
}
private static void addCandidateLocales(ArrayList<Locale> list,
List<String> languages,
String script,
String region,
List<String> variants) {
if (!variants.isEmpty()) {
for (String v : variants) {
for (String language : languages) {
list.add(getInstance(language, script, region, v));
}
}
}
if (!region.isEmpty()) {
for (String language : languages) {
list.add(getInstance(language, script, region, ""));
}
}
if (!script.isEmpty()) {
for (String language : languages) {
list.add(getInstance(language, script, "", ""));
}
if (!variants.isEmpty()) {
for (String v : variants) {
for (String language : languages) {
list.add(getInstance(language, "", region, v));
}
}
}
if (!region.isEmpty()) {
for (String language : languages) {
list.add(getInstance(language, "", region, ""));
}
}
}
for (String language : languages) {
list.add(getInstance(language, "", "", ""));
}
if (languages.contains("zh")) {
if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) {
int chineseIdx = list.indexOf(Locale.CHINESE);
if (chineseIdx >= 0)
list.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
}
if (!list.contains(Locale.SIMPLIFIED_CHINESE)) {
int chineseIdx = list.indexOf(Locale.CHINESE);
if (chineseIdx >= 0) {
if (list.contains(LocaleUtils.LOCALE_ZH_HANS))
list.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
else
list.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
}
}
}
}
// -------------
public static <T> @Nullable T getByCandidateLocales(Map<String, T> map, List<Locale> candidateLocales) {
for (Locale locale : candidateLocales) {
String key = toLanguageKey(locale);
@ -178,20 +288,44 @@ public final class LocaleUtils {
// ---
/// Try to convert ISO 639-3 language codes to ISO 639-1 language codes.
public static String toISO1Language(String languageTag) {
return switch (languageTag) {
private static @Nullable String mapToISO1Language(String iso3Language) {
return switch (iso3Language) {
case "eng" -> "en";
case "spa" -> "es";
case "jpa" -> "ja";
case "rus" -> "ru";
case "ukr" -> "uk";
case "zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
default -> languageTag;
case "zho" -> "zh";
default -> null;
};
}
private static @Nullable String getParentLanguage(String language) {
return switch (language) {
case "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
case "" -> null;
default -> "";
};
}
/// Try to convert ISO 639-3 language codes to ISO 639-1 language codes.
public static String toISO1Language(String languageTag) {
String lang = languageTag;
while (lang != null) {
if (lang.length() <= 2)
return lang;
else {
String iso1 = mapToISO1Language(lang);
if (iso1 != null)
return iso1;
}
lang = getParentLanguage(lang);
}
return languageTag;
}
public static boolean isEnglish(Locale locale) {
return "en".equals(getISO1Language(locale));
}

View File

@ -62,6 +62,7 @@ public final class LocaleUtilsTest {
assertCandidateLocales("zh-Latn", List.of("zh-Latn", "zh", "zh-CN", "und"));
assertCandidateLocales("zh-Latn-CN", List.of("zh-Latn-CN", "zh-Latn", "zh-CN", "zh", "und"));
assertCandidateLocales("zh-pinyin", List.of("zh-Latn-pinyin", "zh-Latn", "zh-pinyin", "zh", "zh-CN", "und"));
assertCandidateLocales("zho", List.of("zho-Hans", "zh-Hans", "zho", "zh-CN", "zh", "und"));
assertCandidateLocales("lzh", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
assertCandidateLocales("lzh-Hant", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
assertCandidateLocales("lzh-Hans", List.of("lzh-Hans", "lzh", "zh-Hans", "zh-CN", "zh", "und"));
@ -72,12 +73,12 @@ public final class LocaleUtilsTest {
assertCandidateLocales("ja", List.of("ja", "und"));
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
assertCandidateLocales("jpa", List.of("jpa", "ja", "und"));
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "jpa", "ja-JP", "ja", "und"));
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "ja-JP", "jpa", "ja", "und"));
assertCandidateLocales("en", List.of("en", "und"));
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
assertCandidateLocales("eng", List.of("eng", "en", "und"));
assertCandidateLocales("eng-US", List.of("eng-US", "eng", "en-US", "en", "und"));
assertCandidateLocales("eng-US", List.of("eng-US", "en-US", "eng", "en", "und"));
assertCandidateLocales("es", List.of("es", "und"));
assertCandidateLocales("spa", List.of("spa", "es", "und"));

View File

@ -12,9 +12,9 @@ HMCL 为多种语言提供本地化支持。
| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | 支持状态 | 志愿者 |
|---------|-----------|------------|-----------|--------|-------------------------------------------|
| 英语 | `en` | (空) | `default` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | **主要** | [Glavo](https://github.com/3gf8jv4dv) |
| 英语 | `en` | (空) | `default` | **主要** | [Glavo](https://github.com/Glavo) |
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | **主要** | [Glavo](https://github.com/Glavo) |
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | **主要** | [Glavo](https://github.com/Glavo) |
| 中文 (文言) | `lzh` | `_lzh` | `lzh` | 次要 | |
| 日语 | `ja` | `_ja` | `ja` | 次要 | |
| 西班牙语 | `es` | `_es` | `es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
@ -48,20 +48,24 @@ HMCL 欢迎任何人参与翻译和贡献。但是维护更多语言的翻译需
我们希望能够找到擅长该语言者帮助我们长期维护新增的本地化文件。
如果可能缺少长期维护者,我们会更慎重地考虑是否要加入对该语言的支持。
我们建议贡献者在提供新语言翻译之前先通过 [Issue](https://github.com/HMCL-dev/HMCL/issues/new?template=feature.yml) 提出一个功能请求,
我们建议贡献者在提供新语言翻译之前先通过 [Issue](https://github.com/HMCL-dev/HMCL/issues/new?template=feature.yml)
提出一个功能请求,
与其他贡献者先进行讨论,确定了未来的维护方式后再进行翻译工作。
### 开始翻译
如果你想为 HMCL 添加新的语言支持,请从翻译 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties) 开始。
如果你想为 HMCL 添加新的语言支持,请从翻译 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties)
开始。
HMCL 的绝大多数文本都位于这个文件中,翻译此文件就能翻译整个界面。
这是一个 Java Properties 文件,格式非常简单。在翻译前请先阅读该格式的介绍: [Properties 文件](https://en.wikipedia.org/wiki/.properties)。
这是一个 Java Properties
文件,格式非常简单。在翻译前请先阅读该格式的介绍: [Properties 文件](https://en.wikipedia.org/wiki/.properties)。
作为翻译的第一步,请从[这张表格](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes)中查询这个语言对应的两字母或三字母语言标签。
例如,英语的语言标签为 `en`
在确定了语言标签后,请在 [`I18N.properties` 文件旁](../HMCL/src/main/resources/assets/lang)创建 `I18N_<语言标签>.properites` (例如 `I18N_en.properties`) 文件。
在确定了语言标签后,请在 [`I18N.properties` 文件旁](../HMCL/src/main/resources/assets/lang)创建
`I18N_<语言标签>.properites` (例如 `I18N_en.properties`) 文件。
随后,你就可以开始在这个文件中进行翻译工作了。
`I18N.properties` 文件会遵循[资源回退机制](#资源回退机制)查询缺失的译文。
@ -136,8 +140,8 @@ HMCL 的维护者会替你完成其他步骤。
例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会根据以下列表的顺序搜索对应的本地化资源:
1. `eng-US`
2. `eng`
3. `en-US`
2. `en-US`
3. `eng`
4. `en`
5. `und`