重构本地化支持 (#4632)

This commit is contained in:
Glavo
2025-10-09 15:18:45 +08:00
committed by GitHub
parent 23037b8afc
commit 35e63fc8c8
7 changed files with 197 additions and 126 deletions

View File

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

View File

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

View File

@@ -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<Locale> 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<Locale> 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()));

View File

@@ -0,0 +1,62 @@
/*
* 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.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);
}
}

View File

@@ -15,7 +15,10 @@
* 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;
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<Integer, Integer> MAPPER = loadMap();
private static Map<Integer, Integer> loadMap() {
var map = new LinkedHashMap<Integer, Integer>();
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() {
}
}

View File

@@ -15,8 +15,11 @@
* 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;
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();
}
}

View File

@@ -15,7 +15,7 @@
* 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;
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<String> 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
}