diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 4e06560a1..b6a908185 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -38,7 +38,7 @@ import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.i18n.Locales; -import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; +import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jackhuang.hmcl.util.javafx.DirtyTracker; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jetbrains.annotations.Nullable; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java index b6d31e4f9..eaf3e1900 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java @@ -39,7 +39,7 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist; import org.jackhuang.hmcl.ui.construct.MultiFileItem; import org.jackhuang.hmcl.ui.construct.OptionToggleButton; import org.jackhuang.hmcl.util.i18n.I18n; -import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; +import org.jackhuang.hmcl.util.i18n.SupportedLocale; import java.util.Arrays; 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 892ad574c..3c705fde3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -19,7 +19,6 @@ package org.jackhuang.hmcl.util.i18n; import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; -import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; 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 a963349b7..441d44249 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 @@ -17,18 +17,9 @@ */ package org.jackhuang.hmcl.util.i18n; -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.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; -import java.io.IOException; import java.io.InputStream; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -69,199 +60,4 @@ public final class Locales { return getLocale(Locale.forLanguageTag(name.trim())); } - @JsonAdapter(SupportedLocale.TypeAdapter.class) - public static final class SupportedLocale { - private final boolean isDefault; - private final String name; - private final Locale locale; - private ResourceBundle resourceBundle; - private ResourceBundle localeNamesBundle; - private DateTimeFormatter dateTimeFormatter; - private List candidateLocales; - - SupportedLocale() { - this.isDefault = true; - this.name = "def"; // TODO: Change to "default" after updating the Config format - - String language = System.getenv("HMCL_LANGUAGE"); - this.locale = StringUtils.isBlank(language) - ? LocaleUtils.SYSTEM_DEFAULT - : Locale.forLanguageTag(language); - } - - SupportedLocale(Locale locale) { - this.isDefault = false; - this.name = locale.toLanguageTag(); - this.locale = locale; - } - - public boolean isDefault() { - return isDefault; - } - - public String getName() { - return name; - } - - public Locale getLocale() { - return locale; - } - - public String getDisplayName(SupportedLocale inLocale) { - if (isDefault()) { - try { - return inLocale.getResourceBundle().getString("lang.default"); - } catch (Throwable e) { - LOG.warning("Failed to get localized name for default locale", e); - return "Default"; - } - } - - Locale inJavaLocale = inLocale.getLocale(); - if (inJavaLocale.getLanguage().length() > 2) { - String iso1 = LocaleUtils.getISO1Language(inJavaLocale); - if (iso1.length() <= 2) { - Locale.Builder builder = new Locale.Builder() - .setLocale(inJavaLocale) - .setLanguage(iso1); - - if (inJavaLocale.getScript().isEmpty()) - builder.setScript(LocaleUtils.getScript(inJavaLocale)); - - inJavaLocale = builder.build(); - } - } - - if (this.locale.getLanguage().equals("lzh")) { - if (inJavaLocale.getLanguage().equals("zh")) - return "文言"; - - String name = locale.getDisplayName(inJavaLocale); - return name.equals("lzh") || name.equals("Literary Chinese") - ? "Chinese (Classical)" - : name; - } - - if (this.locale.getLanguage().equals("en") && this.locale.getScript().equals("Qabs")) { - return "ɥsᴉꞁᵷuƎ (uʍoᗡ ǝpᴉsd∩)"; - } - - return locale.getDisplayName(inJavaLocale); - } - - public ResourceBundle getResourceBundle() { - ResourceBundle bundle = resourceBundle; - if (resourceBundle == null) - resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, - DefaultResourceBundleControl.INSTANCE); - - return bundle; - } - - public List getCandidateLocales() { - if (candidateLocales == null) - candidateLocales = List.copyOf(LocaleUtils.getCandidateLocales(locale)); - return candidateLocales; - } - - public String i18n(String key, Object... formatArgs) { - try { - return String.format(getResourceBundle().getString(key), formatArgs); - } catch (MissingResourceException e) { - LOG.error("Cannot find key " + key + " in resource bundle", e); - } catch (IllegalFormatException e) { - LOG.error("Illegal format string, key=" + key + ", args=" + Arrays.toString(formatArgs), e); - } - - return key + Arrays.toString(formatArgs); - } - - public String i18n(String key) { - try { - return getResourceBundle().getString(key); - } catch (MissingResourceException e) { - LOG.error("Cannot find key " + key + " in resource bundle", e); - return key; - } - } - - 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(); - - if (LocaleUtils.isEnglish(locale)) - return ""; - - if (LocaleUtils.isChinese(locale)) { - String lang; - String charset; - - String script = LocaleUtils.getScript(locale); - switch (script) { - case "Hans": - lang = region.equals("SG") || region.equals("MY") - ? "zh-" + region - : "zh-CN"; - charset = "0x6e38,0x620f"; - break; - case "Hant": - lang = region.equals("HK") || region.equals("MO") - ? "zh-" + region - : "zh-TW"; - charset = "0x904a,0x6232"; - break; - default: - return ""; - } - - return ":lang=" + lang + ":charset=" + charset; - } - - return region.isEmpty() ? language : language + "-" + region; - } - - public boolean isSameLanguage(SupportedLocale other) { - return LocaleUtils.getISO1Language(this.getLocale()) - .equals(LocaleUtils.getISO1Language(other.getLocale())); - } - - public static final class TypeAdapter extends com.google.gson.TypeAdapter { - @Override - public void write(JsonWriter out, SupportedLocale value) throws IOException { - out.value(value.getName()); - } - - @Override - public SupportedLocale read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) - return DEFAULT; - - String language = in.nextString(); - return getLocaleByName(switch (language) { - // TODO: Remove these compatibility codes after updating the Config format - case "zh_CN" -> "zh-Hans"; // For compatibility - case "zh" -> "zh-Hant"; // For compatibility - default -> language; - }); - } - } - } - } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java index 6f027b480..60a7dd04b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/MinecraftWiki.java @@ -27,7 +27,7 @@ public final class MinecraftWiki { private static final Pattern SNAPSHOT_PATTERN = Pattern.compile("^[0-9]{2}w[0-9]{2}.+$"); - public static String getWikiLink(Locales.SupportedLocale locale, GameRemoteVersion version) { + public static String getWikiLink(SupportedLocale locale, GameRemoteVersion version) { String wikiVersion = version.getSelfVersion(); var gameVersion = GameVersionNumber.asGameVersion(wikiVersion); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java new file mode 100644 index 000000000..83c1fa0fe --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/SupportedLocale.java @@ -0,0 +1,238 @@ +/* + * 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 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.jackhuang.hmcl.util.StringUtils; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +@JsonAdapter(SupportedLocale.TypeAdapter.class) +public final class SupportedLocale { + private final boolean isDefault; + private final String name; + private final Locale locale; + private ResourceBundle resourceBundle; + private ResourceBundle localeNamesBundle; + private DateTimeFormatter dateTimeFormatter; + private List candidateLocales; + + SupportedLocale() { + this.isDefault = true; + this.name = "def"; // TODO: Change to "default" after updating the Config format + + String language = System.getenv("HMCL_LANGUAGE"); + this.locale = StringUtils.isBlank(language) + ? LocaleUtils.SYSTEM_DEFAULT + : Locale.forLanguageTag(language); + } + + SupportedLocale(Locale locale) { + this.isDefault = false; + this.name = locale.toLanguageTag(); + this.locale = locale; + } + + public boolean isDefault() { + return isDefault; + } + + public String getName() { + return name; + } + + public Locale getLocale() { + return locale; + } + + public String getDisplayName(SupportedLocale inLocale) { + if (isDefault()) { + try { + return inLocale.getResourceBundle().getString("lang.default"); + } catch (Throwable e) { + LOG.warning("Failed to get localized name for default locale", e); + return "Default"; + } + } + + Locale currentLocale = this.getLocale(); + + String language = currentLocale.getLanguage(); + String script = currentLocale.getScript(); + + // Currently, HMCL does not support any locales with regions or variants, so they are not handled for now + // String region = currentLocale.getCountry(); + // String variant = currentLocale.getDisplayVariant(); + + ResourceBundle localeNames = inLocale.getLocaleNamesBundle(); + + String languageDisplayName = language; + try { + languageDisplayName = localeNames.getString(language); + } catch (Throwable e) { + LOG.warning("Failed to get localized name for language " + language, e); + } + + if (script.isEmpty()) { + return languageDisplayName; + } + + String scriptDisplayName = script; + + try { + scriptDisplayName = localeNames.getString(script); + } catch (Throwable e) { + LOG.warning("Failed to get localized name for script " + script, e); + } + + return languageDisplayName + " (" + scriptDisplayName + ")"; + } + + public ResourceBundle getResourceBundle() { + ResourceBundle bundle = resourceBundle; + if (resourceBundle == null) + resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, + DefaultResourceBundleControl.INSTANCE); + + return bundle; + } + + public ResourceBundle getLocaleNamesBundle() { + ResourceBundle bundle = localeNamesBundle; + if (localeNamesBundle == null) + localeNamesBundle = bundle = ResourceBundle.getBundle("assets.lang.LocaleNames", locale, + DefaultResourceBundleControl.INSTANCE); + + return bundle; + } + + public List getCandidateLocales() { + if (candidateLocales == null) + candidateLocales = List.copyOf(LocaleUtils.getCandidateLocales(locale)); + return candidateLocales; + } + + public String i18n(String key, Object... formatArgs) { + try { + return String.format(getResourceBundle().getString(key), formatArgs); + } catch (MissingResourceException e) { + LOG.error("Cannot find key " + key + " in resource bundle", e); + } catch (IllegalFormatException e) { + LOG.error("Illegal format string, key=" + key + ", args=" + Arrays.toString(formatArgs), e); + } + + return key + Arrays.toString(formatArgs); + } + + public String i18n(String key) { + try { + return getResourceBundle().getString(key); + } catch (MissingResourceException e) { + LOG.error("Cannot find key " + key + " in resource bundle", e); + return key; + } + } + + 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(); + + if (LocaleUtils.isEnglish(locale)) + return ""; + + if (LocaleUtils.isChinese(locale)) { + String lang; + String charset; + + String script = LocaleUtils.getScript(locale); + switch (script) { + case "Hans": + lang = region.equals("SG") || region.equals("MY") + ? "zh-" + region + : "zh-CN"; + charset = "0x6e38,0x620f"; + break; + case "Hant": + lang = region.equals("HK") || region.equals("MO") + ? "zh-" + region + : "zh-TW"; + charset = "0x904a,0x6232"; + break; + default: + return ""; + } + + return ":lang=" + lang + ":charset=" + charset; + } + + return region.isEmpty() ? language : language + "-" + region; + } + + public boolean isSameLanguage(SupportedLocale other) { + return LocaleUtils.getISO1Language(this.getLocale()) + .equals(LocaleUtils.getISO1Language(other.getLocale())); + } + + public static final class TypeAdapter extends com.google.gson.TypeAdapter { + @Override + public void write(JsonWriter out, SupportedLocale value) throws IOException { + out.value(value.getName()); + } + + @Override + public SupportedLocale read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) + return Locales.DEFAULT; + + String language = in.nextString(); + return Locales.getLocaleByName(switch (language) { + // TODO: Remove these compatibility codes after updating the Config format + case "zh_CN" -> "zh-Hans"; // For compatibility + case "zh" -> "zh-Hant"; // For compatibility + default -> language; + }); + } + } +}