优化对于 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) { private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String language = locale.getLanguage();
String region = locale.getCountry(); String region = locale.getCountry();
switch (language) { return switch (LocaleUtils.getISO1Language(locale)) {
case "ru": case "es" -> "es_ES";
return "ru_RU"; case "ja" -> "ja_JP";
case "uk": case "ru" -> "ru_RU";
return "uk_UA"; case "uk" -> "uk_UA";
case "es": case "zh" -> {
return "es_ES"; if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
case "ja": yield "lzh";
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 ""; 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 @Override

View File

@@ -34,45 +34,39 @@ public final class Locales {
private 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()) { public static final SupportedLocale DEFAULT;
@Override
public boolean isDefault() { static {
return true; String language = System.getenv("HMCL_LANGUAGE");
} DEFAULT = new SupportedLocale(true, "def",
}; StringUtils.isBlank(language) ? LocaleUtils.SYSTEM_DEFAULT : Locale.forLanguageTag(language));
}
/** /**
* English * English
*/ */
public static final SupportedLocale EN = new SupportedLocale("en", Locale.ENGLISH); public static final SupportedLocale EN = new SupportedLocale("en");
/** /**
* Spanish * Spanish
*/ */
public static final SupportedLocale ES = new SupportedLocale("es", Locale.forLanguageTag("es")); public static final SupportedLocale ES = new SupportedLocale("es");
/** /**
* Russian * Russian
*/ */
public static final SupportedLocale RU = new SupportedLocale("ru", Locale.forLanguageTag("ru")); public static final SupportedLocale RU = new SupportedLocale("ru");
/** /**
* Ukrainian * Ukrainian
*/ */
public static final SupportedLocale UK = new SupportedLocale("uk", Locale.forLanguageTag("uk")); public static final SupportedLocale UK = new SupportedLocale("uk");
/** /**
* Japanese * Japanese
*/ */
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE); public static final SupportedLocale JA = new SupportedLocale("ja");
/** /**
* Chinese (Simplified) * Chinese (Simplified)
@@ -87,7 +81,7 @@ public final class Locales {
/** /**
* Wenyan (Classical Chinese) * 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); 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) @JsonAdapter(SupportedLocale.TypeAdapter.class)
public static class SupportedLocale { public static final class SupportedLocale {
private final boolean isDefault;
private final String name; private final String name;
private final Locale locale; private final Locale locale;
private ResourceBundle resourceBundle; private ResourceBundle resourceBundle;
private DateTimeFormatter dateTimeFormatter; private DateTimeFormatter dateTimeFormatter;
private List<Locale> candidateLocales; private List<Locale> candidateLocales;
SupportedLocale(String name, Locale locale) { SupportedLocale(boolean isDefault, String name, Locale locale) {
this.isDefault = isDefault;
this.name = name; this.name = name;
this.locale = locale; this.locale = locale;
} }
SupportedLocale(String name) {
this(false, name, Locale.forLanguageTag(name));
}
SupportedLocale(String name, Locale locale) {
this(false, name, locale);
}
public boolean isDefault() { public boolean isDefault() {
return false; return isDefault;
} }
public String getName() { public String getName() {
@@ -128,9 +132,6 @@ public final class Locales {
} }
public String getDisplayName(SupportedLocale inLocale) { public String getDisplayName(SupportedLocale inLocale) {
if (inLocale.locale.getLanguage().equals("lzh"))
inLocale = ZH_HANT;
if (isDefault()) { if (isDefault()) {
try { try {
return inLocale.getResourceBundle().getString("lang.default"); 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 (this.locale.getLanguage().equals("lzh")) {
if (LocaleUtils.isChinese(inLocale.locale)) if (inJavaLocale.getLanguage().equals("zh"))
return "文言"; return "文言";
String name = locale.getDisplayName(inLocale.getLocale()); String name = locale.getDisplayName(inJavaLocale);
return name.equals("lzh") || name.equals("Literary Chinese") return name.equals("lzh") || name.equals("Literary Chinese")
? "Chinese (Classical)" ? "Chinese (Classical)"
: name; : name;
} }
return locale.getDisplayName(inLocale.getLocale()); return locale.getDisplayName(inJavaLocale);
} }
public ResourceBundle getResourceBundle() { public ResourceBundle getResourceBundle() {
@@ -237,8 +253,8 @@ public final class Locales {
} }
public boolean isSameLanguage(SupportedLocale other) { public boolean isSameLanguage(SupportedLocale other) {
return (this.getLocale().getLanguage().equals(other.getLocale().getLanguage())) return LocaleUtils.getISO1Language(this.getLocale())
|| (LocaleUtils.isChinese(this.getLocale()) && LocaleUtils.isChinese(other.getLocale())); .equals(LocaleUtils.getISO1Language(other.getLocale()));
} }
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> { 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, /// - 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`. /// `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 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` /// - For all [supported][LocaleUtils#toISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
/// is added to the end of the candidate list. /// 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 /// @author Glavo
public class DefaultResourceBundleControl extends ResourceBundle.Control { public class DefaultResourceBundleControl extends ResourceBundle.Control {
@@ -52,53 +52,57 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
public List<Locale> getCandidateLocales(String baseName, Locale locale) { public List<Locale> getCandidateLocales(String baseName, Locale locale) {
if (locale.getLanguage().isEmpty()) if (locale.getLanguage().isEmpty())
return getCandidateLocales(baseName, Locale.ENGLISH); return getCandidateLocales(baseName, Locale.ENGLISH);
else if (LocaleUtils.isChinese(locale)) {
if (LocaleUtils.isChinese(locale)) {
String language = locale.getLanguage();
String script = locale.getScript(); String script = locale.getScript();
if (script.isEmpty()) { if (script.isEmpty()) {
script = LocaleUtils.getScript(locale); script = LocaleUtils.getScript(locale);
locale = new Locale.Builder() if (!script.isEmpty())
.setLocale(locale) return getCandidateLocales(baseName, new Locale.Builder()
.setScript(script) .setLocale(locale)
.build(); .setScript(script)
.build());
} }
}
List<Locale> locales = super.getCandidateLocales("", locale); String language = locale.getLanguage();
if (language.equals("zh")) { List<Locale> locales = super.getCandidateLocales(baseName, locale);
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)) { // Is ISO 639-3 language tag
int chineseIdx = locales.indexOf(Locale.CHINESE); if (language.length() == 3) {
String iso1 = LocaleUtils.toISO1Language(locale.getLanguage());
if (chineseIdx >= 0) { if (iso1.length() == 2) {
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 {
locales = ensureEditable(locales); locales = ensureEditable(locales);
locales.removeIf(it -> !it.getLanguage().equals(language)); locales.removeIf(it -> !it.getLanguage().equals(language));
locales.addAll(getCandidateLocales("", new Locale.Builder() locales.addAll(getCandidateLocales(baseName, new Locale.Builder()
.setLocale(locale) .setLocale(locale)
.setLanguage("zh") .setLanguage(iso1)
.build())); .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(); : 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, /// 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. /// the script will be inferred based on the language, the region and the variant.
public static @NotNull String getScript(Locale locale) { 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) { 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_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_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) { public static boolean isChinese(Locale locale) {
return CHINESE_LANGUAGES.contains(locale.getLanguage()); return "zh".equals(getISO1Language(locale));
} }
private LocaleUtils() { private LocaleUtils() {

View File

@@ -71,8 +71,23 @@ public final class LocaleUtilsTest {
assertCandidateLocales("ja", List.of("ja", "und")); assertCandidateLocales("ja", List.of("ja", "und"));
assertCandidateLocales("ja-JP", List.of("ja-JP", "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", 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")); assertCandidateLocales("und", List.of("en", "und"));
} }