支持颠倒的英语 (#4527)
This commit is contained in:
@@ -1071,6 +1071,7 @@ ij_jsp_keep_indents_on_empty_lines = false
|
|||||||
ij_jspx_keep_indents_on_empty_lines = false
|
ij_jspx_keep_indents_on_empty_lines = false
|
||||||
|
|
||||||
[{*.markdown,*.md}]
|
[{*.markdown,*.md}]
|
||||||
|
max_line_length = 200
|
||||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||||
ij_markdown_force_one_space_after_header_symbol = true
|
ij_markdown_force_one_space_after_header_symbol = true
|
||||||
ij_markdown_force_one_space_after_list_bullet = true
|
ij_markdown_force_one_space_after_list_bullet = true
|
||||||
|
|||||||
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
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
|
*.hprof
|
||||||
|
|
||||||
.gradle
|
.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.CreateLocaleNamesResourceBundle
|
||||||
|
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
@@ -206,11 +209,20 @@ tasks.shadowJar {
|
|||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
dependsOn(createPropertiesFile)
|
dependsOn(createPropertiesFile)
|
||||||
|
dependsOn(upsideDownTranslate)
|
||||||
|
dependsOn(createLocaleNamesResourceBundle)
|
||||||
|
dependsOn(createLanguageList)
|
||||||
|
|
||||||
into("assets/") {
|
into("assets/") {
|
||||||
from(hmclPropertiesFile)
|
from(hmclPropertiesFile)
|
||||||
from(embedResources)
|
from(embedResources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
into("assets/lang") {
|
||||||
|
from(createLanguageList.map { it.outputFile })
|
||||||
|
from(upsideDownTranslate.map { it.outputFile })
|
||||||
|
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val makeExecutables by tasks.registering {
|
val makeExecutables by tasks.registering {
|
||||||
@@ -344,6 +356,29 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
|||||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
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 createLocaleNamesResourceBundle by tasks.registering(CreateLocaleNamesResourceBundle::class) {
|
||||||
|
dependsOn(createLanguageList)
|
||||||
|
|
||||||
|
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||||
|
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||||
|
}
|
||||||
|
|
||||||
// mcmod data
|
// mcmod data
|
||||||
|
|
||||||
tasks.register<ParseModDataTask>("parseModData") {
|
tasks.register<ParseModDataTask>("parseModData") {
|
||||||
|
|||||||
@@ -80,8 +80,6 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Locale locale = Locale.getDefault();
|
Locale locale = Locale.getDefault();
|
||||||
if (LocaleUtils.isEnglish(locale))
|
|
||||||
return;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 1.0 : No language option, do not set for these versions
|
* 1.0 : No language option, do not set for these versions
|
||||||
@@ -112,7 +110,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||||
String region = locale.getCountry();
|
String region = locale.getCountry();
|
||||||
|
|
||||||
return switch (LocaleUtils.getISO1Language(locale)) {
|
return switch (LocaleUtils.getISO2Language(locale)) {
|
||||||
case "es" -> "es_ES";
|
case "es" -> "es_ES";
|
||||||
case "ja" -> "ja_JP";
|
case "ja" -> "ja_JP";
|
||||||
case "ru" -> "ru_RU";
|
case "ru" -> "ru_RU";
|
||||||
@@ -129,6 +127,13 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
}
|
}
|
||||||
yield "zh_CN";
|
yield "zh_CN";
|
||||||
}
|
}
|
||||||
|
case "en" -> {
|
||||||
|
if ("Qabs".equals(LocaleUtils.getScript(locale)) && gameVersion.compareTo("1.16") >= 0) {
|
||||||
|
yield "en_UD";
|
||||||
|
}
|
||||||
|
|
||||||
|
yield "";
|
||||||
|
}
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
|||||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.*;
|
import org.jackhuang.hmcl.util.gson.*;
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -222,7 +221,7 @@ public final class Config implements Observable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SerializedName("localization")
|
@SerializedName("localization")
|
||||||
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
|
private final ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(SupportedLocale.DEFAULT);
|
||||||
|
|
||||||
public ObjectProperty<SupportedLocale> localizationProperty() {
|
public ObjectProperty<SupportedLocale> localizationProperty() {
|
||||||
return localization;
|
return localization;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
|||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
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.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
import org.tukaani.xz.XZInputStream;
|
import org.tukaani.xz.XZInputStream;
|
||||||
@@ -65,7 +65,7 @@ public final class SettingsPage extends SettingsView {
|
|||||||
FXUtils.smoothScrolling(scroll);
|
FXUtils.smoothScrolling(scroll);
|
||||||
|
|
||||||
// ==== Languages ====
|
// ==== Languages ====
|
||||||
cboLanguage.getItems().setAll(Locales.LOCALES);
|
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
|
||||||
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
|
||||||
|
|
||||||
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
|
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.MultiFileItem;
|
||||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
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;
|
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.RemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ public final class I18n {
|
|||||||
private I18n() {
|
private I18n() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
private static volatile SupportedLocale locale = SupportedLocale.DEFAULT;
|
||||||
|
|
||||||
public static void setLocale(SupportedLocale locale) {
|
public static void setLocale(SupportedLocale locale) {
|
||||||
I18n.locale = locale;
|
I18n.locale = locale;
|
||||||
@@ -71,6 +70,11 @@ public final class I18n {
|
|||||||
else
|
else
|
||||||
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) {
|
||||||
|
return UpsideDownUtils.translate(version.getSelfVersion());
|
||||||
|
}
|
||||||
|
|
||||||
return version.getSelfVersion();
|
return 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}.+$");
|
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();
|
String wikiVersion = version.getSelfVersion();
|
||||||
var gameVersion = GameVersionNumber.asGameVersion(wikiVersion);
|
var gameVersion = GameVersionNumber.asGameVersion(wikiVersion);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ public final class MinecraftWiki {
|
|||||||
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
|
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
|
||||||
|
|
||||||
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
|
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()) {
|
} else if (SNAPSHOT_PATTERN.matcher(wikiVersion).matches()) {
|
||||||
return locale.i18n("wiki.version.game.snapshot", translatedVersion);
|
return locale.i18n("wiki.version.game.snapshot", translatedVersion);
|
||||||
} else {
|
} 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.isBlank() || "def".equals(name) || "default".equals(name))
|
||||||
|
return DEFAULT;
|
||||||
|
|
||||||
|
return getLocale(Locale.forLanguageTag(name.trim().replace('_', '-')));
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getISO2Language(this.getLocale())
|
||||||
|
.equals(LocaleUtils.getISO2Language(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=New Instance
|
||||||
install.change_version=Change Version
|
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.change_version.process=Change Version Process
|
||||||
install.failed=Failed to install
|
install.failed=Failed to install
|
||||||
install.failed.downloading=Failed to download some required files.
|
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.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_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.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.change_version=%s Incompatible
|
||||||
install.installer.choose=Choose Your %s Version
|
install.installer.choose=Choose Your %s Version
|
||||||
install.installer.cleanroom=Cleanroom
|
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_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.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.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.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.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.
|
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.installing.given=Installing %s modpack
|
||||||
modpack.introduction=Curse, Modrinth, MultiMC, and MCBBS modpacks are currently supported.
|
modpack.introduction=Curse, Modrinth, MultiMC, and MCBBS modpacks are currently supported.
|
||||||
modpack.invalid=Invalid modpack, you can try downloading it again.
|
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.name=Modpack Name
|
||||||
modpack.not_a_valid_name=Invalid modpack name.
|
modpack.not_a_valid_name=Invalid modpack name.
|
||||||
modpack.origin=Source
|
modpack.origin=Source
|
||||||
@@ -1236,7 +1236,7 @@ search.first_page=First
|
|||||||
search.previous_page=Previous
|
search.previous_page=Previous
|
||||||
search.next_page=Next
|
search.next_page=Next
|
||||||
search.last_page=Last
|
search.last_page=Last
|
||||||
search.page_n=%d / %s
|
search.page_n=%1$d / %2$s
|
||||||
|
|
||||||
selector.choose=Choose
|
selector.choose=Choose
|
||||||
selector.choose_file=Choose file
|
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.choose=Choose Java
|
||||||
settings.game.java_directory.invalid=Incorrect Java path
|
settings.game.java_directory.invalid=Incorrect Java path
|
||||||
settings.game.java_directory.version=Specify Java Version
|
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.management=Manage
|
||||||
settings.game.working_directory=Working Directory
|
settings.game.working_directory=Working Directory
|
||||||
settings.game.working_directory.choose=Choose the 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.manage.title=Edit Instance - %1s
|
||||||
version.manage.redownload_assets_index=Update Game Assets
|
version.manage.redownload_assets_index=Update Game Assets
|
||||||
version.manage.remove=Delete Instance
|
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.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_assets=Delete All Assets
|
||||||
version.manage.remove_libraries=Delete All Libraries
|
version.manage.remove_libraries=Delete All Libraries
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import java.util.ResourceBundle;
|
|||||||
/// - For all Chinese locales, `zh-CN` is always added to the candidate list. If `zh-Hans` already exists in the candidate list,
|
/// - 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`.
|
/// `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 Traditional Chinese locales, `zh-TW` is always added to the candidate list (before `zh`).
|
||||||
/// - For all [supported][LocaleUtils#mapToISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
|
/// - For all [supported][LocaleUtils#mapToISO2Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
|
||||||
/// a candidate list with the language code replaced by the ISO 639-1 (Macro)language code is added to the end of the candidate list.
|
/// a candidate list with the language code replaced by the ISO 639-1 (Macro)language code is added to the end of the candidate list.
|
||||||
///
|
///
|
||||||
/// @author Glavo
|
/// @author Glavo
|
||||||
@@ -45,4 +45,11 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
|
|||||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||||
return LocaleUtils.getCandidateLocales(locale);
|
return LocaleUtils.getCandidateLocales(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Locale getFallbackLocale(String baseName, Locale locale) {
|
||||||
|
// By default, when only the base bundle is found, it will attempt to fall back to Locale.getDefault() for further lookup.
|
||||||
|
// Since we always use the base bundle as the English resource file, we want to suppress this behavior.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import org.jetbrains.annotations.Unmodifiable;
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -44,6 +46,56 @@ public final class LocaleUtils {
|
|||||||
|
|
||||||
public static final String DEFAULT_LANGUAGE_KEY = "default";
|
public static final String DEFAULT_LANGUAGE_KEY = "default";
|
||||||
|
|
||||||
|
private static final Map<String, String> subLanguageToParent = new HashMap<>();
|
||||||
|
private static final Map<String, String> iso3To2 = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
try (InputStream input = LocaleUtils.class.getResourceAsStream("/assets/lang/sublanguages.csv")) {
|
||||||
|
if (input != null) {
|
||||||
|
new String(input.readAllBytes()).lines()
|
||||||
|
.filter(line -> !line.startsWith("#") && !line.isBlank())
|
||||||
|
.forEach(line -> {
|
||||||
|
String[] languages = line.split(",");
|
||||||
|
if (languages.length < 2)
|
||||||
|
LOG.warning("Invalid line in sublanguages.csv: " + line);
|
||||||
|
|
||||||
|
String parent = languages[0];
|
||||||
|
for (int i = 1; i < languages.length; i++) {
|
||||||
|
subLanguageToParent.put(languages[i], parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOG.warning("Failed to load sublanguages.csv", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line Format:
|
||||||
|
// (?<iso2>[a-z]{2}),(?<iso3>[a-z]{3})
|
||||||
|
try (InputStream input = LocaleUtils.class.getResourceAsStream("/assets/lang/iso_languages.csv")) {
|
||||||
|
if (input != null) {
|
||||||
|
int lineLength = 2 + 1 + 3;
|
||||||
|
|
||||||
|
byte[] bytes = input.readAllBytes();
|
||||||
|
for (int offset = 0; offset < bytes.length; ) {
|
||||||
|
if (offset > bytes.length - lineLength)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (bytes[offset + 2] != ',')
|
||||||
|
throw new IOException("iso_languages.csv format invalid");
|
||||||
|
|
||||||
|
String iso2 = new String(bytes, offset, 2, StandardCharsets.US_ASCII);
|
||||||
|
String iso3 = new String(bytes, offset + 3, 3, StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
iso3To2.put(iso3, iso2);
|
||||||
|
|
||||||
|
offset += (lineLength + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOG.warning("Failed to load iso_languages.csv", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Locale getInstance(String language, String script, String region,
|
private static Locale getInstance(String language, String script, String region,
|
||||||
String variant) {
|
String variant) {
|
||||||
Locale.Builder builder = new Locale.Builder();
|
Locale.Builder builder = new Locale.Builder();
|
||||||
@@ -64,7 +116,7 @@ public final class LocaleUtils {
|
|||||||
: locale.stripExtensions().toLanguageTag();
|
: locale.stripExtensions().toLanguageTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull String getISO1Language(Locale locale) {
|
public static @NotNull String getISO2Language(Locale locale) {
|
||||||
String language = locale.getLanguage();
|
String language = locale.getLanguage();
|
||||||
if (language.isEmpty()) return "en";
|
if (language.isEmpty()) return "en";
|
||||||
if (language.length() <= 2)
|
if (language.length() <= 2)
|
||||||
@@ -75,7 +127,7 @@ public final class LocaleUtils {
|
|||||||
if (lang.length() <= 2)
|
if (lang.length() <= 2)
|
||||||
return lang;
|
return lang;
|
||||||
else {
|
else {
|
||||||
String iso1 = mapToISO1Language(lang);
|
String iso1 = mapToISO2Language(lang);
|
||||||
if (iso1 != null)
|
if (iso1 != null)
|
||||||
return iso1;
|
return iso1;
|
||||||
}
|
}
|
||||||
@@ -88,6 +140,12 @@ public final class LocaleUtils {
|
|||||||
/// the script will be inferred based on the language, the region and the variant.
|
/// the script will be inferred based on the language, the region and the variant.
|
||||||
public static @NotNull String getScript(Locale locale) {
|
public static @NotNull String getScript(Locale locale) {
|
||||||
if (locale.getScript().isEmpty()) {
|
if (locale.getScript().isEmpty()) {
|
||||||
|
if (isEnglish(locale)) {
|
||||||
|
if ("UD".equals(locale.getCountry())) {
|
||||||
|
return "Qabs";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isChinese(locale)) {
|
if (isChinese(locale)) {
|
||||||
if (CHINESE_LATN_VARIANTS.contains(locale.getVariant()))
|
if (CHINESE_LATN_VARIANTS.contains(locale.getVariant()))
|
||||||
return "Latn";
|
return "Latn";
|
||||||
@@ -130,7 +188,7 @@ public final class LocaleUtils {
|
|||||||
} else if (language.length() <= 2) {
|
} else if (language.length() <= 2) {
|
||||||
languages = List.of(language);
|
languages = List.of(language);
|
||||||
} else {
|
} else {
|
||||||
String iso1Language = mapToISO1Language(language);
|
String iso1Language = mapToISO2Language(language);
|
||||||
languages = iso1Language != null
|
languages = iso1Language != null
|
||||||
? List.of(language, iso1Language)
|
? List.of(language, iso1Language)
|
||||||
: List.of(language);
|
: List.of(language);
|
||||||
@@ -294,37 +352,26 @@ public final class LocaleUtils {
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
/// Map ISO 639-3 language codes to ISO 639-1 language codes.
|
/// Map ISO 639 alpha-3 language codes to ISO 639 alpha-2 language codes.
|
||||||
public static @Nullable String mapToISO1Language(String iso3Language) {
|
public static @Nullable String mapToISO2Language(String iso3Language) {
|
||||||
return switch (iso3Language) {
|
return iso3To2.get(iso3Language);
|
||||||
case "eng" -> "en";
|
|
||||||
case "spa" -> "es";
|
|
||||||
case "jpa" -> "ja";
|
|
||||||
case "rus" -> "ru";
|
|
||||||
case "ukr" -> "uk";
|
|
||||||
case "zho" -> "zh";
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @Nullable String getParentLanguage(String language) {
|
public static @Nullable String getParentLanguage(String language) {
|
||||||
return switch (language) {
|
return !language.isEmpty()
|
||||||
case "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
|
? subLanguageToParent.getOrDefault(language, "")
|
||||||
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
|
: null;
|
||||||
case "" -> null;
|
|
||||||
default -> "";
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEnglish(Locale locale) {
|
public static boolean isEnglish(Locale locale) {
|
||||||
return "en".equals(getISO1Language(locale));
|
return "en".equals(getISO2Language(locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Set<String> CHINESE_TRADITIONAL_REGIONS = Set.of("TW", "HK", "MO");
|
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_LATN_VARIANTS = Set.of("pinyin", "wadegile", "tongyong");
|
||||||
|
|
||||||
public static boolean isChinese(Locale locale) {
|
public static boolean isChinese(Locale locale) {
|
||||||
return "zh".equals(getISO1Language(locale));
|
return "zh".equals(getISO2Language(locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocaleUtils() {
|
private LocaleUtils() {
|
||||||
|
|||||||
187
HMCLCore/src/main/resources/assets/lang/iso_languages.csv
Normal file
187
HMCLCore/src/main/resources/assets/lang/iso_languages.csv
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
aa,aar
|
||||||
|
ab,abk
|
||||||
|
ae,ave
|
||||||
|
af,afr
|
||||||
|
ak,aka
|
||||||
|
am,amh
|
||||||
|
an,arg
|
||||||
|
ar,ara
|
||||||
|
as,asm
|
||||||
|
av,ava
|
||||||
|
ay,aym
|
||||||
|
az,aze
|
||||||
|
ba,bak
|
||||||
|
be,bel
|
||||||
|
bg,bul
|
||||||
|
bh,bih
|
||||||
|
bi,bis
|
||||||
|
bm,bam
|
||||||
|
bn,ben
|
||||||
|
bo,bod
|
||||||
|
br,bre
|
||||||
|
bs,bos
|
||||||
|
ca,cat
|
||||||
|
ce,che
|
||||||
|
ch,cha
|
||||||
|
co,cos
|
||||||
|
cr,cre
|
||||||
|
cs,ces
|
||||||
|
cu,chu
|
||||||
|
cv,chv
|
||||||
|
cy,cym
|
||||||
|
da,dan
|
||||||
|
de,deu
|
||||||
|
dv,div
|
||||||
|
dz,dzo
|
||||||
|
ee,ewe
|
||||||
|
el,ell
|
||||||
|
en,eng
|
||||||
|
eo,epo
|
||||||
|
es,spa
|
||||||
|
et,est
|
||||||
|
eu,eus
|
||||||
|
fa,fas
|
||||||
|
ff,ful
|
||||||
|
fi,fin
|
||||||
|
fj,fij
|
||||||
|
fo,fao
|
||||||
|
fr,fra
|
||||||
|
fy,fry
|
||||||
|
ga,gle
|
||||||
|
gd,gla
|
||||||
|
gl,glg
|
||||||
|
gn,grn
|
||||||
|
gu,guj
|
||||||
|
gv,glv
|
||||||
|
ha,hau
|
||||||
|
he,heb
|
||||||
|
hi,hin
|
||||||
|
ho,hmo
|
||||||
|
hr,hrv
|
||||||
|
ht,hat
|
||||||
|
hu,hun
|
||||||
|
hy,hye
|
||||||
|
hz,her
|
||||||
|
ia,ina
|
||||||
|
id,ind
|
||||||
|
ie,ile
|
||||||
|
ig,ibo
|
||||||
|
ii,iii
|
||||||
|
ik,ipk
|
||||||
|
in,ind
|
||||||
|
io,ido
|
||||||
|
is,isl
|
||||||
|
it,ita
|
||||||
|
iu,iku
|
||||||
|
iw,heb
|
||||||
|
ja,jpn
|
||||||
|
ji,yid
|
||||||
|
jv,jav
|
||||||
|
ka,kat
|
||||||
|
kg,kon
|
||||||
|
ki,kik
|
||||||
|
kj,kua
|
||||||
|
kk,kaz
|
||||||
|
kl,kal
|
||||||
|
km,khm
|
||||||
|
kn,kan
|
||||||
|
ko,kor
|
||||||
|
kr,kau
|
||||||
|
ks,kas
|
||||||
|
ku,kur
|
||||||
|
kv,kom
|
||||||
|
kw,cor
|
||||||
|
ky,kir
|
||||||
|
la,lat
|
||||||
|
lb,ltz
|
||||||
|
lg,lug
|
||||||
|
li,lim
|
||||||
|
ln,lin
|
||||||
|
lo,lao
|
||||||
|
lt,lit
|
||||||
|
lu,lub
|
||||||
|
lv,lav
|
||||||
|
mg,mlg
|
||||||
|
mh,mah
|
||||||
|
mi,mri
|
||||||
|
mk,mkd
|
||||||
|
ml,mal
|
||||||
|
mn,mon
|
||||||
|
mo,mol
|
||||||
|
mr,mar
|
||||||
|
ms,msa
|
||||||
|
mt,mlt
|
||||||
|
my,mya
|
||||||
|
na,nau
|
||||||
|
nb,nob
|
||||||
|
nd,nde
|
||||||
|
ne,nep
|
||||||
|
ng,ndo
|
||||||
|
nl,nld
|
||||||
|
nn,nno
|
||||||
|
no,nor
|
||||||
|
nr,nbl
|
||||||
|
nv,nav
|
||||||
|
ny,nya
|
||||||
|
oc,oci
|
||||||
|
oj,oji
|
||||||
|
om,orm
|
||||||
|
or,ori
|
||||||
|
os,oss
|
||||||
|
pa,pan
|
||||||
|
pi,pli
|
||||||
|
pl,pol
|
||||||
|
ps,pus
|
||||||
|
pt,por
|
||||||
|
qu,que
|
||||||
|
rm,roh
|
||||||
|
rn,run
|
||||||
|
ro,ron
|
||||||
|
ru,rus
|
||||||
|
rw,kin
|
||||||
|
sa,san
|
||||||
|
sc,srd
|
||||||
|
sd,snd
|
||||||
|
se,sme
|
||||||
|
sg,sag
|
||||||
|
si,sin
|
||||||
|
sk,slk
|
||||||
|
sl,slv
|
||||||
|
sm,smo
|
||||||
|
sn,sna
|
||||||
|
so,som
|
||||||
|
sq,sqi
|
||||||
|
sr,srp
|
||||||
|
ss,ssw
|
||||||
|
st,sot
|
||||||
|
su,sun
|
||||||
|
sv,swe
|
||||||
|
sw,swa
|
||||||
|
ta,tam
|
||||||
|
te,tel
|
||||||
|
tg,tgk
|
||||||
|
th,tha
|
||||||
|
ti,tir
|
||||||
|
tk,tuk
|
||||||
|
tl,tgl
|
||||||
|
tn,tsn
|
||||||
|
to,ton
|
||||||
|
tr,tur
|
||||||
|
ts,tso
|
||||||
|
tt,tat
|
||||||
|
tw,twi
|
||||||
|
ty,tah
|
||||||
|
ug,uig
|
||||||
|
uk,ukr
|
||||||
|
ur,urd
|
||||||
|
uz,uzb
|
||||||
|
ve,ven
|
||||||
|
vi,vie
|
||||||
|
vo,vol
|
||||||
|
wa,wln
|
||||||
|
wo,wol
|
||||||
|
xh,xho
|
||||||
|
yi,yid
|
||||||
|
za,zha
|
||||||
|
zh,zho
|
||||||
|
zu,zul
|
||||||
|
1
HMCLCore/src/main/resources/assets/lang/sublanguages.csv
Normal file
1
HMCLCore/src/main/resources/assets/lang/sublanguages.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zh,cmn,lzh,cdo,cjy,cpx,czh,gan,hak,hsn,mnp,nan,wuu,yue
|
||||||
|
@@ -72,8 +72,8 @@ public final class LocaleUtilsTest {
|
|||||||
|
|
||||||
assertCandidateLocales("ja", List.of("ja", "und"));
|
assertCandidateLocales("ja", List.of("ja", "und"));
|
||||||
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
|
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
|
||||||
assertCandidateLocales("jpa", List.of("jpa", "ja", "und"));
|
assertCandidateLocales("jpn", List.of("jpn", "ja", "und"));
|
||||||
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "ja-JP", "jpa", "ja", "und"));
|
assertCandidateLocales("jpn-JP", List.of("jpn-JP", "ja-JP", "jpn", "ja", "und"));
|
||||||
|
|
||||||
assertCandidateLocales("en", List.of("en", "und"));
|
assertCandidateLocales("en", List.of("en", "und"));
|
||||||
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
|
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
|
||||||
@@ -190,4 +190,21 @@ public final class LocaleUtilsTest {
|
|||||||
LocaleUtils.findAllLocalizedFiles(testDir, "meow", Set.of("json", "toml")));
|
LocaleUtils.findAllLocalizedFiles(testDir, "meow", Set.of("json", "toml")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMapToISO2Language() throws IOException {
|
||||||
|
assertEquals("en", LocaleUtils.mapToISO2Language("eng"));
|
||||||
|
assertEquals("es", LocaleUtils.mapToISO2Language("spa"));
|
||||||
|
assertEquals("ja", LocaleUtils.mapToISO2Language("jpn"));
|
||||||
|
assertEquals("ru", LocaleUtils.mapToISO2Language("rus"));
|
||||||
|
assertEquals("uk", LocaleUtils.mapToISO2Language("ukr"));
|
||||||
|
assertEquals("zh", LocaleUtils.mapToISO2Language("zho"));
|
||||||
|
assertEquals("zu", LocaleUtils.mapToISO2Language("zul"));
|
||||||
|
|
||||||
|
assertNull(LocaleUtils.mapToISO2Language(null));
|
||||||
|
assertNull(LocaleUtils.mapToISO2Language(""));
|
||||||
|
assertNull(LocaleUtils.mapToISO2Language("cmn"));
|
||||||
|
assertNull(LocaleUtils.mapToISO2Language("lzh"));
|
||||||
|
assertNull(LocaleUtils.mapToISO2Language("tlh"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,9 @@ java {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
into("org/jackhuang/hmcl/gradle/l10n") {
|
||||||
|
from(projectDir.resolve("../HMCLCore/src/main/resources/assets/lang/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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.DefaultTask;
|
||||||
import org.gradle.api.GradleException;
|
import org.gradle.api.GradleException;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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.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<>(LocalizationUtils::compareLocale);
|
||||||
|
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(", ", "[", "]")));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* 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.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
public abstract class CreateLocaleNamesResourceBundle extends DefaultTask {
|
||||||
|
|
||||||
|
@InputFile
|
||||||
|
public abstract RegularFileProperty getLanguagesFile();
|
||||||
|
|
||||||
|
@OutputDirectory
|
||||||
|
public abstract DirectoryProperty getOutputDirectory();
|
||||||
|
|
||||||
|
private static String mapToFileName(String base, String ext, Locale locale) {
|
||||||
|
if (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ResourceBundle.Control CONTROL = new ResourceBundle.Control() {
|
||||||
|
};
|
||||||
|
|
||||||
|
@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)
|
||||||
|
.sorted(LocalizationUtils::compareLocale)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedLanguages.contains(Locale.ENGLISH))
|
||||||
|
throw new GradleException("Missing english in supported languages: " + supportedLanguages);
|
||||||
|
|
||||||
|
// Ensure English is at the first position, this assumption will be used later
|
||||||
|
if (!supportedLanguages.get(0).equals(Locale.ENGLISH)) {
|
||||||
|
supportedLanguages = new ArrayList<>(supportedLanguages);
|
||||||
|
supportedLanguages.remove(Locale.ENGLISH);
|
||||||
|
supportedLanguages.add(0, Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumMap<LocaleField, SortedSet<String>> names = new EnumMap<>(LocaleField.class);
|
||||||
|
for (LocaleField field : LocaleField.values()) {
|
||||||
|
names.put(field, supportedLanguages.stream()
|
||||||
|
.map(field::get)
|
||||||
|
.filter(it -> !it.isBlank())
|
||||||
|
.collect(Collectors.toCollection(() -> new TreeSet<>(field))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Locale, Properties> overrides = new HashMap<>();
|
||||||
|
for (Locale currentLanguage : supportedLanguages) {
|
||||||
|
InputStream overrideFile = CreateLocaleNamesResourceBundle.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overrides.put(currentLanguage, overrideProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Locale, LocaleNames> allLocaleNames = new HashMap<>();
|
||||||
|
|
||||||
|
// For Upside Down English
|
||||||
|
UpsideDownTranslate.Translator upsideDownTranslator = new UpsideDownTranslate.Translator();
|
||||||
|
for (Locale currentLocale : supportedLanguages) {
|
||||||
|
Properties currentOverrides = overrides.get(currentLocale);
|
||||||
|
if (currentLocale.getLanguage().length() > 2 && currentOverrides.isEmpty()) {
|
||||||
|
// The JDK does not provide localized texts for these languages
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocaleNames currentDisplayNames = new LocaleNames();
|
||||||
|
|
||||||
|
for (LocaleField field : LocaleField.values()) {
|
||||||
|
SortedMap<String, String> nameToDisplayName = currentDisplayNames.getNameToDisplayName(field);
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for (String name : names.get(field)) {
|
||||||
|
String displayName = currentOverrides.getProperty(name);
|
||||||
|
|
||||||
|
getDisplayName:
|
||||||
|
if (displayName == null) {
|
||||||
|
if (currentLocale.equals(UpsideDownTranslate.EN_QABS)) {
|
||||||
|
String englishDisplayName = allLocaleNames.get(Locale.ENGLISH).getNameToDisplayName(field).get(name);
|
||||||
|
if (englishDisplayName != null) {
|
||||||
|
displayName = upsideDownTranslator.translate(englishDisplayName);
|
||||||
|
break getDisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although it cannot correctly handle the inheritance relationship between languages,
|
||||||
|
// we will not apply this function to sublanguages.
|
||||||
|
List<Locale> candidateLocales = CONTROL.getCandidateLocales("", currentLocale);
|
||||||
|
|
||||||
|
for (Locale candidateLocale : candidateLocales) {
|
||||||
|
Properties candidateOverride = overrides.get(candidateLocale);
|
||||||
|
if (candidateOverride != null && candidateOverride.containsKey(name)) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName = field.getDisplayName(name, currentLocale);
|
||||||
|
|
||||||
|
// JDK does not have a built-in translation
|
||||||
|
if (displayName.isBlank() || displayName.equals(name)) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is just a duplicate of the parent content, ignored it
|
||||||
|
for (Locale candidateLocale : candidateLocales) {
|
||||||
|
LocaleNames candidateLocaleNames = allLocaleNames.get(candidateLocale);
|
||||||
|
if (candidateLocaleNames != null) {
|
||||||
|
String candidateDisplayName = candidateLocaleNames.getNameToDisplayName(field).get(name);
|
||||||
|
if (displayName.equals(candidateDisplayName)) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore it if the JDK falls back to English when querying the display name
|
||||||
|
if (!currentLocale.equals(Locale.ENGLISH)
|
||||||
|
&& displayName.equals(allLocaleNames.get(Locale.ENGLISH).getNameToDisplayName(field).get(name))) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nameToDisplayName.put(name, displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allLocaleNames.put(currentLocale, currentDisplayNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Locale, LocaleNames> entry : allLocaleNames.entrySet()) {
|
||||||
|
if (!entry.getValue().isEmpty()) {
|
||||||
|
Path targetFile = outputDir.resolve(mapToFileName("LocaleNames", "properties", entry.getKey()));
|
||||||
|
entry.getValue().writeTo(targetFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class LocaleNames {
|
||||||
|
private final EnumMap<LocaleField, SortedMap<String, String>> displayNames = new EnumMap<>(LocaleField.class);
|
||||||
|
|
||||||
|
LocaleNames() {
|
||||||
|
for (LocaleField field : LocaleField.values()) {
|
||||||
|
displayNames.put(field, new TreeMap<>(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return displayNames.values().stream().allMatch(Map::isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedMap<String, String> getNameToDisplayName(LocaleField field) {
|
||||||
|
return displayNames.get(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeTo(Path file) throws IOException {
|
||||||
|
try (var writer = Files.newBufferedWriter(file, StandardOpenOption.CREATE_NEW)) {
|
||||||
|
boolean firstBlock = true;
|
||||||
|
|
||||||
|
for (var entry : displayNames.entrySet()) {
|
||||||
|
LocaleField field = entry.getKey();
|
||||||
|
SortedMap<String, String> values = entry.getValue();
|
||||||
|
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
if (firstBlock)
|
||||||
|
firstBlock = false;
|
||||||
|
else
|
||||||
|
writer.newLine();
|
||||||
|
|
||||||
|
writer.write("# " + field.blockHeader + "\n");
|
||||||
|
|
||||||
|
for (var nameToDisplay : values.entrySet()) {
|
||||||
|
writer.write(nameToDisplay.getKey() + "=" + nameToDisplay.getValue() + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum LocaleField implements Comparator<String> {
|
||||||
|
LANGUAGE("Languages") {
|
||||||
|
@Override
|
||||||
|
public String get(Locale locale) {
|
||||||
|
return locale.getLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName(String fieldValue, Locale inLocale) {
|
||||||
|
return new Locale.Builder()
|
||||||
|
.setLanguage(fieldValue)
|
||||||
|
.build()
|
||||||
|
.getDisplayLanguage(inLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(String l1, String l2) {
|
||||||
|
return LocalizationUtils.compareLanguage(l1, l2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SCRIPT("Scripts") {
|
||||||
|
@Override
|
||||||
|
public String get(Locale locale) {
|
||||||
|
return locale.getScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName(String fieldValue, Locale inLocale) {
|
||||||
|
return new Locale.Builder()
|
||||||
|
.setScript(fieldValue)
|
||||||
|
.build()
|
||||||
|
.getDisplayScript(inLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(String s1, String s2) {
|
||||||
|
return LocalizationUtils.compareScript(s1, s2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final String blockHeader;
|
||||||
|
|
||||||
|
LocaleField(String blockHeader) {
|
||||||
|
this.blockHeader = blockHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String get(Locale locale);
|
||||||
|
|
||||||
|
public abstract String getDisplayName(String fieldValue, Locale inLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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.GradleException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
final class LocalizationUtils {
|
||||||
|
public static final Map<String, String> subLanguageToParent;
|
||||||
|
|
||||||
|
static {
|
||||||
|
InputStream input = LocalizationUtils.class.getResourceAsStream("sublanguages.csv");
|
||||||
|
if (input == null)
|
||||||
|
throw new GradleException("Missing sublanguages.csv file");
|
||||||
|
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
try (input) {
|
||||||
|
new String(input.readAllBytes()).lines()
|
||||||
|
.filter(line -> !line.startsWith("#") && !line.isBlank())
|
||||||
|
.forEach(line -> {
|
||||||
|
String[] languages = line.split(",");
|
||||||
|
if (languages.length < 2)
|
||||||
|
throw new GradleException("Invalid line in sublanguages.csv: " + line);
|
||||||
|
|
||||||
|
String parent = languages[0];
|
||||||
|
for (int i = 1; i < languages.length; i++) {
|
||||||
|
map.put(languages[i], parent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new GradleException("Failed to read sublanguages.csv", e);
|
||||||
|
}
|
||||||
|
subLanguageToParent = Collections.unmodifiableMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int compareScript(String s1, String s2) {
|
||||||
|
return s1.compareTo(s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int compareLocale(Locale l1, Locale l2) {
|
||||||
|
int c = compareLanguage(l1.getLanguage(), l2.getLanguage());
|
||||||
|
if (c != 0)
|
||||||
|
return c;
|
||||||
|
|
||||||
|
c = compareScript(l1.getScript(), 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalizationUtils() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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=颠倒
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# 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=文言
|
||||||
@@ -10,16 +10,63 @@ HMCL 为多种语言提供本地化支持。
|
|||||||
|
|
||||||
目前,HMCL 为这些语言提供支持:
|
目前,HMCL 为这些语言提供支持:
|
||||||
|
|
||||||
| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | [游戏语言文件](https://minecraft.wiki/w/Language) | 支持状态 | 志愿者 |
|
| 语言 | 语言标签 | 首选本地化键 | 首选本地化文件后缀 | [游戏语言文件](https://minecraft.wiki/w/Language) | 支持状态 | 志愿者 |
|
||||||
|---------|-----------|------------|-----------|---------------------------------------------|--------|-------------------------------------------|
|
|---------|-----------|-----------|------------|---------------------------------------------|--------|-------------------------------------------|
|
||||||
| 英语 | `en` | (空) | `default` | `en_us` | **主要** | [Glavo](https://github.com/Glavo) |
|
| 英语 | `en` | `default` | (空) | `en_us` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||||
| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) |
|
| 英语 (颠倒) | `en-Qabs` | `en-Qabs` | `en_Qabs` | `en_ud` | 自动 | |
|
||||||
| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | `zh_tw` <br/> `zh_hk` | **主要** | [Glavo](https://github.com/Glavo) |
|
| 中文 (简体) | `zh-Hans` | `zh` | `_zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||||
| 中文 (文言) | `lzh` | `_lzh` | `lzh` | `lzh` | 次要 | |
|
| 中文 (繁体) | `zh-Hant` | `zh-Hant` | `_zh_Hant` | `zh_tw` <br/> `zh_hk` | **主要** | [Glavo](https://github.com/Glavo) |
|
||||||
| 日语 | `ja` | `_ja` | `ja` | `ja_jp` | 次要 | |
|
| 中文 (文言) | `lzh` | `lzh` | `_lzh` | `lzh` | 次要 | |
|
||||||
| 西班牙语 | `es` | `_es` | `es` | `es_es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
| 日语 | `ja` | `ja` | `_ja` | `ja_jp` | 次要 | |
|
||||||
| 俄语 | `ru` | `_ru` | `ru` | `ru_ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
| 西班牙语 | `es` | `es` | `_es` | `es_es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
||||||
| 乌克兰语 | `uk` | `_uk` | `uk` | `uk_ua` | 次要 | |
|
| 俄语 | `ru` | `ru` | `_ru` | `ru_ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) |
|
||||||
|
| 乌克兰语 | `uk` | `uk` | `_uk` | `uk_ua` | 次要 | |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>关于语言标签</summary>
|
||||||
|
|
||||||
|
HMCL 使用符合 IETF BCP 47 规范的语言标签。
|
||||||
|
|
||||||
|
对于 ISO 639 标准中定义的语言,如果同时存在两字母语言代码和三字母语言代码,那么应当优先选择两字母语言代码。
|
||||||
|
|
||||||
|
例如,对于英语,我们使用 `en` 而不是 `eng` 作为语言代码。
|
||||||
|
|
||||||
|
对于 Minecraft 所定义的非标准语言,应当优先使用语言文件的 `language.code` 中定义的代码,而非游戏语言文件的名称
|
||||||
|
(但对于存在两字母代码的语言,应当将三字母语言代码替换为对应的两字母语言代码)。
|
||||||
|
这是因为 Minecraft 有时候会用现实中实际存在的国家/地区代码来表示虚构语言 (比如说海盗英语的语言文件为 `en_pt`,但 `PT` 其实是葡萄牙的国家代码)。
|
||||||
|
|
||||||
|
例如,对于颠倒的英语,我们使用 `en-Qabs` 作为语言代码,而不是 `en-UD`。
|
||||||
|
|
||||||
|
此外,语言代码中应当尽可能选择地区中立的语言标签。
|
||||||
|
|
||||||
|
例如,对于简体中文和繁体中文,我们使用 `zh-Hans`和 `zh-Hant` 作为语言代码,而不是 `zh-CN` 和 `zh-TW`。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>关于本地化键和本地化文件后缀</summary>
|
||||||
|
|
||||||
|
本地化文件后缀和本地化键用于为[本地化资源](#本地化资源)命名。
|
||||||
|
|
||||||
|
通常来说,本地化键就是这份本地化资源对应的语言代码,而本地化文件后缀是将语言代码中的 `-` 替换为 `_`,并加上一个前缀下划线得到的。
|
||||||
|
|
||||||
|
作为特例,对于默认的资源,本地化键为 `default`,本地化文件后缀为空。
|
||||||
|
|
||||||
|
由于[资源回退机制](#资源回退机制)的存在。
|
||||||
|
如果没有完全匹配当前语言环境的资源,HMCL 会根据当前环境的语言标签推导出一个搜索列表,根据该列表依次搜索资源。
|
||||||
|
|
||||||
|
我们建议在提供本地化资源时,总是提供默认资源 (对应 `default` 本地化键和空的本地化文件后缀),
|
||||||
|
以确保所有用户都能正常加载资源。
|
||||||
|
|
||||||
|
并且我们建议尽可能为本地化资源使用更宽泛的语言标签,使用户更不容易回退到默认资源上。
|
||||||
|
|
||||||
|
例如,如果你提供了一份简体中文的本地化资源,那么我们推荐使用 `zh` 作为本地化键,而不是更具体的 `zh-Hans`,
|
||||||
|
这样它会对于所有使用中文的用户生效,避免对于这些用户回退到默认资源上。
|
||||||
|
|
||||||
|
如果你想同时提供简体中文和繁体中文的资源,那么推荐对用户占比更高的资源使用更宽泛的 `zh` 作为本地化键,使其作为默认的中文资源,
|
||||||
|
而对用户占比更低的资源使用更具体的 `zh-Hans`/`zh-Hant` 作为本地化键。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
HMCL 会要求所有 Pull Request 在更新文档和本地化资源时同步更新所有**主要**支持的语言对应的资源。
|
HMCL 会要求所有 Pull Request 在更新文档和本地化资源时同步更新所有**主要**支持的语言对应的资源。
|
||||||
如果 PR 作者对相关语言并不了解,那么可以直接在评论中提出翻译请求,
|
如果 PR 作者对相关语言并不了解,那么可以直接在评论中提出翻译请求,
|
||||||
@@ -56,7 +103,8 @@ HMCL 欢迎任何人参与翻译和贡献。但是维护更多语言的翻译需
|
|||||||
如果你想为 HMCL 添加新的语言支持,请从翻译 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties) 开始。
|
如果你想为 HMCL 添加新的语言支持,请从翻译 [`I18N.properties`](../HMCL/src/main/resources/assets/lang/I18N.properties) 开始。
|
||||||
HMCL 的绝大多数文本都位于这个文件中,翻译此文件就能翻译整个界面。
|
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)中查询这个语言对应的两字母或三字母语言标签。
|
作为翻译的第一步,请从[这张表格](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes)中查询这个语言对应的两字母或三字母语言标签。
|
||||||
例如,英语的语言标签为 `en`。
|
例如,英语的语言标签为 `en`。
|
||||||
|
|||||||
Reference in New Issue
Block a user