优化对于 ISO 639-3 语言代码的支持 (#4455)

This commit is contained in:
Glavo
2025-09-11 19:13:17 +08:00
committed by GitHub
parent c65e4d83a2
commit 9ded2e489d
5 changed files with 145 additions and 96 deletions

View File

@@ -111,36 +111,27 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String language = locale.getLanguage();
String region = locale.getCountry();
switch (language) {
case "ru":
return "ru_RU";
case "uk":
return "uk_UA";
case "es":
return "es_ES";
case "ja":
return "ja_JP";
case "lzh":
return gameVersion.compareTo("1.16") >= 0
? "lzh"
: "";
case "zh":
default:
if (LocaleUtils.isChinese(locale)) {
String script = LocaleUtils.getScript(locale);
if ("Hant".equals(script)) {
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
return "zh_HK";
return "zh_TW";
}
return "zh_CN";
}
return switch (LocaleUtils.getISO1Language(locale)) {
case "es" -> "es_ES";
case "ja" -> "ja_JP";
case "ru" -> "ru_RU";
case "uk" -> "uk_UA";
case "zh" -> {
if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
yield "lzh";
return "";
}
String script = LocaleUtils.getScript(locale);
if ("Hant".equals(script)) {
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
yield "zh_HK";
yield "zh_TW";
}
yield "zh_CN";
}
default -> "";
};
}
@Override

View File

