From 35e63fc8c89acc2fc41d6e19181c512b5b56da1a Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 9 Oct 2025 15:18:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=9C=AC=E5=9C=B0=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20(#4632)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/util/i18n/I18n.java | 21 +-- .../hmcl/util/i18n/MinecraftWiki.java | 5 +- .../hmcl/util/i18n/SupportedLocale.java | 51 ++++--- .../hmcl/util/i18n/translator/Translator.java | 62 +++++++++ .../Translator_en_Qabs.java} | 28 ++-- .../Translator_lzh.java} | 129 +++++++++--------- .../TranslatorTest.java} | 27 ++-- 7 files changed, 197 insertions(+), 126 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java rename HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/{UpsideDownUtils.java => translator/Translator_en_Qabs.java} (80%) rename HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/{WenyanUtils.java => translator/Translator_lzh.java} (79%) rename HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/{WenyanUtilsTest.java => translator/TranslatorTest.java} (74%) 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 281d09722..8a07cdbeb 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 @@ -19,7 +19,7 @@ package org.jackhuang.hmcl.util.i18n; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jackhuang.hmcl.util.i18n.translator.Translator; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -55,6 +55,10 @@ public final class I18n { return locale.getResourceBundle(); } + public static Translator getTranslator() { + return locale.getTranslator(); + } + public static String i18n(String key, Object... formatArgs) { return locale.i18n(key, formatArgs); } @@ -64,22 +68,11 @@ public final class I18n { } public static String formatDateTime(TemporalAccessor time) { - return locale.formatDateTime(time); + return getTranslator().formatDateTime(time); } public static String getDisplaySelfVersion(RemoteVersion version) { - if (locale.getLocale().getLanguage().equals("lzh")) { - if (version instanceof GameRemoteVersion) - return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion())); - else - return WenyanUtils.translateGenericVersion(version.getSelfVersion()); - } - - if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) { - return UpsideDownUtils.translate(version.getSelfVersion()); - } - - return version.getSelfVersion(); + return getTranslator().getDisplayVersion(version); } /// Find the builtin localized resource with given name and suffix. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java index ac1b5536e..54ea8493d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.util.i18n; import org.jackhuang.hmcl.download.game.GameRemoteVersion; +import org.jackhuang.hmcl.util.i18n.translator.Translator_lzh; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.util.Locale; @@ -36,9 +37,9 @@ public final class MinecraftWiki { if (wikiVersion.startsWith("2.0")) translatedVersion = "二點〇"; else if (wikiVersion.startsWith("1.0.0-rc2")) - translatedVersion = WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2")); + translatedVersion = Translator_lzh.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2")); else - translatedVersion = WenyanUtils.translateGameVersion(gameVersion); + translatedVersion = Translator_lzh.translateGameVersion(gameVersion); if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) { return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version); 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 570fd2153..a1d2b4ca9 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 @@ -24,12 +24,13 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.i18n.translator.Translator; import java.io.IOException; import java.io.InputStream; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -74,8 +75,8 @@ public final class SupportedLocale { private final Locale locale; private ResourceBundle resourceBundle; private ResourceBundle localeNamesBundle; - private DateTimeFormatter dateTimeFormatter; private List candidateLocales; + private Translator translator; SupportedLocale() { this.isDefault = true; @@ -200,23 +201,6 @@ public final class SupportedLocale { } } - public String formatDateTime(TemporalAccessor time) { - DateTimeFormatter formatter = dateTimeFormatter; - if (formatter == null) { - if (LocaleUtils.isEnglish(locale) && "Qabs".equals(locale.getScript())) { - return UpsideDownUtils.formatDateTime(time); - } - - if (locale.getLanguage().equals("lzh")) { - return WenyanUtils.formatDateTime(time); - } - - formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format")) - .withZone(ZoneId.systemDefault()); - } - return formatter.format(time); - } - public String getFcMatchPattern() { String language = locale.getLanguage(); String region = locale.getCountry(); @@ -252,6 +236,31 @@ public final class SupportedLocale { return region.isEmpty() ? language : language + "-" + region; } + public Translator getTranslator() { + Translator translator = this.translator; + if (translator != null) + return translator; + + List candidateLocales = getCandidateLocales(); + + for (Locale candidateLocale : candidateLocales) { + String className = DefaultResourceBundleControl.INSTANCE.toBundleName(Translator.class.getSimpleName(), candidateLocale); + if (Translator.class.getResource(className + ".class") != null) { + try { + Class clazz = Class.forName(Translator.class.getPackageName() + "." + className); + + MethodHandle constructor = MethodHandles.publicLookup() + .findConstructor(clazz, MethodType.methodType(void.class, SupportedLocale.class)); + + return this.translator = (Translator) constructor.invoke(this); + } catch (Throwable e) { + LOG.warning("Failed to create instance for " + className, e); + } + } + } + return this.translator = new Translator(this); + } + public boolean isSameLanguage(SupportedLocale other) { return LocaleUtils.getRootLanguage(this.getLocale()) .equals(LocaleUtils.getRootLanguage(other.getLocale())); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java new file mode 100644 index 000000000..026928919 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java @@ -0,0 +1,62 @@ +/* + * 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.translator; + +import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.util.i18n.SupportedLocale; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +/// @author Glavo +public class Translator { + protected final SupportedLocale supportedLocale; + protected final Locale locale; + + public Translator(SupportedLocale supportedLocale) { + this.supportedLocale = supportedLocale; + this.locale = supportedLocale.getLocale(); + } + + public final SupportedLocale getSupportedLocale() { + return supportedLocale; + } + + public final Locale getLocale() { + return locale; + } + + public String getDisplayVersion(RemoteVersion remoteVersion) { + return remoteVersion.getSelfVersion(); + } + + /// @see [#formatDateTime(TemporalAccessor)] + protected DateTimeFormatter dateTimeFormatter; + + public String formatDateTime(TemporalAccessor time) { + DateTimeFormatter formatter = dateTimeFormatter; + if (formatter == null) { + formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(supportedLocale.getResourceBundle().getString("datetime.format")) + .withZone(ZoneId.systemDefault()); + } + return formatter.format(time); + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/UpsideDownUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java similarity index 80% rename from HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/UpsideDownUtils.java rename to HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java index f951ce82c..24bf7edfe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/UpsideDownUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java @@ -15,7 +15,10 @@ * 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; +package org.jackhuang.hmcl.util.i18n.translator; + +import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.util.i18n.SupportedLocale; import java.io.IOException; import java.io.InputStream; @@ -30,13 +33,16 @@ import java.util.Map; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /// @author Glavo -public final class UpsideDownUtils { +public class Translator_en_Qabs extends Translator { + private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a") + .withZone(ZoneId.systemDefault()); + private static final Map MAPPER = loadMap(); private static Map loadMap() { var map = new LinkedHashMap(); - InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt"); + InputStream inputStream = Translator_en_Qabs.class.getResourceAsStream("/assets/lang/upside_down.txt"); if (inputStream != null) { try (inputStream) { new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> { @@ -65,13 +71,17 @@ public final class UpsideDownUtils { return builder.reverse().toString(); } - private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a") - .withZone(ZoneId.systemDefault()); + public Translator_en_Qabs(SupportedLocale locale) { + super(locale); + } - public static String formatDateTime(TemporalAccessor time) { + @Override + public String getDisplayVersion(RemoteVersion remoteVersion) { + return translate(remoteVersion.getSelfVersion()); + } + + @Override + public String formatDateTime(TemporalAccessor time) { return translate(BASE_FORMATTER.format(time)); } - - private UpsideDownUtils() { - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java similarity index 79% rename from HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java rename to HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java index 8b7087c0e..3bf7b6474 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java @@ -15,8 +15,11 @@ * 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; +package org.jackhuang.hmcl.util.i18n.translator; +import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.game.GameRemoteVersion; +import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.time.Instant; @@ -27,10 +30,8 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * @author Glavo - */ -public final class WenyanUtils { +/// @author Glavo +public class Translator_lzh extends Translator { private static final String DOT = "點"; private static final String[] NUMBERS = { @@ -79,37 +80,8 @@ public final class WenyanUtils { builder.append(hour % 2 == 0 ? '正' : '初'); } - public static String formatDateTime(TemporalAccessor time) { - LocalDateTime localDateTime; - if (time instanceof Instant) - localDateTime = ((Instant) time).atZone(ZoneId.systemDefault()).toLocalDateTime(); - else - localDateTime = LocalDateTime.from(time); - - StringBuilder builder = new StringBuilder(16); - - appendYear(builder, localDateTime.getYear()); - builder.append('年'); - builder.append(numberToString(localDateTime.getMonthValue())); - builder.append('月'); - builder.append(numberToString(localDateTime.getDayOfMonth())); - builder.append('日'); - - builder.append(' '); - - appendHour(builder, localDateTime.getHour()); - builder.append(numberToString(localDateTime.getMinute())); - builder.append('分'); - builder.append(numberToString(localDateTime.getSecond())); - builder.append('秒'); - - return builder.toString(); - } - public static String translateGameVersion(GameVersionNumber gameVersion) { - if (gameVersion instanceof GameVersionNumber.Release) { - var release = (GameVersionNumber.Release) gameVersion; - + if (gameVersion instanceof GameVersionNumber.Release release) { StringBuilder builder = new StringBuilder(); appendDigitByDigit(builder, String.valueOf(release.getMajor())); builder.append(DOT); @@ -120,6 +92,7 @@ public final class WenyanUtils { appendDigitByDigit(builder, String.valueOf(release.getPatch())); } + //noinspection StatementWithEmptyBody if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) { // do nothing } else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) { @@ -134,9 +107,7 @@ public final class WenyanUtils { } return builder.toString(); - } else if (gameVersion instanceof GameVersionNumber.Snapshot) { - var snapshot = (GameVersionNumber.Snapshot) gameVersion; - + } else if (gameVersion instanceof GameVersionNumber.Snapshot snapshot) { StringBuilder builder = new StringBuilder(); appendDigitByDigit(builder, String.valueOf(snapshot.getYear())); @@ -152,35 +123,20 @@ public final class WenyanUtils { return builder.toString(); } else if (gameVersion instanceof GameVersionNumber.Special) { String version = gameVersion.toString(); - switch (version.toLowerCase(Locale.ROOT)) { - case "2.0": - return "二點〇"; - case "2.0_blue": - return "二點〇藍"; - case "2.0_red": - return "二點〇赤"; - case "2.0_purple": - return "二點〇紫"; - case "1.rv-pre1": - return "一點真視之預一"; - case "3d shareware v1.34": - return "躍然享件一點三四"; - case "20w14infinite": - case "20w14~": - case "20w14∞": - return "二〇週一四宇"; - case "22w13oneblockatatime": - return "二二週一三典"; - case "23w13a_or_b": - return "二三週一三暨"; - case "24w14potato": - return "二四週一四芋"; - case "25w14craftmine": - return "二五週一四礦"; - default: - return version; - } - + return switch (version.toLowerCase(Locale.ROOT)) { + case "2.0" -> "二點〇"; + case "2.0_blue" -> "二點〇藍"; + case "2.0_red" -> "二點〇赤"; + case "2.0_purple" -> "二點〇紫"; + case "1.rv-pre1" -> "一點真視之預一"; + case "3d shareware v1.34" -> "躍然享件一點三四"; + case "20w14infinite", "20w14~", "20w14∞" -> "二〇週一四宇"; + case "22w13oneblockatatime" -> "二二週一三典"; + case "23w13a_or_b" -> "二三週一三暨"; + case "24w14potato" -> "二四週一四芋"; + case "25w14craftmine" -> "二五週一四礦"; + default -> version; + }; } else { return gameVersion.toString(); } @@ -211,6 +167,43 @@ public final class WenyanUtils { return version; } - private WenyanUtils() { + public Translator_lzh(SupportedLocale locale) { + super(locale); + } + + @Override + public String getDisplayVersion(RemoteVersion remoteVersion) { + if (remoteVersion instanceof GameRemoteVersion) + return translateGameVersion(GameVersionNumber.asGameVersion(remoteVersion.getSelfVersion())); + else + return translateGenericVersion(remoteVersion.getSelfVersion()); + } + + @Override + public String formatDateTime(TemporalAccessor time) { + LocalDateTime localDateTime; + if (time instanceof Instant instant) + localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime(); + else + localDateTime = LocalDateTime.from(time); + + StringBuilder builder = new StringBuilder(16); + + appendYear(builder, localDateTime.getYear()); + builder.append('年'); + builder.append(numberToString(localDateTime.getMonthValue())); + builder.append('月'); + builder.append(numberToString(localDateTime.getDayOfMonth())); + builder.append('日'); + + builder.append(' '); + + appendHour(builder, localDateTime.getHour()); + builder.append(numberToString(localDateTime.getMinute())); + builder.append('分'); + builder.append(numberToString(localDateTime.getSecond())); + builder.append('秒'); + + return builder.toString(); } } diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/translator/TranslatorTest.java similarity index 74% rename from HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java rename to HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/translator/TranslatorTest.java index e361ebd79..a59015235 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/translator/TranslatorTest.java @@ -15,7 +15,7 @@ * 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; +package org.jackhuang.hmcl.util.i18n.translator; import org.junit.jupiter.api.Test; @@ -26,33 +26,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Glavo */ -public final class WenyanUtilsTest { +public final class TranslatorTest { - private static void assertYearToString(String value, int year) { + //region lzh + + private static void assertYearToLZH(String value, int year) { StringBuilder builder = new StringBuilder(2); - WenyanUtils.appendYear(builder, year); + Translator_lzh.appendYear(builder, year); assertEquals(value, builder.toString()); } @Test - public void testYearToString() { - assertYearToString("甲子", 1984); - assertYearToString("乙巳", 2025); - assertYearToString("甲子", -2996); - assertYearToString("庚子", 1000); + public void testYearToLZH() { + assertYearToLZH("甲子", 1984); + assertYearToLZH("乙巳", 2025); + assertYearToLZH("甲子", -2996); + assertYearToLZH("庚子", 1000); } @Test - public void testHourToString() { - + public void testHourToLZH() { List list = List.of( "子正", "丑初", "丑正", "寅初", "寅正", "卯初", "卯正", "辰初", "辰正", "巳初", "巳正", "午初", "午正", "未初", "未正", "申初", "申正", "酉初", "酉正", "戌初", "戌正", "亥初", "亥正", "子初" ); for (int hour = 0; hour < list.size(); hour++) { StringBuilder builder = new StringBuilder(2); - WenyanUtils.appendHour(builder, hour); + Translator_lzh.appendHour(builder, hour); assertEquals(list.get(hour), builder.toString()); } } + + //endregion }