mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-24 03:33:46 -04:00
Merge 5bb062bd86cbc0bdedd1c7d11b693fafba83b02f into bd9ae189f83e33a6977bbe056774c851e96fe0a7
This commit is contained in:
commit
c285b968e3
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
*.hprof
|
||||
|
||||
.gradle
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import org.jackhuang.hmcl.gradle.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNames
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
@ -206,11 +209,20 @@ tasks.shadowJar {
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(createPropertiesFile)
|
||||
dependsOn(upsideDownTranslate)
|
||||
dependsOn(createLocaleNames)
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
into("assets/") {
|
||||
from(hmclPropertiesFile)
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
into("assets/lang") {
|
||||
from(createLanguageList.map { it.outputFile })
|
||||
from(upsideDownTranslate.map { it.outputFile })
|
||||
from(createLocaleNames.map { it.outputDirectory })
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables by tasks.registering {
|
||||
@ -344,6 +356,29 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||
}
|
||||
|
||||
// l10n
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate by tasks.registering(UpsideDownTranslate::class) {
|
||||
inputFile.set(layout.projectDirectory.file("src/main/resources/assets/lang/I18N.properties"))
|
||||
outputFile.set(generatedDir.map { it.file("generated/i18n/I18N_en_Qabs.properties") })
|
||||
}
|
||||
|
||||
val createLanguageList by tasks.registering(CreateLanguageList::class) {
|
||||
resourceBundleDir.set(layout.projectDirectory.dir("src/main/resources/assets/lang"))
|
||||
resourceBundleBaseName.set("I18N")
|
||||
additionalLanguages.set(listOf("en-Qabs"))
|
||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||
}
|
||||
|
||||
val createLocaleNames by tasks.registering(CreateLocaleNames::class) {
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||
}
|
||||
|
||||
// mcmod data
|
||||
|
||||
tasks.register<ParseModDataTask>("parseModData") {
|
||||
|
@ -80,8 +80,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
Locale locale = Locale.getDefault();
|
||||
if (LocaleUtils.isEnglish(locale))
|
||||
return;
|
||||
|
||||
/*
|
||||
* 1.0 : No language option, do not set for these versions
|
||||
@ -129,6 +127,13 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
yield "zh_CN";
|
||||
}
|
||||
case "en" -> {
|
||||
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
|
||||
yield "en_UD";
|
||||
}
|
||||
|
||||
yield "";
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
@ -37,8 +37,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
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;
|
||||
@ -222,7 +221,7 @@ public final class Config implements Observable {
|
||||
}
|
||||
|
||||
@SerializedName("localization")
|
||||
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
||||
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(SupportedLocale.DEFAULT);
|
||||
|
||||
public ObjectProperty<SupportedLocale> localizationProperty() {
|
||||
return localization;
|
||||
|
@ -33,7 +33,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
@ -65,7 +65,7 @@ public final class SettingsPage extends SettingsView {
|
||||
FXUtils.smoothScrolling(scroll);
|
||||
|
||||
// ==== Languages ====
|
||||
cboLanguage.getItems().setAll(Locales.LOCALES);
|
||||
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
|
||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||
|
||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -34,7 +33,7 @@ public final class I18n {
|
||||
private I18n() {
|
||||
}
|
||||
|
||||
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
||||
private static volatile SupportedLocale locale = SupportedLocale.DEFAULT;
|
||||
|
||||
public static void setLocale(SupportedLocale locale) {
|
||||
I18n.locale = locale;
|
||||
@ -71,6 +70,11 @@ public final class I18n {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,273 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.annotations.JsonAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
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;
|
||||
|
||||
public final class Locales {
|
||||
private Locales() {
|
||||
}
|
||||
|
||||
|
||||
public static final SupportedLocale DEFAULT;
|
||||
|
||||
static {
|
||||
String language = System.getenv("HMCL_LANGUAGE");
|
||||
DEFAULT = new SupportedLocale(true, "def",
|
||||
StringUtils.isBlank(language) ? LocaleUtils.SYSTEM_DEFAULT : Locale.forLanguageTag(language));
|
||||
}
|
||||
|
||||
/**
|
||||
* English
|
||||
*/
|
||||
public static final SupportedLocale EN = new SupportedLocale("en");
|
||||
|
||||
/**
|
||||
* Spanish
|
||||
*/
|
||||
public static final SupportedLocale ES = new SupportedLocale("es");
|
||||
|
||||
/**
|
||||
* Russian
|
||||
*/
|
||||
public static final SupportedLocale RU = new SupportedLocale("ru");
|
||||
|
||||
/**
|
||||
* Ukrainian
|
||||
*/
|
||||
public static final SupportedLocale UK = new SupportedLocale("uk");
|
||||
|
||||
/**
|
||||
* Japanese
|
||||
*/
|
||||
public static final SupportedLocale JA = new SupportedLocale("ja");
|
||||
|
||||
/**
|
||||
* Chinese (Simplified)
|
||||
*/
|
||||
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", LocaleUtils.LOCALE_ZH_HANS);
|
||||
|
||||
/**
|
||||
* Chinese (Traditional)
|
||||
*/
|
||||
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", LocaleUtils.LOCALE_ZH_HANT);
|
||||
|
||||
/**
|
||||
* Wenyan (Classical Chinese)
|
||||
*/
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh");
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
|
||||
|
||||
public static SupportedLocale getLocaleByName(String name) {
|
||||
if (name == null) return DEFAULT;
|
||||
|
||||
for (SupportedLocale locale : LOCALES) {
|
||||
if (locale.getName().equalsIgnoreCase(name))
|
||||
return locale;
|
||||
}
|
||||
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
@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 DateTimeFormatter dateTimeFormatter;
|
||||
private List<Locale> candidateLocales;
|
||||
|
||||
SupportedLocale(boolean isDefault, String name, Locale locale) {
|
||||
this.isDefault = isDefault;
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
SupportedLocale(String name) {
|
||||
this(false, name, Locale.forLanguageTag(name));
|
||||
}
|
||||
|
||||
SupportedLocale(String name, Locale locale) {
|
||||
this(false, name, 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;
|
||||
}
|
||||
|
||||
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<Locale> 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 (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<SupportedLocale> {
|
||||
@Override
|
||||
public void write(JsonWriter out, SupportedLocale value) throws IOException {
|
||||
out.value(value.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupportedLocale read(JsonReader in) throws IOException {
|
||||
return getLocaleByName(in.nextString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
@ -41,7 +41,7 @@ public final class MinecraftWiki {
|
||||
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
|
||||
|
||||
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
|
||||
return getWikiLink(Locales.ZH_HANT, version);
|
||||
return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version);
|
||||
} else if (SNAPSHOT_PATTERN.matcher(wikiVersion).matches()) {
|
||||
return locale.i18n("wiki.version.game.snapshot", translatedVersion);
|
||||
} else {
|
||||
|
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||
public final class SupportedLocale {
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale();
|
||||
|
||||
private static final ConcurrentMap<Locale, SupportedLocale> LOCALES = new ConcurrentHashMap<>();
|
||||
|
||||
public static List<SupportedLocale> getSupportedLocales() {
|
||||
List<SupportedLocale> list = new ArrayList<>();
|
||||
list.add(DEFAULT);
|
||||
|
||||
InputStream locales = SupportedLocale.class.getResourceAsStream("/assets/lang/languages.json");
|
||||
if (locales != null) {
|
||||
try (locales) {
|
||||
list.addAll(JsonUtils.fromNonNullJsonFully(locales, JsonUtils.listTypeOf(SupportedLocale.class)));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load languages.json", e);
|
||||
}
|
||||
}
|
||||
return List.copyOf(list);
|
||||
}
|
||||
|
||||
public static SupportedLocale getLocale(Locale locale) {
|
||||
return LOCALES.computeIfAbsent(locale, SupportedLocale::new);
|
||||
}
|
||||
|
||||
public static SupportedLocale getLocaleByName(String name) {
|
||||
if (name == null || name.isEmpty() || "def".equals(name) || "default".equals(name))
|
||||
return DEFAULT;
|
||||
|
||||
return getLocale(Locale.forLanguageTag(name.trim()));
|
||||
}
|
||||
|
||||
private final boolean isDefault;
|
||||
private final String name;
|
||||
private final Locale locale;
|
||||
private ResourceBundle resourceBundle;
|
||||
private ResourceBundle localeNamesBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
private List<Locale> 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<Locale> 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<SupportedLocale> {
|
||||
@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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/// @author Glavo
|
||||
public final class UpsideDownUtils {
|
||||
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
|
||||
|
||||
private static void putChars(char baseChar, String upsideDownChars) {
|
||||
for (int i = 0; i < upsideDownChars.length(); i++) {
|
||||
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void putChars(String baseChars, String upsideDownChars) {
|
||||
if (baseChars.length() != upsideDownChars.length()) {
|
||||
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
|
||||
}
|
||||
|
||||
for (int i = 0; i < baseChars.length(); i++) {
|
||||
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
|
||||
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
|
||||
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
|
||||
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
|
||||
}
|
||||
|
||||
public static String translate(String str) {
|
||||
StringBuilder builder = new StringBuilder(str.length());
|
||||
str.codePoints().forEach(ch -> builder.appendCodePoint(MAPPER.getOrDefault(ch, ch)));
|
||||
return builder.reverse().toString();
|
||||
}
|
||||
|
||||
private static DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
return translate(BASE_FORMATTER.format(time));
|
||||
}
|
||||
|
||||
private UpsideDownUtils() {
|
||||
}
|
||||
}
|
@ -692,7 +692,7 @@ input.url=The input must be a valid URL.
|
||||
|
||||
install=New Instance
|
||||
install.change_version=Change Version
|
||||
install.change_version.confirm=Are you sure you want to switch %s from version %s to %s?
|
||||
install.change_version.confirm=Are you sure you want to switch %1$s from version %2$s to %3$s?
|
||||
install.change_version.process=Change Version Process
|
||||
install.failed=Failed to install
|
||||
install.failed.downloading=Failed to download some required files.
|
||||
@ -702,7 +702,7 @@ install.failed.install_online=Failed to identify the provided file. If you are i
|
||||
install.failed.malformed=The downloaded files are corrupted. You can try resolving this problem by switching to another download source in "Settings → Download → Download Source".
|
||||
install.failed.optifine_conflict=Cannot install both OptiFine and Fabric on Minecraft 1.13 or later.
|
||||
install.failed.optifine_forge_1.17=For Minecraft 1.17.1, Forge is only compatible with OptiFine H1 pre2 or later. You can install them by checking "Snapshots" when choosing an OptiFine version in HMCL.
|
||||
install.failed.version_mismatch=This loader requires the game version %s, but the installed one is %s.
|
||||
install.failed.version_mismatch=This loader requires the game version %1$s, but the installed one is %2$s.
|
||||
install.installer.change_version=%s Incompatible
|
||||
install.installer.choose=Choose Your %s Version
|
||||
install.installer.cleanroom=Cleanroom
|
||||
@ -787,7 +787,7 @@ launch.advice.forge37_0_60=Forge versions prior to 37.0.60 are not compatible wi
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or later. Please use Java 8 or later versions.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions prior to 1.8.0_51. Please install the latest Java 8 version.
|
||||
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later. Please use Java 8 instead.
|
||||
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %s to launch Minecraft %s.
|
||||
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %1$s to launch Minecraft %2$s.
|
||||
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge.
|
||||
launch.advice.newer_java=You are using an older Java version to launch the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash.
|
||||
launch.advice.not_enough_space=You have allocated a memory size larger than the actual %d MiB of memory installed on your computer. You may experience degraded performance or even be unable to launch the game.
|
||||
@ -906,7 +906,7 @@ modpack.installing=Installing modpack
|
||||
modpack.installing.given=Installing %s modpack
|
||||
modpack.introduction=Curse, Modrinth, MultiMC, and MCBBS modpacks are currently supported.
|
||||
modpack.invalid=Invalid modpack, you can try downloading it again.
|
||||
modpack.mismatched_type=Modpack type mismatched, the current instance is a(n) %s type, but the provided one is %s type.
|
||||
modpack.mismatched_type=Modpack type mismatched, the current instance is a(n) %1$s type, but the provided one is %2$s type.
|
||||
modpack.name=Modpack Name
|
||||
modpack.not_a_valid_name=Invalid modpack name.
|
||||
modpack.origin=Source
|
||||
@ -1236,7 +1236,7 @@ search.first_page=First
|
||||
search.previous_page=Previous
|
||||
search.next_page=Next
|
||||
search.last_page=Last
|
||||
search.page_n=%d / %s
|
||||
search.page_n=%1$d / %2$s
|
||||
|
||||
selector.choose=Choose
|
||||
selector.choose_file=Choose file
|
||||
@ -1333,7 +1333,7 @@ settings.game.java_directory.bit=%s bit
|
||||
settings.game.java_directory.choose=Choose Java
|
||||
settings.game.java_directory.invalid=Incorrect Java path
|
||||
settings.game.java_directory.version=Specify Java Version
|
||||
settings.game.java_directory.template=%s (%s)
|
||||
settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=Manage
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose the working directory
|
||||
@ -1479,7 +1479,7 @@ version.manage.manage=Edit Instance
|
||||
version.manage.manage.title=Edit Instance - %1s
|
||||
version.manage.redownload_assets_index=Update Game Assets
|
||||
version.manage.remove=Delete Instance
|
||||
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%s"? You can still find its files in your recycle bin by the name of "%s".
|
||||
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s".
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove_assets=Delete All Assets
|
||||
version.manage.remove_libraries=Delete All Libraries
|
||||
|
@ -18,11 +18,13 @@
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@ -44,6 +46,23 @@ public final class LocaleUtils {
|
||||
|
||||
public static final String DEFAULT_LANGUAGE_KEY = "default";
|
||||
|
||||
private static final Map<String, String> subLanguageToParent = new HashMap<>();
|
||||
|
||||
static {
|
||||
try (InputStream input = LocaleUtils.class.getResourceAsStream("/assets/lang/sublanguages.json")) {
|
||||
if (input != null) {
|
||||
JsonUtils.fromJsonFully(input, JsonUtils.mapTypeOf(String.class, JsonUtils.listTypeOf(String.class)))
|
||||
.forEach((parent, subList) -> {
|
||||
for (String subLanguage : subList) {
|
||||
subLanguageToParent.put(subLanguage, parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load sublanguages.json file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Locale getInstance(String language, String script, String region,
|
||||
String variant) {
|
||||
Locale.Builder builder = new Locale.Builder();
|
||||
@ -88,6 +107,12 @@ public final class LocaleUtils {
|
||||
/// the script will be inferred based on the language, the region and the variant.
|
||||
public static @NotNull String getScript(Locale locale) {
|
||||
if (locale.getScript().isEmpty()) {
|
||||
if (isEnglish(locale)) {
|
||||
if ("UD".equals(locale.getCountry())) {
|
||||
return "Qabs";
|
||||
}
|
||||
}
|
||||
|
||||
if (isChinese(locale)) {
|
||||
if (CHINESE_LATN_VARIANTS.contains(locale.getVariant()))
|
||||
return "Latn";
|
||||
@ -308,12 +333,9 @@ public final class LocaleUtils {
|
||||
}
|
||||
|
||||
public static @Nullable String getParentLanguage(String language) {
|
||||
return switch (language) {
|
||||
case "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
|
||||
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
|
||||
case "" -> null;
|
||||
default -> "";
|
||||
};
|
||||
return !language.isEmpty()
|
||||
? subLanguageToParent.getOrDefault(language, "")
|
||||
: null;
|
||||
}
|
||||
|
||||
public static boolean isEnglish(Locale locale) {
|
||||
|
17
HMCLCore/src/main/resources/assets/lang/sublanguages.json
Normal file
17
HMCLCore/src/main/resources/assets/lang/sublanguages.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"zh" : [
|
||||
"cmn",
|
||||
"lzh",
|
||||
"cdo",
|
||||
"cjy",
|
||||
"cpx",
|
||||
"czh",
|
||||
"gan",
|
||||
"hak",
|
||||
"hsn",
|
||||
"mnp",
|
||||
"nan",
|
||||
"wuu",
|
||||
"yue"
|
||||
]
|
||||
}
|
@ -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.gradle;
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.gradle.l10n;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputDirectory;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class CreateLanguageList extends DefaultTask {
|
||||
@InputDirectory
|
||||
public abstract DirectoryProperty getResourceBundleDir();
|
||||
|
||||
@Input
|
||||
public abstract Property<@NotNull String> getResourceBundleBaseName();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<@NotNull String> getAdditionalLanguages();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path inputDir = getResourceBundleDir().get().getAsFile().toPath();
|
||||
if (!Files.isDirectory(inputDir))
|
||||
throw new GradleException("Input directory not exists: " + inputDir);
|
||||
|
||||
|
||||
SortedSet<Locale> locales = new TreeSet<>(new LocaleComparator());
|
||||
locales.addAll(getAdditionalLanguages().getOrElse(List.of()).stream()
|
||||
.map(Locale::forLanguageTag)
|
||||
.toList());
|
||||
|
||||
String baseName = getResourceBundleBaseName().get();
|
||||
String suffix = ".properties";
|
||||
|
||||
try (var stream = Files.newDirectoryStream(inputDir, file -> {
|
||||
String fileName = file.getFileName().toString();
|
||||
return fileName.startsWith(baseName) && fileName.endsWith(suffix);
|
||||
})) {
|
||||
for (Path file : stream) {
|
||||
String fileName = file.getFileName().toString();
|
||||
if (fileName.length() == baseName.length() + suffix.length())
|
||||
locales.add(Locale.ENGLISH);
|
||||
else if (fileName.charAt(baseName.length()) == '_') {
|
||||
String localeName = fileName.substring(baseName.length() + 1, fileName.length() - suffix.length());
|
||||
|
||||
// TODO: Delete this if the I18N file naming is changed
|
||||
if (baseName.equals("I18N")) {
|
||||
if (localeName.equals("zh"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hant"));
|
||||
else if (localeName.equals("zh_CN"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hans"));
|
||||
else
|
||||
locales.add(Locale.forLanguageTag(localeName.replace('_', '-')));
|
||||
} else {
|
||||
if (localeName.equals("zh"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hans"));
|
||||
else
|
||||
locales.add(Locale.forLanguageTag(localeName.replace('_', '-')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
Files.writeString(outputFile, locales.stream().map(locale -> '"' + locale.toLanguageTag() + '"')
|
||||
.collect(Collectors.joining(", ", "[", "]")));
|
||||
}
|
||||
|
||||
private final class LocaleComparator implements Comparator<Locale> {
|
||||
Map<String, String> subLanguageToParent = new HashMap<>();
|
||||
|
||||
{
|
||||
Path file = getProject().getRootProject().getLayout().getProjectDirectory()
|
||||
.file("HMCLCore/src/main/resources/assets/lang/sublanguages.json").getAsFile().toPath();
|
||||
|
||||
try (var reader = Files.newBufferedReader(file)) {
|
||||
new Gson().fromJson(reader, new TypeToken<Map<String, List<String>>>() {
|
||||
}).forEach((parent, subList) -> {
|
||||
for (String subLanguage : subList) {
|
||||
subLanguageToParent.put(subLanguage, parent);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new GradleException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> resolveLanguage(String language) {
|
||||
List<String> langList = new ArrayList<>();
|
||||
|
||||
String lang = language;
|
||||
while (true) {
|
||||
langList.add(0, lang);
|
||||
|
||||
String parent = subLanguageToParent.get(lang);
|
||||
if (parent != null) {
|
||||
lang = parent;
|
||||
} else {
|
||||
return langList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int compareLanguage(String l1, String l2) {
|
||||
var list1 = resolveLanguage(l1);
|
||||
var list2 = resolveLanguage(l2);
|
||||
|
||||
int n = Math.min(list1.size(), list2.size());
|
||||
for (int i = 0; i < n; i++) {
|
||||
int c = list1.get(i).compareTo(list2.get(i));
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
return Integer.compare(list1.size(), list2.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Locale l1, Locale l2) {
|
||||
int c = compareLanguage(l1.getLanguage(), l2.getLanguage());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = l1.getScript().compareTo(l2.getScript());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = l1.getCountry().compareTo(l2.getCountry());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = l1.getVariant().compareTo(l2.getVariant());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
return l1.toString().compareTo(l2.toLanguageTag());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.gradle.l10n;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class CreateLocaleNames extends DefaultTask {
|
||||
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getLanguagesFile();
|
||||
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
private static String mapToFileName(String base, String ext, Locale locale) {
|
||||
if (locale.getLanguage().isEmpty() || locale.equals(Locale.ENGLISH))
|
||||
return base + "." + ext;
|
||||
else if (locale.toLanguageTag().equals("zh-Hans"))
|
||||
return base + "_zh." + ext;
|
||||
else
|
||||
return base + "_" + locale.toLanguageTag().replace('-', '_') + "." + ext;
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path languagesFile = getLanguagesFile().get().getAsFile().toPath();
|
||||
Path outputDir = getOutputDirectory().get().getAsFile().toPath();
|
||||
|
||||
if (Files.isDirectory(outputDir)) {
|
||||
Files.walkFileTree(outputDir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
|
||||
Files.deleteIfExists(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FileVisitResult postVisitDirectory(@NotNull Path dir, @Nullable IOException exc) throws IOException {
|
||||
if (!dir.equals(outputDir))
|
||||
Files.deleteIfExists(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
Files.deleteIfExists(outputDir);
|
||||
Files.createDirectories(outputDir);
|
||||
|
||||
List<Locale> supportedLanguages;
|
||||
try (var reader = Files.newBufferedReader(languagesFile)) {
|
||||
supportedLanguages = new Gson().fromJson(reader, new TypeToken<List<String>>() {
|
||||
}).stream()
|
||||
.map(Locale::forLanguageTag)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (!supportedLanguages.get(0).equals(Locale.ENGLISH))
|
||||
throw new GradleException("The first language must be english.");
|
||||
|
||||
// For Upside Down English
|
||||
UpsideDownTranslate.Translator upsideDownTranslator = new UpsideDownTranslate.Translator();
|
||||
Map<String, String> englishDisplayNames = new HashMap<>();
|
||||
|
||||
SortedSet<String> languages = supportedLanguages.stream()
|
||||
.map(Locale::getLanguage)
|
||||
.filter(it -> !it.isBlank())
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
SortedSet<String> scripts = supportedLanguages.stream()
|
||||
.map(Locale::getScript)
|
||||
.filter(it -> !it.isBlank())
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
for (Locale currentLanguage : supportedLanguages) {
|
||||
InputStream overrideFile = CreateLocaleNames.class.getResourceAsStream(
|
||||
mapToFileName("LocaleNamesOverride", "properties", currentLanguage));
|
||||
|
||||
Properties overrideProperties = new Properties();
|
||||
if (overrideFile != null) {
|
||||
try (var reader = new InputStreamReader(overrideFile, StandardCharsets.UTF_8)) {
|
||||
overrideProperties.load(reader);
|
||||
}
|
||||
}
|
||||
|
||||
Path targetFile = outputDir.resolve(mapToFileName("LocaleNames", "properties", currentLanguage));
|
||||
if (Files.exists(targetFile))
|
||||
throw new GradleException(String.format("File %s already exists", targetFile));
|
||||
|
||||
try (var writer = Files.newBufferedWriter(targetFile)) {
|
||||
writer.write("""
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
""");
|
||||
|
||||
writer.write("# Languages\n");
|
||||
for (String language : languages) {
|
||||
String displayName = overrideProperties.getProperty(language);
|
||||
if (displayName == null) {
|
||||
if (currentLanguage.equals(UpsideDownTranslate.EN_QABS) && englishDisplayNames.containsKey(language)) {
|
||||
displayName = upsideDownTranslator.translate(englishDisplayNames.get(language));
|
||||
} else {
|
||||
displayName = new Locale.Builder()
|
||||
.setLanguage(language)
|
||||
.build()
|
||||
.getDisplayLanguage(currentLanguage);
|
||||
|
||||
if (displayName.equals(language)
|
||||
|| (!currentLanguage.equals(Locale.ENGLISH) && displayName.equals(englishDisplayNames.get(language))))
|
||||
continue; // Skip
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLanguage.equals(Locale.ENGLISH))
|
||||
englishDisplayNames.put(language, displayName);
|
||||
|
||||
writer.write(language + "=" + displayName + "\n");
|
||||
}
|
||||
writer.write('\n');
|
||||
|
||||
writer.write("# Scripts\n");
|
||||
for (String script : scripts) {
|
||||
String displayName = overrideProperties.getProperty(script);
|
||||
if (displayName == null) {
|
||||
if (currentLanguage.equals(UpsideDownTranslate.EN_QABS) && englishDisplayNames.containsKey(script)) {
|
||||
displayName = upsideDownTranslator.translate(englishDisplayNames.get(script));
|
||||
} else {
|
||||
displayName = new Locale.Builder()
|
||||
.setScript(script)
|
||||
.build()
|
||||
.getDisplayScript(currentLanguage);
|
||||
|
||||
if (displayName.equals(script)
|
||||
|| (!currentLanguage.equals(Locale.ENGLISH) && displayName.equals(englishDisplayNames.get(script))))
|
||||
continue; // Skip
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLanguage.equals(Locale.ENGLISH))
|
||||
englishDisplayNames.put(script, displayName);
|
||||
|
||||
writer.write(script + "=" + displayName + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class UpsideDownTranslate extends DefaultTask {
|
||||
|
||||
static final Locale EN_QABS = Locale.forLanguageTag("en-Qabs");
|
||||
|
||||
private static final Map<String, String> PROPERTIES = Map.of(
|
||||
"datetime.format", "MMM d, yyyy, h:mm:ss a"
|
||||
);
|
||||
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getInputFile();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path inputFile = getInputFile().get().getAsFile().toPath();
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
|
||||
Properties english = new Properties();
|
||||
try (var reader = Files.newBufferedReader(inputFile)) {
|
||||
english.load(reader);
|
||||
}
|
||||
|
||||
Properties output = new Properties();
|
||||
Translator translator = new Translator();
|
||||
english.forEach((k, v) -> {
|
||||
if (PROPERTIES.containsKey(k.toString())) {
|
||||
output.setProperty(k.toString(), PROPERTIES.get(k.toString()));
|
||||
} else {
|
||||
output.put(k, translator.translate(v.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
try (var writer = Files.newBufferedWriter(outputFile)) {
|
||||
output.store(writer, "This file is automatically generated, please do not modify it manually");
|
||||
}
|
||||
}
|
||||
|
||||
static final class Translator {
|
||||
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
|
||||
|
||||
private static void putChars(char baseChar, String upsideDownChars) {
|
||||
for (int i = 0; i < upsideDownChars.length(); i++) {
|
||||
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void putChars(String baseChars, String upsideDownChars) {
|
||||
if (baseChars.length() != upsideDownChars.length()) {
|
||||
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
|
||||
}
|
||||
|
||||
for (int i = 0; i < baseChars.length(); i++) {
|
||||
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
|
||||
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
|
||||
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
|
||||
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
|
||||
}
|
||||
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("^%(\\d\\$)?(\\d+)?(\\.\\d+)?([sdf])");
|
||||
private static final Pattern XML_TAG_PATTERN = Pattern.compile("^<(?<tag>[a-zA-Z]+)( href=\"[^\"]*\")?>");
|
||||
|
||||
private final StringBuilder resultBuilder = new StringBuilder();
|
||||
|
||||
private void appendToLineBuilder(String input) {
|
||||
for (int i = 0; i < input.length(); ) {
|
||||
int ch = input.codePointAt(i);
|
||||
|
||||
if (ch == '%') {
|
||||
Matcher matcher = FORMAT_PATTERN.matcher(input).region(i, input.length());
|
||||
if (matcher.find()) {
|
||||
String formatString = matcher.group();
|
||||
resultBuilder.insert(0, formatString);
|
||||
i += formatString.length();
|
||||
continue;
|
||||
}
|
||||
} else if (ch == '<') {
|
||||
Matcher matcher = XML_TAG_PATTERN.matcher(input).region(i, input.length());
|
||||
if (matcher.find()) {
|
||||
String beginTag = matcher.group();
|
||||
String endTag = "</" + matcher.group(1) + ">";
|
||||
|
||||
int endTagOffset = input.indexOf(endTag, i + beginTag.length());
|
||||
if (endTagOffset > 0) {
|
||||
resultBuilder.insert(0, endTag);
|
||||
appendToLineBuilder(input.substring(i + beginTag.length(), endTagOffset));
|
||||
resultBuilder.insert(0, beginTag);
|
||||
|
||||
i = endTagOffset + endTag.length();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int udCh = MAPPER.getOrDefault(ch, ch);
|
||||
if (Character.isBmpCodePoint(udCh)) {
|
||||
resultBuilder.insert(0, (char) udCh);
|
||||
} else {
|
||||
resultBuilder.insert(0, Character.toChars(udCh));
|
||||
}
|
||||
|
||||
i += Character.charCount(ch);
|
||||
}
|
||||
}
|
||||
|
||||
String translate(String input) {
|
||||
resultBuilder.setLength(0);
|
||||
appendToLineBuilder(input);
|
||||
return resultBuilder.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Languages
|
||||
lzh=Classical Chinese
|
||||
|
||||
# Scripts
|
||||
Qabs=Upside down
|
@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Languages
|
||||
lzh=文言
|
||||
|
||||
# Scripts
|
||||
Qabs=颠倒
|
@ -13,6 +13,7 @@ HMCL 为多种语言提供本地化支持。
|
||||
| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | [游戏语言文件](https://minecraft.wiki/w/Language) | 支持状态 | 志愿者 |
|
||||
|---------|-----------|------------|-----------|---------------------------------------------|--------|-------------------------------------------|
|
||||
| 英语 | `en` | (空) | `default` | `en_us` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 英语 (颠倒) | `en-Qabs` | `en_Qabs` | `en-Qabs` | `en_ud` | 自动 | |
|
||||
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | `zh_tw` <br/> `zh_hk` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||
| 中文 (文言) | `lzh` | `_lzh` | `lzh` | `lzh` | 次要 | |
|
||||
@ -56,7 +57,8 @@ HMCL 欢迎任何人参与翻译和贡献。但是维护更多语言的翻译需
|
||||
如果你想为 HMCL 添加新的语言支持,请从翻译 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties) 开始。
|
||||
HMCL 的绝大多数文本都位于这个文件中,翻译此文件就能翻译整个界面。
|
||||
|
||||
这是一个 Java Properties 文件,格式非常简单。在翻译前请先阅读该格式的介绍: [Properties 文件](https://en.wikipedia.org/wiki/.properties)。
|
||||
这是一个 Java Properties 文件,格式非常简单。
|
||||
在翻译前请先阅读该格式的介绍: [Properties 文件](https://en.wikipedia.org/wiki/.properties)。
|
||||
|
||||
作为翻译的第一步,请从[这张表格](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes)中查询这个语言对应的两字母或三字母语言标签。
|
||||
例如,英语的语言标签为 `en`。
|
||||
|
Loading…
x
Reference in New Issue
Block a user