mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-19 00:36:10 -04:00
增强本地化支持 (#4379)
This commit is contained in:
parent
b38076f847
commit
6ab216dcb6
@ -21,7 +21,7 @@ import org.jackhuang.hmcl.Metadata;
|
|||||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
@ -81,7 +81,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Locale locale = Locale.getDefault();
|
Locale locale = Locale.getDefault();
|
||||||
if (Locales.isEnglish(locale))
|
if (LocaleUtils.isEnglish(locale))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -115,14 +115,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
String region = locale.getCountry();
|
String region = locale.getCountry();
|
||||||
|
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case "zh":
|
|
||||||
case "cmn":
|
|
||||||
if (Locales.isSimplifiedChinese(locale))
|
|
||||||
return "zh_CN";
|
|
||||||
if (gameVersion.compareTo("1.16") >= 0
|
|
||||||
&& (region.equals("HK") || region.equals("MO")))
|
|
||||||
return "zh_HK";
|
|
||||||
return "zh_TW";
|
|
||||||
case "ru":
|
case "ru":
|
||||||
return "ru_RU";
|
return "ru_RU";
|
||||||
case "uk":
|
case "uk":
|
||||||
@ -135,7 +127,18 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
return gameVersion.compareTo("1.16") >= 0
|
return gameVersion.compareTo("1.16") >= 0
|
||||||
? "lzh"
|
? "lzh"
|
||||||
: "";
|
: "";
|
||||||
|
case "zh":
|
||||||
default:
|
default:
|
||||||
|
if (LocaleUtils.isChinese(locale)) {
|
||||||
|
String script = LocaleUtils.getScript(locale);
|
||||||
|
if ("Hant".equals(script)) {
|
||||||
|
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
|
||||||
|
return "zh_HK";
|
||||||
|
return "zh_TW";
|
||||||
|
}
|
||||||
|
return "zh_CN";
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ import javafx.beans.property.SimpleObjectProperty;
|
|||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.util.Lazy;
|
import org.jackhuang.hmcl.util.Lazy;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.i18n.DefaultResourceBundleControl;
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
@ -29,11 +31,13 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Objects;
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
@ -50,7 +54,21 @@ public final class FontManager {
|
|||||||
public static final double DEFAULT_FONT_SIZE = 12.0f;
|
public static final double DEFAULT_FONT_SIZE = 12.0f;
|
||||||
|
|
||||||
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
|
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
|
||||||
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
Font font;
|
||||||
|
|
||||||
|
// Recommended
|
||||||
|
|
||||||
|
font = tryLoadLocalizedFont(Metadata.HMCL_CURRENT_DIRECTORY.resolve("font"));
|
||||||
|
if (font != null)
|
||||||
|
return font;
|
||||||
|
|
||||||
|
font = tryLoadLocalizedFont(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("font"));
|
||||||
|
if (font != null)
|
||||||
|
return font;
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
|
||||||
|
font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
|
||||||
if (font != null)
|
if (font != null)
|
||||||
return font;
|
return font;
|
||||||
|
|
||||||
@ -69,6 +87,8 @@ public final class FontManager {
|
|||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
|
|
||||||
String fcMatchPattern;
|
String fcMatchPattern;
|
||||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
||||||
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
|
&& !(fcMatchPattern = I18n.getLocale().getFcMatchPattern()).isEmpty())
|
||||||
@ -108,6 +128,7 @@ public final class FontManager {
|
|||||||
for (String extension : FONT_EXTENSIONS) {
|
for (String extension : FONT_EXTENSIONS) {
|
||||||
Path path = dir.resolve("font." + extension);
|
Path path = dir.resolve("font." + extension);
|
||||||
if (Files.isRegularFile(path)) {
|
if (Files.isRegularFile(path)) {
|
||||||
|
LOG.info("Load font file: " + path);
|
||||||
try {
|
try {
|
||||||
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||||
if (font != null) {
|
if (font != null) {
|
||||||
@ -123,6 +144,53 @@ public final class FontManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Font tryLoadLocalizedFont(Path dir) {
|
||||||
|
if (!Files.isDirectory(dir))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try (Stream<Path> list = Files.list(dir)) {
|
||||||
|
Map<String, Path> map = new HashMap<>();
|
||||||
|
|
||||||
|
Set<String> extensions = Set.of(FONT_EXTENSIONS);
|
||||||
|
list.forEach(file -> {
|
||||||
|
if (Files.isRegularFile(file)) {
|
||||||
|
String fileName = file.getFileName().toString();
|
||||||
|
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||||
|
|
||||||
|
if (fileName.startsWith("font") && extensions.contains(extension)) {
|
||||||
|
map.put(fileName.substring(0, fileName.length() - extension.length() - 1), file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List<Locale> candidateLocales = I18n.getLocale().getCandidateLocales();
|
||||||
|
|
||||||
|
for (Locale locale : candidateLocales) {
|
||||||
|
String key = DefaultResourceBundleControl.INSTANCE.toBundleName("font", locale);
|
||||||
|
|
||||||
|
Path path = map.get(key);
|
||||||
|
if (path != null) {
|
||||||
|
LOG.info("Load font file: " + path);
|
||||||
|
try {
|
||||||
|
Font font = Font.loadFont(
|
||||||
|
path.toAbsolutePath().normalize().toUri().toURL().toExternalForm(),
|
||||||
|
DEFAULT_FONT_SIZE);
|
||||||
|
if (font != null)
|
||||||
|
return font;
|
||||||
|
} catch (MalformedURLException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warning("Failed to load font " + path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warning("Failed to load font " + dir, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Font findByFcMatch(String pattern) {
|
public static Font findByFcMatch(String pattern) {
|
||||||
Path fcMatch = SystemUtils.which("fc-match");
|
Path fcMatch = SystemUtils.which("fc-match");
|
||||||
if (fcMatch == null)
|
if (fcMatch == null)
|
||||||
@ -147,6 +215,7 @@ public final class FontManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG.info("Load font file: " + path);
|
||||||
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
|
||||||
if (fonts == null) {
|
if (fonts == null) {
|
||||||
LOG.warning("Failed to load font from " + path);
|
LOG.warning("Failed to load font from " + path);
|
||||||
|
@ -30,11 +30,9 @@ public final class I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
||||||
private static volatile ResourceBundle resourceBundle = locale.getResourceBundle();
|
|
||||||
|
|
||||||
public static void setLocale(SupportedLocale locale) {
|
public static void setLocale(SupportedLocale locale) {
|
||||||
I18n.locale = locale;
|
I18n.locale = locale;
|
||||||
resourceBundle = locale.getResourceBundle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SupportedLocale getLocale() {
|
public static SupportedLocale getLocale() {
|
||||||
@ -42,11 +40,11 @@ public final class I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isUseChinese() {
|
public static boolean isUseChinese() {
|
||||||
return Locales.isChinese(locale.getLocale());
|
return LocaleUtils.isChinese(locale.getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResourceBundle getResourceBundle() {
|
public static ResourceBundle getResourceBundle() {
|
||||||
return resourceBundle;
|
return locale.getResourceBundle();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String i18n(String key, Object... formatArgs) {
|
public static String i18n(String key, Object... formatArgs) {
|
||||||
|
@ -22,9 +22,12 @@ import com.google.gson.stream.JsonReader;
|
|||||||
import com.google.gson.stream.JsonWriter;
|
import com.google.gson.stream.JsonWriter;
|
||||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.temporal.TemporalAccessor;
|
import java.time.temporal.TemporalAccessor;
|
||||||
@ -36,7 +39,15 @@ public final class Locales {
|
|||||||
private Locales() {
|
private Locales() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final SupportedLocale DEFAULT = new SupportedLocale("def", Locale.getDefault()) {
|
private static Locale getDefaultLocale() {
|
||||||
|
String language = System.getenv("HMCL_LANGUAGE");
|
||||||
|
if (StringUtils.isNotBlank(language))
|
||||||
|
return Locale.forLanguageTag(language);
|
||||||
|
else
|
||||||
|
return LocaleUtils.SYSTEM_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final SupportedLocale DEFAULT = new SupportedLocale("def", getDefaultLocale()) {
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayName(SupportedLocale inLocale) {
|
public String getDisplayName(SupportedLocale inLocale) {
|
||||||
try {
|
try {
|
||||||
@ -76,12 +87,12 @@ public final class Locales {
|
|||||||
/**
|
/**
|
||||||
* Chinese (Simplified)
|
* Chinese (Simplified)
|
||||||
*/
|
*/
|
||||||
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", Locale.forLanguageTag("zh-Hans"));
|
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", LocaleUtils.LOCALE_ZH_HANS);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chinese (Traditional)
|
* Chinese (Traditional)
|
||||||
*/
|
*/
|
||||||
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant"));
|
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", LocaleUtils.LOCALE_ZH_HANT);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wenyan (Classical Chinese)
|
* Wenyan (Classical Chinese)
|
||||||
@ -90,27 +101,14 @@ public final class Locales {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayName(SupportedLocale inLocale) {
|
public String getDisplayName(SupportedLocale inLocale) {
|
||||||
if (isChinese(inLocale.locale))
|
if (LocaleUtils.isChinese(inLocale.locale))
|
||||||
return "文言";
|
return "文言";
|
||||||
|
|
||||||
String name = super.getDisplayName(inLocale);
|
String name = super.getDisplayName(inLocale);
|
||||||
return name.equals("lzh") || name.equals("Literary Chinese")
|
return name.equals("lzh") || name.equals("Literary Chinese")
|
||||||
? "Classical Chinese"
|
? "Chinese (Classical)"
|
||||||
: name;
|
: name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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, UK, ZH_HANS, ZH_HANT, WENYAN);
|
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
|
||||||
@ -126,40 +124,13 @@ public final class Locales {
|
|||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEnglish(Locale locale) {
|
|
||||||
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isChinese(Locale locale) {
|
|
||||||
switch (locale.getLanguage()) {
|
|
||||||
case "zh":
|
|
||||||
case "lzh":
|
|
||||||
case "cmn":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isSimplifiedChinese(Locale locale) {
|
|
||||||
if (locale.getLanguage().equals("zh") || locale.getLanguage().equals("cmn")) {
|
|
||||||
String script = locale.getScript();
|
|
||||||
if (script.isEmpty()) {
|
|
||||||
String region = locale.getCountry();
|
|
||||||
return region.isEmpty() || region.equals("CN") || region.equals("SG") || region.equals("MY");
|
|
||||||
} else
|
|
||||||
return script.equals("Hans");
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||||
public static class SupportedLocale {
|
public static class SupportedLocale {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final Locale locale;
|
private final Locale locale;
|
||||||
private ResourceBundle resourceBundle;
|
private ResourceBundle resourceBundle;
|
||||||
private DateTimeFormatter dateTimeFormatter;
|
private DateTimeFormatter dateTimeFormatter;
|
||||||
|
private List<Locale> candidateLocales;
|
||||||
|
|
||||||
SupportedLocale(String name, Locale locale) {
|
SupportedLocale(String name, Locale locale) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -183,11 +154,18 @@ public final class Locales {
|
|||||||
public ResourceBundle getResourceBundle() {
|
public ResourceBundle getResourceBundle() {
|
||||||
ResourceBundle bundle = resourceBundle;
|
ResourceBundle bundle = resourceBundle;
|
||||||
if (resourceBundle == null)
|
if (resourceBundle == null)
|
||||||
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, Control.INSTANCE);
|
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale,
|
||||||
|
DefaultResourceBundleControl.INSTANCE);
|
||||||
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Locale> getCandidateLocales() {
|
||||||
|
if (candidateLocales == null)
|
||||||
|
candidateLocales = List.copyOf(LocaleUtils.getCandidateLocales(locale));
|
||||||
|
return candidateLocales;
|
||||||
|
}
|
||||||
|
|
||||||
public String i18n(String key, Object... formatArgs) {
|
public String i18n(String key, Object... formatArgs) {
|
||||||
try {
|
try {
|
||||||
return String.format(getResourceBundle().getString(key), formatArgs);
|
return String.format(getResourceBundle().getString(key), formatArgs);
|
||||||
@ -211,13 +189,23 @@ public final class Locales {
|
|||||||
|
|
||||||
public String formatDateTime(TemporalAccessor time) {
|
public String formatDateTime(TemporalAccessor time) {
|
||||||
DateTimeFormatter formatter = dateTimeFormatter;
|
DateTimeFormatter formatter = dateTimeFormatter;
|
||||||
if (formatter == null)
|
if (formatter == null) {
|
||||||
|
if (locale.getLanguage().equals("lzh"))
|
||||||
|
return WenyanUtils.formatDateTime(time);
|
||||||
|
|
||||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
|
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
|
||||||
.withZone(ZoneId.systemDefault());
|
.withZone(ZoneId.systemDefault());
|
||||||
|
}
|
||||||
return formatter.format(time);
|
return formatter.format(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDisplaySelfVersion(RemoteVersion version) {
|
public String getDisplaySelfVersion(RemoteVersion version) {
|
||||||
|
if (locale.getLanguage().equals("lzh")) {
|
||||||
|
if (version instanceof GameRemoteVersion)
|
||||||
|
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
|
||||||
|
else
|
||||||
|
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
||||||
|
}
|
||||||
return version.getSelfVersion();
|
return version.getSelfVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,23 +213,29 @@ public final class Locales {
|
|||||||
String language = locale.getLanguage();
|
String language = locale.getLanguage();
|
||||||
String region = locale.getCountry();
|
String region = locale.getCountry();
|
||||||
|
|
||||||
if (isEnglish(locale))
|
if (LocaleUtils.isEnglish(locale))
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
if (isChinese(locale)) {
|
if (LocaleUtils.isChinese(locale)) {
|
||||||
String lang;
|
String lang;
|
||||||
String charset;
|
String charset;
|
||||||
|
|
||||||
if (isSimplifiedChinese(locale)) {
|
String script = LocaleUtils.getScript(locale);
|
||||||
lang = region.equals("SG") || region.equals("MY")
|
switch (script) {
|
||||||
? "zh-" + region
|
case "Hans":
|
||||||
: "zh-CN";
|
lang = region.equals("SG") || region.equals("MY")
|
||||||
charset = "0x6e38,0x620f";
|
? "zh-" + region
|
||||||
} else {
|
: "zh-CN";
|
||||||
lang = region.equals("HK") || region.equals("MO")
|
charset = "0x6e38,0x620f";
|
||||||
? "zh-" + region
|
break;
|
||||||
: "zh-TW";
|
case "Hant":
|
||||||
charset = "0x904a,0x6232";
|
lang = region.equals("HK") || region.equals("MO")
|
||||||
|
? "zh-" + region
|
||||||
|
: "zh-TW";
|
||||||
|
charset = "0x904a,0x6232";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return ":lang=" + lang + ":charset=" + charset;
|
return ":lang=" + lang + ":charset=" + charset;
|
||||||
@ -250,9 +244,34 @@ public final class Locales {
|
|||||||
return region.isEmpty() ? language : language + "-" + region;
|
return region.isEmpty() ? language : language + "-" + region;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the builtin localized resource with given name and suffix.
|
||||||
|
///
|
||||||
|
/// For example, if the current locale is `zh-CN`, when calling `findBuiltinResource("assets.lang.foo", "json")`,
|
||||||
|
/// this method will look for the following built-in resources in order:
|
||||||
|
///
|
||||||
|
/// - `assets/lang/foo_zh_Hans_CN.json`
|
||||||
|
/// - `assets/lang/foo_zh_Hans.json`
|
||||||
|
/// - `assets/lang/foo_zh_CN.json`
|
||||||
|
/// - `assets/lang/foo_zh.json`
|
||||||
|
/// - `assets/lang/foo.json`
|
||||||
|
///
|
||||||
|
/// This method will open and return the first found resource;
|
||||||
|
/// if none of the above resources exist, it returns `null`.
|
||||||
|
public @Nullable InputStream findBuiltinResource(String name, String suffix) {
|
||||||
|
var control = DefaultResourceBundleControl.INSTANCE;
|
||||||
|
var classLoader = Locales.class.getClassLoader();
|
||||||
|
for (Locale locale : getCandidateLocales()) {
|
||||||
|
String resourceName = control.toResourceName(control.toBundleName(name, locale), suffix);
|
||||||
|
InputStream input = classLoader.getResourceAsStream(resourceName);
|
||||||
|
if (input != null)
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSameLanguage(SupportedLocale other) {
|
public boolean isSameLanguage(SupportedLocale other) {
|
||||||
return this.getLocale().getLanguage().equals(other.getLocale().getLanguage())
|
return (this.getLocale().getLanguage().equals(other.getLocale().getLanguage()))
|
||||||
|| isChinese(this.getLocale()) && isChinese(other.getLocale());
|
|| (LocaleUtils.isChinese(this.getLocale()) && LocaleUtils.isChinese(other.getLocale()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
||||||
@ -268,50 +287,4 @@ public final class Locales {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Control extends ResourceBundle.Control {
|
|
||||||
public static final Control INSTANCE = new Control();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
|
||||||
List<Locale> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -50,13 +50,15 @@ public final class MinecraftWiki {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String variantSuffix;
|
String variantSuffix;
|
||||||
if (Locales.isChinese(locale.getLocale())) {
|
if (LocaleUtils.isChinese(locale.getLocale())) {
|
||||||
if (Locales.isSimplifiedChinese(locale.getLocale()))
|
if (!"Hant".equals(LocaleUtils.getScript(locale.getLocale()))) {
|
||||||
variantSuffix = "?variant=zh-cn";
|
variantSuffix = "?variant=zh-cn";
|
||||||
else if (locale.getLocale().getCountry().equals("HK") || locale.getLocale().getCountry().equals("MO"))
|
} else {
|
||||||
variantSuffix = "?variant=zh-hk";
|
String region = locale.getLocale().getCountry();
|
||||||
else
|
variantSuffix = region.equals("HK") || region.equals("MO")
|
||||||
variantSuffix = "?variant=zh-tw";
|
? "?variant=zh-hk"
|
||||||
|
: "?variant=zh-tw";
|
||||||
|
}
|
||||||
} else
|
} else
|
||||||
variantSuffix = "";
|
variantSuffix = "";
|
||||||
|
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 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<String> 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")));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
/// Overrides the default behavior of [ResourceBundle.Control], optimizing the candidate list generation logic.
|
||||||
|
///
|
||||||
|
/// Compared to the default implementation, [DefaultResourceBundleControl] optimizes the following scenarios:
|
||||||
|
///
|
||||||
|
/// - If no language is specified (such as [Locale#ROOT]), `en` is used instead.
|
||||||
|
/// - For all Chinese locales, if no script is specified, the script (`Hans`/`Hant`/`Latn`) is always inferred based on region and variant.
|
||||||
|
/// - For all Chinese locales, `zh-CN` is always added to the candidate list. If `zh-Hans` already exists in the candidate list,
|
||||||
|
/// `zh-CN` is inserted before `zh`; otherwise, it is inserted after `zh`.
|
||||||
|
/// - For all Traditional Chinese locales, `zh-TW` is always added to the candidate list (before `zh`).
|
||||||
|
/// - For all Chinese variants (such as `lzh`, `cmn`, `yue`, etc.), a candidate list with the language code replaced by `zh`
|
||||||
|
/// is added to the end of the candidate list.
|
||||||
|
///
|
||||||
|
/// @author Glavo
|
||||||
|
public class DefaultResourceBundleControl extends ResourceBundle.Control {
|
||||||
|
|
||||||
|
public static final DefaultResourceBundleControl INSTANCE = new DefaultResourceBundleControl();
|
||||||
|
|
||||||
|
public DefaultResourceBundleControl() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Locale> ensureEditable(List<Locale> list) {
|
||||||
|
return list instanceof ArrayList<?>
|
||||||
|
? list
|
||||||
|
: new ArrayList<>(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||||
|
if (locale.getLanguage().isEmpty())
|
||||||
|
return getCandidateLocales(baseName, Locale.ENGLISH);
|
||||||
|
|
||||||
|
if (LocaleUtils.isChinese(locale)) {
|
||||||
|
String language = locale.getLanguage();
|
||||||
|
String script = locale.getScript();
|
||||||
|
|
||||||
|
if (script.isEmpty()) {
|
||||||
|
script = LocaleUtils.getScript(locale);
|
||||||
|
locale = new Locale.Builder()
|
||||||
|
.setLocale(locale)
|
||||||
|
.setScript(script)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Locale> locales = super.getCandidateLocales("", locale);
|
||||||
|
|
||||||
|
if (language.equals("zh")) {
|
||||||
|
if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) {
|
||||||
|
locales = ensureEditable(locales);
|
||||||
|
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||||
|
if (chineseIdx >= 0)
|
||||||
|
locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
|
||||||
|
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||||
|
|
||||||
|
if (chineseIdx >= 0) {
|
||||||
|
locales = ensureEditable(locales);
|
||||||
|
if (locales.contains(LocaleUtils.LOCALE_ZH_HANS))
|
||||||
|
locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
|
||||||
|
else
|
||||||
|
locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
locales = ensureEditable(locales);
|
||||||
|
locales.removeIf(it -> !it.getLanguage().equals(language));
|
||||||
|
|
||||||
|
locales.addAll(getCandidateLocales("", new Locale.Builder()
|
||||||
|
.setLocale(locale)
|
||||||
|
.setLanguage("zh")
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return locales;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getCandidateLocales(baseName, locale);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Glavo
|
||||||
|
*/
|
||||||
|
public class LocaleUtils {
|
||||||
|
|
||||||
|
public static final Locale SYSTEM_DEFAULT = Locale.getDefault();
|
||||||
|
|
||||||
|
public static final Locale LOCALE_ZH_HANS = Locale.forLanguageTag("zh-Hans");
|
||||||
|
public static final Locale LOCALE_ZH_HANT = Locale.forLanguageTag("zh-Hant");
|
||||||
|
|
||||||
|
public static String toLanguageKey(Locale locale) {
|
||||||
|
if (locale.getLanguage().isEmpty())
|
||||||
|
return "default";
|
||||||
|
else
|
||||||
|
return locale.toLanguageTag();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
|
||||||
|
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getScript(Locale locale) {
|
||||||
|
if (locale.getScript().isEmpty()) {
|
||||||
|
if (isChinese(locale)) {
|
||||||
|
if (CHINESE_LATN_VARIANTS.contains(locale.getVariant()))
|
||||||
|
return "Latn";
|
||||||
|
if (locale.getLanguage().equals("lzh") || CHINESE_TRADITIONAL_REGIONS.contains(locale.getCountry()))
|
||||||
|
return "Hant";
|
||||||
|
else
|
||||||
|
return "Hans";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return locale.getScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
public static boolean isEnglish(Locale locale) {
|
||||||
|
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Set<String> CHINESE_TRADITIONAL_REGIONS = Set.of("TW", "HK", "MO");
|
||||||
|
public static final Set<String> CHINESE_LATN_VARIANTS = Set.of("pinyin", "wadegile", "tongyong");
|
||||||
|
public static final Set<String> CHINESE_LANGUAGES = Set.of(
|
||||||
|
"zh",
|
||||||
|
"zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
|
||||||
|
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static boolean isChinese(Locale locale) {
|
||||||
|
return CHINESE_LANGUAGES.contains(locale.getLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocaleUtils() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.gson.TypeAdapter;
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.google.gson.stream.JsonToken;
|
||||||
|
import com.google.gson.stream.JsonWriter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@JsonAdapter(LocalizedText.Adapter.class)
|
||||||
|
public final class LocalizedText {
|
||||||
|
private final @Nullable String value;
|
||||||
|
private final @Nullable Map<String, String> localizedValues;
|
||||||
|
|
||||||
|
public LocalizedText(String value) {
|
||||||
|
this.value = value;
|
||||||
|
this.localizedValues = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalizedText(@NotNull Map<String, String> localizedValues) {
|
||||||
|
this.value = null;
|
||||||
|
this.localizedValues = Objects.requireNonNull(localizedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText(@NotNull List<Locale> candidates) {
|
||||||
|
if (localizedValues != null) {
|
||||||
|
for (Locale locale : candidates) {
|
||||||
|
String value = localizedValues.get(LocaleUtils.toLanguageKey(locale));
|
||||||
|
if (value != null)
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Adapter extends TypeAdapter<LocalizedText> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalizedText read(JsonReader jsonReader) throws IOException {
|
||||||
|
JsonToken nextToken = jsonReader.peek();
|
||||||
|
if (nextToken == JsonToken.NULL) {
|
||||||
|
return null;
|
||||||
|
} else if (nextToken == JsonToken.STRING) {
|
||||||
|
return new LocalizedText(jsonReader.nextString());
|
||||||
|
} else if (nextToken == JsonToken.BEGIN_OBJECT) {
|
||||||
|
LinkedHashMap<String, String> localizedValues = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
jsonReader.beginObject();
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
String name = jsonReader.nextName();
|
||||||
|
String value = jsonReader.nextString();
|
||||||
|
|
||||||
|
localizedValues.put(name, value);
|
||||||
|
}
|
||||||
|
jsonReader.endObject();
|
||||||
|
|
||||||
|
return new LocalizedText(localizedValues);
|
||||||
|
} else {
|
||||||
|
throw new JsonSyntaxException("Unexpected token " + nextToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(JsonWriter jsonWriter, LocalizedText localizedText) throws IOException {
|
||||||
|
if (localizedText == null) {
|
||||||
|
jsonWriter.nullValue();
|
||||||
|
} else if (localizedText.localizedValues != null) {
|
||||||
|
|
||||||
|
jsonWriter.beginObject();
|
||||||
|
for (var entry : localizedText.localizedValues.entrySet()) {
|
||||||
|
jsonWriter.name(entry.getKey());
|
||||||
|
jsonWriter.value(entry.getValue());
|
||||||
|
}
|
||||||
|
jsonWriter.endObject();
|
||||||
|
} else {
|
||||||
|
jsonWriter.value(localizedText.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Glavo
|
||||||
|
*/
|
||||||
|
public final class LocaleUtilsTest {
|
||||||
|
private static void assertCandidateLocales(String languageTag, List<String> candidateLocales) {
|
||||||
|
assertEquals(candidateLocales,
|
||||||
|
LocaleUtils.getCandidateLocales(Locale.forLanguageTag(languageTag))
|
||||||
|
.stream()
|
||||||
|
.map(Locale::toLanguageTag)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetCandidateLocales() {
|
||||||
|
assertCandidateLocales("zh", List.of("zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-CN", List.of("zh-Hans-CN", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-SG", List.of("zh-Hans-SG", "zh-Hans", "zh-SG", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-MY", List.of("zh-Hans-MY", "zh-Hans", "zh-MY", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-US", List.of("zh-Hans-US", "zh-Hans", "zh-US", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-TW", List.of("zh-Hant-TW", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("zh-HK", List.of("zh-Hant-HK", "zh-Hant", "zh-HK", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("zh-MO", List.of("zh-Hant-MO", "zh-Hant", "zh-MO", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("zh-Hans", List.of("zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-Hant", List.of("zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("zh-Hans-US", List.of("zh-Hans-US", "zh-Hans", "zh-US", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-Hant-CN", List.of("zh-Hant-CN", "zh-Hant", "zh-CN", "zh-TW", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-Hans-TW", List.of("zh-Hans-TW", "zh-Hans", "zh-TW", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-Latn", List.of("zh-Latn", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("zh-Latn-CN", List.of("zh-Latn-CN", "zh-Latn", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("zh-pinyin", List.of("zh-Latn-pinyin", "zh-Latn", "zh-pinyin", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("lzh", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("lzh-Hant", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und"));
|
||||||
|
assertCandidateLocales("lzh-Hans", List.of("lzh-Hans", "lzh", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("cmn", List.of("cmn-Hans", "cmn", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("cmn-Hans", List.of("cmn-Hans", "cmn", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
assertCandidateLocales("yue", List.of("yue-Hans", "yue", "zh-Hans", "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(LocaleUtils.isChinese(Locale.CHINESE));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.SIMPLIFIED_CHINESE));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.TRADITIONAL_CHINESE));
|
||||||
|
assertTrue(LocaleUtils.isChinese(LocaleUtils.LOCALE_ZH_HANS));
|
||||||
|
assertTrue(LocaleUtils.isChinese(LocaleUtils.LOCALE_ZH_HANT));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.forLanguageTag("lzh")));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.forLanguageTag("cmn")));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.forLanguageTag("cmn-Hans")));
|
||||||
|
assertTrue(LocaleUtils.isChinese(Locale.forLanguageTag("yue")));
|
||||||
|
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.ROOT));
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.ENGLISH));
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.JAPANESE));
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.forLanguageTag("es")));
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.forLanguageTag("ru")));
|
||||||
|
assertFalse(LocaleUtils.isChinese(Locale.forLanguageTag("uk")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetScript() {
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.CHINESE));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("zh")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("zh-Hans")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("zh-Hans-US")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("zh-SG")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("zh-MY")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("cmn")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("cmn-Hans")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("cmn-CN")));
|
||||||
|
assertEquals("Hans", LocaleUtils.getScript(Locale.forLanguageTag("lzh-Hans")));
|
||||||
|
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("zh-Hant")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("zh-TW")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("zh-HK")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("zh-MO")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("cmn-Hant")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("lzh")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("lzh-Hant")));
|
||||||
|
assertEquals("Hant", LocaleUtils.getScript(Locale.forLanguageTag("lzh-CN")));
|
||||||
|
|
||||||
|
assertEquals("Latn", LocaleUtils.getScript(Locale.forLanguageTag("zh-pinyin")));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user