From 73531dbf60d09704df85cd85d44ab70b19f3b747 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 30 Aug 2025 22:21:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E8=A8=80=E6=96=87?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=20(#4361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/download/VersionsPage.java | 2 +- .../org/jackhuang/hmcl/util/i18n/I18n.java | 14 +- .../org/jackhuang/hmcl/util/i18n/Locales.java | 36 ++- .../jackhuang/hmcl/util/i18n/WenyanUtils.java | 212 ++++++++++++++++++ .../resources/assets/lang/I18N_lzh.properties | 2 +- .../hmcl/util/i18n/WenyanUtilsTest.java | 42 ++++ .../util/versioning/GameVersionNumber.java | 52 ++++- 7 files changed, 338 insertions(+), 22 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java create mode 100644 HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 73476d12f..ba2bcec56 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -181,7 +181,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab } setGraphic(pane); - content.setTitle(remoteVersion.getSelfVersion()); + content.setTitle(I18n.getDisplaySelfVersion(remoteVersion)); if (remoteVersion.getReleaseDate() != null) { content.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate())); } else { 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 61f5b49ff..3df2fc80b 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 @@ -17,10 +17,9 @@ */ package org.jackhuang.hmcl.util.i18n; +import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.*; @@ -33,12 +32,10 @@ public final class I18n { private static volatile SupportedLocale locale = Locales.DEFAULT; private static volatile ResourceBundle resourceBundle = locale.getResourceBundle(); - private static volatile DateTimeFormatter dateTimeFormatter; public static void setLocale(SupportedLocale locale) { I18n.locale = locale; resourceBundle = locale.getResourceBundle(); - dateTimeFormatter = null; } public static boolean isUseChinese() { @@ -75,12 +72,11 @@ public final class I18n { } public static String formatDateTime(TemporalAccessor time) { - DateTimeFormatter formatter = dateTimeFormatter; - if (formatter == null) { - formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format")).withZone(ZoneId.systemDefault()); - } + return locale.formatDateTime(time); + } - return formatter.format(time); + public static String getDisplaySelfVersion(RemoteVersion version) { + return locale.getDisplaySelfVersion(version); } public static boolean hasKey(String key) { 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 e19467c2a..a2fb4c6a4 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 @@ -20,8 +20,14 @@ package org.jackhuang.hmcl.util.i18n; import com.google.gson.annotations.JsonAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.game.GameRemoteVersion; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; 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; @@ -65,7 +71,20 @@ public final class Locales { /** * Wenyan (Classical Chinese) */ - public static final SupportedLocale WENYAN = new SupportedLocale(Locale.forLanguageTag("lzh")); + public static final SupportedLocale WENYAN = new SupportedLocale(Locale.forLanguageTag("lzh")) { + @Override + public String formatDateTime(TemporalAccessor time) { + return WenyanUtils.formatDateTime(time); + } + + @Override + public String getDisplaySelfVersion(RemoteVersion version) { + if (version instanceof GameRemoteVersion) + return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion())); + else + return WenyanUtils.translateGenericVersion(version.getSelfVersion()); + } + }; public static final List LOCALES = List.of(DEFAULT, EN, ES, JA, RU, ZH_CN, ZH, WENYAN); @@ -104,9 +123,10 @@ public final class Locales { } @JsonAdapter(SupportedLocale.TypeAdapter.class) - public static final class SupportedLocale { + public static class SupportedLocale { private final Locale locale; private ResourceBundle resourceBundle; + private DateTimeFormatter dateTimeFormatter; SupportedLocale(Locale locale) { this.locale = locale; @@ -162,6 +182,18 @@ public final class Locales { return bundle; } + public String formatDateTime(TemporalAccessor time) { + DateTimeFormatter formatter = dateTimeFormatter; + if (formatter == null) + formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format")) + .withZone(ZoneId.systemDefault()); + return formatter.format(time); + } + + public String getDisplaySelfVersion(RemoteVersion version) { + return version.getSelfVersion(); + } + public static final class TypeAdapter extends com.google.gson.TypeAdapter { @Override public void write(JsonWriter out, SupportedLocale value) throws IOException { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java new file mode 100644 index 000000000..2edaaca84 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/WenyanUtils.java @@ -0,0 +1,212 @@ +/* + * 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.jackhuang.hmcl.util.versioning.GameVersionNumber; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Glavo + */ +public final class WenyanUtils { + private static final String DOT = "點"; + + private static final String[] NUMBERS = { + "〇", "一", "二", "三", "四", "五", "六", "七", "八", "九", + "十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", + "廿", "廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", + "卅", "卅一", "卅二", "卅三", "卅四", "卅五", "卅六", "卅七", "卅八", "卅九", + "卌", "卌一", "卌二", "卌三", "卌四", "卌五", "卌六", "卌七", "卌八", "卌九", + "五十", "五十一", "五十二", "五十三", "五十四", "五十五", "五十六", "五十七", "五十八", "五十九", + "六十", "六十一", "六十二", "六十三", "六十四", "六十五", "六十六", "六十七", "六十八", "六十九", + }; + + private static final char[] TIAN_GAN = {'甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'}; + private static final char[] DI_ZHI = {'子', '醜', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'}; + + private static String digitToString(char digit) { + return digit >= '0' && digit <= '9' + ? NUMBERS[digit - '0'] + : String.valueOf(digit); + } + + private static String numberToString(int number) { + return number >= 0 && number < NUMBERS.length ? NUMBERS[number] : String.valueOf(number); + } + + private static void appendDigitByDigit(StringBuilder builder, String number) { + for (int i = 0; i < number.length(); i++) { + builder.append(digitToString(number.charAt(i))); + } + } + + private static int mod(int a, int b) { + int r = a % b; + return r >= 0 ? r : r + b; + } + + static void appendYear(StringBuilder builder, int year) { + int yearOffset = year - 1984; + + builder.append(TIAN_GAN[mod(yearOffset, TIAN_GAN.length)]); + builder.append(DI_ZHI[mod(yearOffset, DI_ZHI.length)]); + } + + 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(' '); + + builder.append(numberToString(localDateTime.getHour())); + builder.append('时'); + 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; + + StringBuilder builder = new StringBuilder(); + appendDigitByDigit(builder, String.valueOf(release.getMajor())); + builder.append(DOT); + appendDigitByDigit(builder, String.valueOf(release.getMinor())); + + if (release.getPatch() != 0) { + builder.append(DOT); + appendDigitByDigit(builder, String.valueOf(release.getPatch())); + } + + if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) { + // do nothing + } else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) { + builder.append("之預"); + appendDigitByDigit(builder, release.getEaVersion().toString()); + } else if (release.getEaType() == GameVersionNumber.Release.TYPE_RC) { + builder.append("之候"); + appendDigitByDigit(builder, release.getEaVersion().toString()); + } else { + // Unsupported + return gameVersion.toString(); + } + + return builder.toString(); + } else if (gameVersion instanceof GameVersionNumber.Snapshot) { + var snapshot = (GameVersionNumber.Snapshot) gameVersion; + + StringBuilder builder = new StringBuilder(); + + appendDigitByDigit(builder, String.valueOf(snapshot.getYear())); + builder.append('週'); + appendDigitByDigit(builder, String.valueOf(snapshot.getWeek())); + + char suffix = snapshot.getSuffix(); + if (suffix >= 'a' && (suffix - 'a') < TIAN_GAN.length) + builder.append(TIAN_GAN[suffix - 'a']); + else + builder.append(suffix); + + 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; + } + + } else { + return gameVersion.toString(); + } + } + + private static final Pattern GENERIC_VERSION_PATTERN = + Pattern.compile("^[0-9]+(\\.[0-9]+)*"); + + public static String translateGenericVersion(String version) { + Matcher matcher = GENERIC_VERSION_PATTERN.matcher(version); + if (matcher.find()) { + String prefix = matcher.group(); + StringBuilder builder = new StringBuilder(version.length()); + + for (int i = 0; i < prefix.length(); i++) { + char ch = prefix.charAt(i); + if (ch >= '0' && ch <= '9') + builder.append(digitToString(ch)); + else if (ch == '.') + builder.append(DOT); + else + builder.append(ch); + } + builder.append(version, prefix.length(), version.length()); + return builder.toString(); + } + + return version; + } + + private WenyanUtils() { + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index d2d4e76a8..99bf4da9c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1197,7 +1197,7 @@ version.update=更模組包 wiki.tooltip=礦藝大典 wiki.version.game=https://zh.minecraft.wiki/w/Special:Search?search=%s&variant=zh-tw -wiki.version.game.search=爪哇版%s +wiki.version.game.search=Java版%s wizard.prev=< 前步 wizard.failed=未成 diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java new file mode 100644 index 000000000..c1e575168 --- /dev/null +++ b/HMCL/src/test/java/org/jackhuang/hmcl/util/i18n/WenyanUtilsTest.java @@ -0,0 +1,42 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Glavo + */ +public final class WenyanUtilsTest { + + private static void assertYearToString(String value, int year) { + StringBuilder builder = new StringBuilder(2); + WenyanUtils.appendYear(builder, year); + assertEquals(value, builder.toString()); + } + + @Test + public void testYearToString() { + assertYearToString("甲子", 1984); + assertYearToString("乙巳", 2025); + assertYearToString("甲子", -2996); + assertYearToString("庚子", 1000); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java index 7569aead8..b1c5ac909 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/GameVersionNumber.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.util.versioning; +import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; @@ -135,7 +136,7 @@ public abstract class GameVersionNumber implements Comparable return value; } - static final class Old extends GameVersionNumber { + public static final class Old extends GameVersionNumber { static Old parse(String value) { Type type; int prefixLength = 1; @@ -211,16 +212,16 @@ public abstract class GameVersionNumber implements Comparable } } - static final class Release extends GameVersionNumber { + public static final class Release extends GameVersionNumber { private static final Pattern PATTERN = Pattern.compile("1\\.(?[0-9]+)(\\.(?[0-9]+))?((?(-[a-zA-Z]+| Pre-Release ))(?.+))?"); - static final int TYPE_GA = Integer.MAX_VALUE; + public static final int TYPE_GA = Integer.MAX_VALUE; - static final int TYPE_UNKNOWN = 0; - static final int TYPE_EXP = 1; - static final int TYPE_PRE = 2; - static final int TYPE_RC = 3; + public static final int TYPE_UNKNOWN = 0; + public static final int TYPE_EXP = 1; + public static final int TYPE_PRE = 2; + public static final int TYPE_RC = 3; static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO); @@ -259,6 +260,7 @@ public abstract class GameVersionNumber implements Comparable private final int minor; private final int patch; + @MagicConstant(intValues = {TYPE_GA, TYPE_UNKNOWN, TYPE_EXP, TYPE_PRE, TYPE_RC}) private final int eaType; private final VersionNumber eaVersion; @@ -326,6 +328,26 @@ public abstract class GameVersionNumber implements Comparable throw new AssertionError(other.getClass()); } + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } + + public int getEaType() { + return eaType; + } + + public VersionNumber getEaVersion() { + return eaVersion; + } + @Override public int hashCode() { return Objects.hash(major, minor, patch, eaType, eaVersion); @@ -340,7 +362,7 @@ public abstract class GameVersionNumber implements Comparable } } - static final class Snapshot extends GameVersionNumber { + public static final class Snapshot extends GameVersionNumber { static Snapshot parse(String value) { if (value.length() != 6 || value.charAt(2) != 'w') throw new IllegalArgumentException(value); @@ -391,6 +413,18 @@ public abstract class GameVersionNumber implements Comparable throw new AssertionError(other.getClass()); } + public int getYear() { + return (intValue >> 16) & 0xff; + } + + public int getWeek() { + return (intValue >> 8) & 0xff; + } + + public char getSuffix() { + return (char) (intValue & 0xff); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -405,7 +439,7 @@ public abstract class GameVersionNumber implements Comparable } } - static final class Special extends GameVersionNumber { + public static final class Special extends GameVersionNumber { private VersionNumber versionNumber; private GameVersionNumber prev;