@@ -34,45 +34,39 @@ public final class Locales {
private Locales() {
}
private static Locale getDefaultLocale() {
String language = System.getenv("HMCL_LANGUAGE");
if (StringUtils.isNotBlank(language))
return Locale.forLanguageTag(language);
else
return LocaleUtils.SYSTEM_DEFAULT;
}
public static final SupportedLocale DEFAULT = new SupportedLocale("def", getDefaultLocale()) {
@Override
public boolean isDefault() {
return true;
}
};
public static final SupportedLocale DEFAULT;
static {
String language = System.getenv("HMCL_LANGUAGE");
DEFAULT = new SupportedLocale(true, "def",
StringUtils.isBlank(language) ? LocaleUtils.SYSTEM_DEFAULT : Locale.forLanguageTag(language));
}
/**
* English
*/
public static final SupportedLocale EN = new SupportedLocale("en", Locale.ENGLISH);
public static final SupportedLocale EN = new SupportedLocale("en");
/**
* Spanish
*/
public static final SupportedLocale ES = new SupportedLocale("es", Locale.forLanguageTag("es"));
public static final SupportedLocale ES = new SupportedLocale("es");
/**
* Russian
*/
public static final SupportedLocale RU = new SupportedLocale("ru", Locale.forLanguageTag("ru"));
public static final SupportedLocale RU = new SupportedLocale("ru");
/**
* Ukrainian
*/
public static final SupportedLocale UK = new SupportedLocale("uk", Locale.forLanguageTag("uk"));
public static final SupportedLocale UK = new SupportedLocale("uk");
/**
* Japanese
*/
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE);
public static final SupportedLocale JA = new SupportedLocale("ja");
/**
* Chinese (Simplified)
@@ -87,7 +81,7 @@ public final class Locales {
/**
* Wenyan (Classical Chinese)
*/
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh"));
public static final SupportedLocale WENYAN = new SupportedLocale("lzh");
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
@@ -103,20 +97,30 @@ public final class Locales {
}
@JsonAdapter(SupportedLocale.TypeAdapter.class)
public static class SupportedLocale {
public static final class SupportedLocale {
private final boolean isDefault;
private final String name;
private final Locale locale;
private ResourceBundle resourceBundle;
private DateTimeFormatter dateTimeFormatter;
private List<Locale> candidateLocales;
SupportedLocale(String name, Locale locale) {
SupportedLocale(boolean isDefault, String name, Locale locale) {
this.isDefault = isDefault;
this.name = name;
this.locale = locale;
}
SupportedLocale(String name) {
this(false, name, Locale.forLanguageTag(name));
}
SupportedLocale(String name, Locale locale) {
this(false, name, locale);
}
public boolean isDefault() {
return false;
return isDefault;
}
public String getName() {
@@ -128,9 +132,6 @@ public final class Locales {
}
public String getDisplayName(SupportedLocale inLocale) {
if (inLocale.locale.getLanguage().equals("lzh"))
inLocale = ZH_HANT;
if (isDefault()) {
try {
return inLocale.getResourceBundle().getString("lang.default");
@@ -140,17 +141,32 @@ public final class Locales {
}
}
Locale inJavaLocale = inLocale.getLocale();
if (LocaleUtils.isISO3Language(inJavaLocale.getLanguage())) {
String iso1 = LocaleUtils.getISO1Language(inJavaLocale);
if (LocaleUtils.isISO1Language(iso1)) {
Locale.Builder builder = new Locale.Builder()
.setLocale(inJavaLocale)
.setLanguage(iso1);
if (inJavaLocale.getScript().isEmpty())
builder.setScript(LocaleUtils.getScript(inJavaLocale));
inJavaLocale = builder.build();
}
}
if (this.locale.getLanguage().equals("lzh")) {
if (LocaleUtils.isChinese(inLocale.locale))
if (inJavaLocale.getLanguage().equals("zh"))
return "文言";
String name = locale.getDisplayName(inLocale.getLocale());
String name = locale.getDisplayName(inJavaLocale);
return name.equals("lzh") || name.equals("Literary Chinese")
? "Chinese (Classical)"
: name;
}
return locale.getDisplayName(inLocale.getLocale());
return locale.getDisplayName(inJavaLocale);
}
public ResourceBundle getResourceBundle() {
@@ -237,8 +253,8 @@ public final class Locales {
}
public boolean isSameLanguage(SupportedLocale other) {
return (this.getLocale().getLanguage().equals(other.getLocale().getLanguage()))
|| (LocaleUtils.isChinese(this.getLocale()) && LocaleUtils.isChinese(other.getLocale()));
return LocaleUtils.getISO1Language(this.getLocale())
.equals(LocaleUtils.getISO1Language(other.getLocale()));
}
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {

View File

@@ -31,8 +31,8 @@ import java.util.ResourceBundle;
/// - For all Chinese locales, `zh-CN` is always added to the candidate list. If `zh-Hans` already exists in the candidate list,
/// `zh-CN` is inserted before `zh`; otherwise, it is inserted after `zh`.
/// - For all Traditional Chinese locales, `zh-TW` is always added to the candidate list (before `zh`).
/// - For all Chinese variants (such as `lzh`, `cmn`, `yue`, etc.), a candidate list with the language code replaced by `zh`
/// is added to the end of the candidate list.
/// - For all [supported][LocaleUtils#toISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
/// 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.
///
/// @author Glavo
public class DefaultResourceBundleControl extends ResourceBundle.Control {
@@ -52,53 +52,57 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
if (locale.getLanguage().isEmpty())
return getCandidateLocales(baseName, Locale.ENGLISH);
if (LocaleUtils.isChinese(locale)) {
String language = locale.getLanguage();
else if (LocaleUtils.isChinese(locale)) {
String script = locale.getScript();
if (script.isEmpty()) {
script = LocaleUtils.getScript(locale);
locale = new Locale.Builder()
.setLocale(locale)
.setScript(script)
.build();
if (!script.isEmpty())
return getCandidateLocales(baseName, new Locale.Builder()
.setLocale(locale)
.setScript(script)
.build());
}
}
List<Locale> locales = super.getCandidateLocales("", locale);
String language = locale.getLanguage();
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);
}
List<Locale> locales = super.getCandidateLocales(baseName, locale);
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
int chineseIdx = locales.indexOf(Locale.CHINESE);
// Is ISO 639-3 language tag
if (language.length() == 3) {
String iso1 = LocaleUtils.toISO1Language(locale.getLanguage());
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);
}
}
} else {
if (iso1.length() == 2) {
locales = ensureEditable(locales);
locales.removeIf(it -> !it.getLanguage().equals(language));
locales.addAll(getCandidateLocales("", new Locale.Builder()
locales.addAll(getCandidateLocales(baseName, new Locale.Builder()
.setLocale(locale)
.setLanguage("zh")
.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);
}
return locales;
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 super.getCandidateLocales(baseName, locale);
return locales;
}
}

View File

@@ -52,6 +52,20 @@ public final class LocaleUtils {
: locale.stripExtensions().toLanguageTag();
}
public static boolean isISO1Language(String language) {
return language.length() == 2;
}
public static boolean isISO3Language(String language) {
return language.length() == 3;
}
public static @NotNull String getISO1Language(Locale locale) {
String language = locale.getLanguage();
if (language.isEmpty()) return "en";
return isISO3Language(language) ? toISO1Language(language) : language;
}
/// Get the script of the locale. If the script is empty and the language is Chinese,
/// the script will be inferred based on the language, the region and the variant.
public static @NotNull String getScript(Locale locale) {
@@ -164,20 +178,29 @@ 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) {
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;
};
}
public static boolean isEnglish(Locale locale) {
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
return "en".equals(getISO1Language(locale));
}
public static final Set<String> CHINESE_TRADITIONAL_REGIONS = Set.of("TW", "HK", "MO");
public static final Set<String> CHINESE_LATN_VARIANTS = Set.of("pinyin", "wadegile", "tongyong");
public static final Set<String> CHINESE_LANGUAGES = Set.of(
"zh",
"zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue"
);
public static boolean isChinese(Locale locale) {
return CHINESE_LANGUAGES.contains(locale.getLanguage());
return "zh".equals(getISO1Language(locale));
}
private LocaleUtils() {

View File

@@ -71,8 +71,23 @@ 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("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("es", List.of("es", "und"));
assertCandidateLocales("spa", List.of("spa", "es", "und"));
assertCandidateLocales("ru", List.of("ru", "und"));
assertCandidateLocales("rus", List.of("rus", "ru", "und"));
assertCandidateLocales("uk", List.of("uk", "und"));
assertCandidateLocales("ukr", List.of("ukr", "uk", "und"));
assertCandidateLocales("und", List.of("en", "und"));
}