From cb467ae24006f7f50babdfcaf391615d791c5c05 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 23 Sep 2025 15:47:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=AC=E5=9C=B0=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20(#4539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化代码 - 修复错误用词 - 优化语言名称显示 --- .../jackhuang/hmcl/game/HMCLGameLauncher.java | 2 +- .../hmcl/util/i18n/SupportedLocale.java | 46 ++++++---- .../jackhuang/hmcl/util/i18n/LocaleUtils.java | 89 ++++++++----------- .../resources/assets/lang/sublanguages.csv | 2 +- .../hmcl/util/i18n/LocaleUtilsTest.java | 31 +++++-- .../l10n/LocaleNamesOverride.properties | 4 +- .../l10n/LocaleNamesOverride_lzh.properties | 20 +++++ docs/Localization_zh.md | 11 +-- 8 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride_lzh.properties 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 8278297c8..e19fc0721 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -110,7 +110,7 @@ public final class HMCLGameLauncher extends DefaultLauncher { private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) { String region = locale.getCountry(); - return switch (LocaleUtils.getISO2Language(locale)) { + return switch (LocaleUtils.getRootLanguage(locale)) { case "es" -> "es_ES"; case "ja" -> "ja_JP"; case "ru" -> "ru_RU"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java index 34d45919b..570fd2153 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java @@ -33,6 +33,7 @@ import java.time.temporal.TemporalAccessor; 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; @@ -117,11 +118,16 @@ public final class SupportedLocale { Locale currentLocale = this.getLocale(); String language = currentLocale.getLanguage(); - String script = currentLocale.getScript(); - - // Currently, HMCL does not support any locales with regions or variants, so they are not handled for now - // String region = currentLocale.getCountry(); - // String variant = currentLocale.getDisplayVariant(); + String subLanguage; + { + String parentLanguage = LocaleUtils.getParentLanguage(language); + if (parentLanguage != null) { + subLanguage = language; + language = parentLanguage; + } else { + subLanguage = ""; + } + } ResourceBundle localeNames = inLocale.getLocaleNamesBundle(); @@ -132,19 +138,21 @@ public final class SupportedLocale { LOG.warning("Failed to get localized name for language " + language, e); } - if (script.isEmpty()) { - return languageDisplayName; - } + // Currently, HMCL does not support any locales with regions or variants, so they are not handled for now + List subTags = Stream.of(subLanguage, currentLocale.getScript()) + .filter(it -> !it.isEmpty()) + .map(it -> { + try { + return localeNames.getString(it); + } catch (Throwable e) { + LOG.warning("Failed to get localized name of " + it, e); + } + return it; + }).toList(); - String scriptDisplayName = script; - - try { - scriptDisplayName = localeNames.getString(script); - } catch (Throwable e) { - LOG.warning("Failed to get localized name for script " + script, e); - } - - return languageDisplayName + " (" + scriptDisplayName + ")"; + return subTags.isEmpty() + ? languageDisplayName + : languageDisplayName + " (" + String.join(", ", subTags) + ")"; } public ResourceBundle getResourceBundle() { @@ -245,8 +253,8 @@ public final class SupportedLocale { } public boolean isSameLanguage(SupportedLocale other) { - return LocaleUtils.getISO2Language(this.getLocale()) - .equals(LocaleUtils.getISO2Language(other.getLocale())); + return LocaleUtils.getRootLanguage(this.getLocale()) + .equals(LocaleUtils.getRootLanguage(other.getLocale())); } public static final class TypeAdapter extends com.google.gson.TypeAdapter { 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 44d60344a..e04e42f88 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 @@ -116,24 +116,26 @@ public final class LocaleUtils { : locale.stripExtensions().toLanguageTag(); } - public static @NotNull String getISO2Language(Locale locale) { - String language = locale.getLanguage(); + public static @NotNull String getRootLanguage(Locale locale) { + return getRootLanguage(locale.getLanguage()); + } + + /// - If `language` is a sublanguage of a [macrolanguage](https://en.wikipedia.org/wiki/ISO_639_macrolanguage), + /// return the macrolanguage; + /// - If `language` is an ISO 639 alpha-3 language code and there is a corresponding ISO 639 alpha-2 language code, return the ISO 639 alpha-2 code; + /// - If `language` is empty, return `en`; + /// - Otherwise, return the `language`. + public static @NotNull String getRootLanguage(String language) { if (language.isEmpty()) return "en"; if (language.length() <= 2) return language; - String lang = language; - while (lang != null) { - if (lang.length() <= 2) - return lang; - else { - String iso1 = mapToISO2Language(lang); - if (iso1 != null) - return iso1; - } - lang = getParentLanguage(lang); - } - return language; + String iso2 = mapToISO2Language(language); + if (iso2 != null) + return iso2; + + String parent = getParentLanguage(language); + return parent != null ? parent : language; } /// Get the script of the locale. If the script is empty and the language is Chinese, @@ -180,66 +182,52 @@ public final class LocaleUtils { ArrayList result = new ArrayList<>(); do { - List languages; + String currentLanguage; - if (language.isEmpty()) { - result.add(Locale.ROOT); - break; - } else if (language.length() <= 2) { - languages = List.of(language); + if (language.length() <= 2) { + currentLanguage = language; } else { - String iso1Language = mapToISO2Language(language); - languages = iso1Language != null - ? List.of(language, iso1Language) - : List.of(language); + String iso2 = mapToISO2Language(language); + currentLanguage = iso2 != null + ? iso2 + : language; } - addCandidateLocales(result, languages, script, region, variants); + addCandidateLocales(result, currentLanguage, script, region, variants); } while ((language = getParentLanguage(language)) != null); + result.add(Locale.ROOT); return List.copyOf(result); } private static void addCandidateLocales(ArrayList list, - List languages, + String language, String script, String region, List variants) { if (!variants.isEmpty()) { for (String v : variants) { - for (String language : languages) { - list.add(getInstance(language, script, region, v)); - } + list.add(getInstance(language, script, region, v)); } } if (!region.isEmpty()) { - for (String language : languages) { - list.add(getInstance(language, script, region, "")); - } + list.add(getInstance(language, script, region, "")); } if (!script.isEmpty()) { - for (String language : languages) { - list.add(getInstance(language, script, "", "")); - } + list.add(getInstance(language, script, "", "")); if (!variants.isEmpty()) { for (String v : variants) { - for (String language : languages) { - list.add(getInstance(language, "", region, v)); - } + list.add(getInstance(language, "", region, v)); } } if (!region.isEmpty()) { - for (String language : languages) { - list.add(getInstance(language, "", region, "")); - } + list.add(getInstance(language, "", region, "")); } } - for (String language : languages) { - list.add(getInstance(language, "", "", "")); - } + list.add(getInstance(language, "", "", "")); - if (languages.contains("zh")) { + if (language.equals("zh")) { if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) { int chineseIdx = list.indexOf(Locale.CHINESE); if (chineseIdx >= 0) @@ -353,25 +341,26 @@ public final class LocaleUtils { // --- /// Map ISO 639 alpha-3 language codes to ISO 639 alpha-2 language codes. + /// Returns `null` if there is no corresponding ISO 639 alpha-2 language code. public static @Nullable String mapToISO2Language(String iso3Language) { return iso3To2.get(iso3Language); } + /// If `language` is a sublanguage of a [macrolanguage](https://en.wikipedia.org/wiki/ISO_639_macrolanguage), + /// return the macrolanguage; otherwise, return `null`. public static @Nullable String getParentLanguage(String language) { - return !language.isEmpty() - ? subLanguageToParent.getOrDefault(language, "") - : null; + return subLanguageToParent.get(language); } public static boolean isEnglish(Locale locale) { - return "en".equals(getISO2Language(locale)); + return "en".equals(getRootLanguage(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 boolean isChinese(Locale locale) { - return "zh".equals(getISO2Language(locale)); + return "zh".equals(getRootLanguage(locale)); } private LocaleUtils() { diff --git a/HMCLCore/src/main/resources/assets/lang/sublanguages.csv b/HMCLCore/src/main/resources/assets/lang/sublanguages.csv index 1ed422f35..5c8217c0e 100644 --- a/HMCLCore/src/main/resources/assets/lang/sublanguages.csv +++ b/HMCLCore/src/main/resources/assets/lang/sublanguages.csv @@ -1 +1 @@ -zh,cmn,lzh,cdo,cjy,cpx,czh,gan,hak,hsn,mnp,nan,wuu,yue \ No newline at end of file +zh,cdo,cjy,cmn,cnp,cpx,csp,czh,czo,gan,hak,hnm,hsn,luh,lzh,mnp,nan,sjc,wuu,yue \ No newline at end of file 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 906e51e07..9074138e5 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 @@ -62,7 +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("zho", List.of("zh-Hans", "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")); @@ -71,23 +71,23 @@ public final class LocaleUtilsTest { assertCandidateLocales("yue", List.of("yue-Hans", "yue", "zh-Hans", "zh-CN", "zh", "und")); assertCandidateLocales("ja", List.of("ja", "und")); + assertCandidateLocales("jpn", List.of("ja", "und")); assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und")); - assertCandidateLocales("jpn", List.of("jpn", "ja", "und")); - assertCandidateLocales("jpn-JP", List.of("jpn-JP", "ja-JP", "jpn", "ja", "und")); + assertCandidateLocales("jpn-JP", List.of("ja-JP", "ja", "und")); assertCandidateLocales("en", List.of("en", "und")); + assertCandidateLocales("eng", 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", "en-US", "eng", "en", "und")); + assertCandidateLocales("eng-US", List.of("en-US", "en", "und")); assertCandidateLocales("es", List.of("es", "und")); - assertCandidateLocales("spa", List.of("spa", "es", "und")); + assertCandidateLocales("spa", List.of("es", "und")); assertCandidateLocales("ru", List.of("ru", "und")); - assertCandidateLocales("rus", List.of("rus", "ru", "und")); + assertCandidateLocales("rus", List.of("ru", "und")); assertCandidateLocales("uk", List.of("uk", "und")); - assertCandidateLocales("ukr", List.of("ukr", "uk", "und")); + assertCandidateLocales("ukr", List.of("uk", "und")); assertCandidateLocales("und", List.of("en", "und")); } @@ -192,7 +192,7 @@ public final class LocaleUtilsTest { } @Test - public void testMapToISO2Language() throws IOException { + public void testMapToISO2Language() { assertEquals("en", LocaleUtils.mapToISO2Language("eng")); assertEquals("es", LocaleUtils.mapToISO2Language("spa")); assertEquals("ja", LocaleUtils.mapToISO2Language("jpn")); @@ -207,4 +207,17 @@ public final class LocaleUtilsTest { assertNull(LocaleUtils.mapToISO2Language("lzh")); assertNull(LocaleUtils.mapToISO2Language("tlh")); } + + @Test + public void testGetParentLanguage() { + assertEquals("zh", LocaleUtils.getParentLanguage("cmn")); + assertEquals("zh", LocaleUtils.getParentLanguage("yue")); + assertEquals("zh", LocaleUtils.getParentLanguage("lzh")); + + assertNull(LocaleUtils.getParentLanguage("")); + assertNull(LocaleUtils.getParentLanguage("en")); + assertNull(LocaleUtils.getParentLanguage("eng")); + assertNull(LocaleUtils.getParentLanguage("zh")); + assertNull(LocaleUtils.getParentLanguage("zho")); + } } diff --git a/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride.properties b/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride.properties index b5cfee7c7..a3e98903b 100644 --- a/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride.properties +++ b/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride.properties @@ -17,7 +17,7 @@ # # Languages -lzh=Classical Chinese +lzh=Classical # Scripts -Qabs=Upside down +Qabs=Upside Down diff --git a/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride_lzh.properties b/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride_lzh.properties new file mode 100644 index 000000000..5c91a7a6d --- /dev/null +++ b/buildSrc/src/main/resources/org/jackhuang/hmcl/gradle/l10n/LocaleNamesOverride_lzh.properties @@ -0,0 +1,20 @@ +# +# Hello Minecraft! Launcher +# Copyright (C) 2025 huangyuhui and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Languages +en=英吉利文 diff --git a/docs/Localization_zh.md b/docs/Localization_zh.md index 9b6369375..1df599346 100644 --- a/docs/Localization_zh.md +++ b/docs/Localization_zh.md @@ -178,16 +178,11 @@ HMCL 的维护者会替你完成其他步骤。 对于能够混合的资源 (例如 `.properties` 文件),HMCL 会根据此列表的优先级混合资源; 对于难以混合的资源 (例如字体文件),HMCL 会根据此列表加载找到的最高优先级的资源。 -如果当前语言使用 ISO 639-3 标准的三字符标签,那么 HMCL 也会尝试搜索其对应的 ISO 639-1 两字符标签所对应的资源。 -如果一个语言没有两字符标签,但其对应的宏语言存在两字符标签,那么 HMCL 会搜索对应的宏语言的两字符标签的资源。 +如果当前语言使用 ISO 639 标准的三字符标签,但同时也存在对应的两字符标签,那么 HMCL 会将其映射至两字符后再搜索资源。 -例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会根据以下列表的顺序搜索对应的本地化资源: +例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会将其映射至 `en-US` 后再根据上述规则搜索本地化资源。 -1. `eng-US` -2. `en-US` -3. `eng` -4. `en` -5. `und` +如果当前语言是一个 [ISO 639 宏语言](https://en.wikipedia.org/wiki/ISO_639_macrolanguage)的子语言,那么 HMCL 也会搜索宏语言对应的资源。 ### 对于中文的额外规则