From 9ded2e489d8e31077293fdec054d9aed3499c77b Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 11 Sep 2025 19:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=B9=E4=BA=8E=20ISO=2063?= =?UTF-8?q?9-3=20=E8=AF=AD=E8=A8=80=E4=BB=A3=E7=A0=81=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20(#4455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/HMCLGameLauncher.java | 45 +++++------ .../org/jackhuang/hmcl/util/i18n/Locales.java | 76 +++++++++++-------- .../i18n/DefaultResourceBundleControl.java | 68 +++++++++-------- .../jackhuang/hmcl/util/i18n/LocaleUtils.java | 37 +++++++-- .../hmcl/util/i18n/LocaleUtilsTest.java | 15 ++++ 5 files changed, 145 insertions(+), 96 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index ce5353291..d1240c52c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -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 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java index 995b5b8f6..2333f462e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java @@ -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 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 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 { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java index bf28931e0..259b47159 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java @@ -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 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 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 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; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java index cebb0b611..796845304 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java @@ -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 CHINESE_TRADITIONAL_REGIONS = Set.of("TW", "HK", "MO"); public static final Set CHINESE_LATN_VARIANTS = Set.of("pinyin", "wadegile", "tongyong"); - public static final Set 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() { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java index df3383acc..18210d4f1 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java @@ -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")); }