diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/FontManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/FontManager.java index e63c7ca49..fd1f6d598 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/FontManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/FontManager.java @@ -22,6 +22,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.scene.text.Font; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.util.Lazy; +import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemUtils; @@ -32,7 +33,6 @@ import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Locale; import java.util.Objects; import static org.jackhuang.hmcl.setting.ConfigHolder.config; @@ -69,10 +69,10 @@ public final class FontManager { return font; } + String fcMatchPattern; if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() - && Locale.getDefault() != Locale.ROOT - && !"en".equals(Locale.getDefault().getLanguage())) - return findByFcMatch(); + && !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty()) + return findByFcMatch(fcMatchPattern); else return null; }); @@ -123,20 +123,15 @@ public final class FontManager { return null; } - public static Font findByFcMatch() { + public static Font findByFcMatch(String pattern) { Path fcMatch = SystemUtils.which("fc-match"); if (fcMatch == null) return null; try { - Locale locale = Locale.getDefault(); - if (locale.getLanguage().equals("lzh")) - locale = Locale.TRADITIONAL_CHINESE; - String result = SystemUtils.run(fcMatch.toString(), - ":lang=" + locale.toLanguageTag(), + pattern, "--format", "%{family}\\n%{file}").trim(); - String[] results = result.split("\\n"); if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) { LOG.warning("Unexpected output from fc-match: " + result); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java index 47dbb25fe..c7e0cc45a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -43,7 +43,7 @@ public final class I18n { } public static boolean isUseChinese() { - return locale.getLocale().getLanguage().equals("zh"); + return Locales.isChinese(locale.getLocale()); } public static ResourceBundle getResourceBundle() { 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 b9fa2fb00..67f30a20d 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 @@ -28,9 +28,7 @@ import java.io.IOException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; +import java.util.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -184,35 +182,8 @@ public final class Locales { public ResourceBundle getResourceBundle() { ResourceBundle bundle = resourceBundle; - - if (resourceBundle == null) { - resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() { - @Override - public List getCandidateLocales(String baseName, Locale locale) { - if (isSimplifiedChinese(locale)) { - return List.of( - Locale.SIMPLIFIED_CHINESE, - Locale.CHINESE, - Locale.ROOT - ); - } - - if (locale.getLanguage().equals("lzh")) { - return List.of( - locale, - Locale.CHINESE, - Locale.ROOT - ); - } - - if (locale.getLanguage().equals("en")) { - return List.of(Locale.ROOT); - } - - return super.getCandidateLocales(baseName, locale); - } - }); - } + if (resourceBundle == null) + resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, Control.INSTANCE); return bundle; } @@ -229,6 +200,35 @@ public final class Locales { return version.getSelfVersion(); } + public String getFcMatchPattern() { + String language = locale.getLanguage(); + String country = locale.getCountry(); + + if (isEnglish(locale)) + return ""; + + if (isChinese(locale)) { + String lang; + String charset; + + if (isSimplifiedChinese(locale)) { + lang = country.equals("SG") || country.equals("MY") + ? "zh-" + country + : "zh-CN"; + charset = "0x6e38,0x620f"; + } else { + lang = country.equals("HK") || country.equals("MO") + ? "zh-" + country + : "zh-TW"; + charset = "0x904a,0x6232"; + } + + return ":lang=" + lang + ":charset=" + charset; + } + + return country.isEmpty() ? language : language + "-" + country; + } + public boolean isSameLanguage(SupportedLocale other) { return this.getLocale().getLanguage().equals(other.getLocale().getLanguage()) || isChinese(this.getLocale()) && isChinese(other.getLocale()); @@ -246,4 +246,51 @@ public final class Locales { } } } + + public static final class Control extends ResourceBundle.Control { + public static final Control INSTANCE = new Control(); + + @Override + public List getCandidateLocales(String baseName, Locale locale) { + List candidateLocales = super.getCandidateLocales(baseName, locale); + if (isChinese(locale)) { + int chineseIndex = candidateLocales.indexOf(Locale.CHINESE); + + // For "lzh" and "cmn" + if (chineseIndex < 0) { + if (!(candidateLocales instanceof ArrayList)) + candidateLocales = new ArrayList<>(candidateLocales); + + int i = candidateLocales.size() - 1; + while (i >= 0) { + Locale l = candidateLocales.get(i); + if (!isEnglish(l)) + break; + i--; + } + + chineseIndex = i + 1; + candidateLocales.add(chineseIndex, Locale.CHINESE); + } + + if (isSimplifiedChinese(locale)) { + if (!candidateLocales.contains(Locale.SIMPLIFIED_CHINESE)) { + if (!(candidateLocales instanceof ArrayList)) + candidateLocales = new ArrayList<>(candidateLocales); + candidateLocales.add(chineseIndex, Locale.SIMPLIFIED_CHINESE); + } + } + } + + if (candidateLocales.size() == 1 && candidateLocales.get(0).getLanguage().isEmpty()) { + if (!(candidateLocales instanceof ArrayList)) + candidateLocales = new ArrayList<>(candidateLocales); + + candidateLocales.add(0, Locale.ENGLISH); + } + + return candidateLocales; + } + + } } diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/LocalesTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/LocalesTest.java new file mode 100644 index 000000000..5767953d5 --- /dev/null +++ b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/LocalesTest.java @@ -0,0 +1,90 @@ +/* + * 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 . + */ +package org.jackhuang.hmcl.util.i18n; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +public final class LocalesTest { + private static void assertCandidateLocales(String languageTag, List candidateLocales) { + assertEquals(candidateLocales, + Locales.Control.INSTANCE.getCandidateLocales("", Locale.forLanguageTag(languageTag)) + .stream() + .map(Locale::toLanguageTag) + .collect(Collectors.toList())); + } + + @Test + public void testGetCandidateLocales() { + assertCandidateLocales("zh", List.of("zh-CN", "zh", "und")); + assertCandidateLocales("zh-CN", List.of("zh-Hans-CN", "zh-Hans", "zh-CN", "zh", "und")); + assertCandidateLocales("zh-Hans", List.of("zh-Hans", "zh-CN", "zh", "und")); + assertCandidateLocales("zh-Hant", List.of("zh-Hant", "zh-TW", "zh", "und")); + assertCandidateLocales("zh-Hans-US", List.of("zh-Hans-US", "zh-Hans", "zh-US", "zh-CN", "zh", "und")); + assertCandidateLocales("zh-US", List.of("zh-US", "zh", "und")); + assertCandidateLocales("zh-TW", List.of("zh-Hant-TW", "zh-Hant", "zh-TW", "zh", "und")); + assertCandidateLocales("zh-SG", List.of("zh-Hans-SG", "zh-Hans", "zh-SG", "zh-CN", "zh", "und")); + assertCandidateLocales("zh-MY", List.of("zh-MY", "zh-CN", "zh", "und")); + assertCandidateLocales("lzh", List.of("lzh", "zh", "und")); + assertCandidateLocales("cmn", List.of("cmn", "zh-CN", "zh", "und")); + assertCandidateLocales("cmn-Hans", List.of("cmn-Hans", "cmn", "zh-CN", "zh", "und")); + + assertCandidateLocales("ja", List.of("ja", "und")); + assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und")); + + assertCandidateLocales("en", List.of("en", "und")); + assertCandidateLocales("und", List.of("en", "und")); + } + + @Test + public void testIsChinese() { + assertTrue(Locales.isChinese(Locale.CHINESE)); + assertTrue(Locales.isChinese(Locale.SIMPLIFIED_CHINESE)); + assertTrue(Locales.isChinese(Locale.TRADITIONAL_CHINESE)); + assertTrue(Locales.isChinese(Locale.forLanguageTag("lzh"))); + assertTrue(Locales.isChinese(Locale.forLanguageTag("cmn"))); + assertTrue(Locales.isChinese(Locale.forLanguageTag("cmn-Hans"))); + } + + @Test + public void testIsSimplifiedChinese() { + assertTrue(Locales.isSimplifiedChinese(Locale.CHINESE)); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-Hans"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-Hans-US"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-SG"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-MY"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("cmn"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("cmn-Hans"))); + assertTrue(Locales.isSimplifiedChinese(Locale.forLanguageTag("cmn-CN"))); + + assertFalse(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-Hant"))); + assertFalse(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-TW"))); + assertFalse(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-HK"))); + assertFalse(Locales.isSimplifiedChinese(Locale.forLanguageTag("zh-MO"))); + assertFalse(Locales.isSimplifiedChinese(Locale.forLanguageTag("cmn-Hant"))); + } +}