优化文言文翻译 (#4361)

This commit is contained in:
Glavo 2025-08-30 22:21:45 +08:00 committed by GitHub
parent 5af6858d4c
commit 73531dbf60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 338 additions and 22 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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<SupportedLocale> 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<SupportedLocale> {
@Override
public void write(JsonWriter out, SupportedLocale value) throws IOException {

View File

@ -0,0 +1,212 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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() {
}
}

View File

@ -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=未成

View File

@ -0,0 +1,42 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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<GameVersionNumber>
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<GameVersionNumber>
}
}
static final class Release extends GameVersionNumber {
public static final class Release extends GameVersionNumber {
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>.+))?");
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<GameVersionNumber>
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<GameVersionNumber>
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<GameVersionNumber>
}
}
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<GameVersionNumber>
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<GameVersionNumber>
}
}
static final class Special extends GameVersionNumber {
public static final class Special extends GameVersionNumber {
private VersionNumber versionNumber;
private GameVersionNumber prev;