优化自动切换游戏语言功能 (#4353)

This commit is contained in:
Glavo 2025-08-31 14:57:23 +08:00 committed by GitHub
parent 30f47704f1
commit 86085031f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 124 additions and 64 deletions

View File

@ -21,16 +21,18 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
@ -59,63 +61,83 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists()) {
if (config().isDisableAutoGameOptions())
return;
}
if (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
Path runDir = repository.getRunDirectory(version.getId()).toPath();
Path optionsFile = runDir.resolve("options.txt");
Path configFolder = runDir.resolve("config");
if (Files.exists(optionsFile))
return;
if (Files.isDirectory(configFolder)) {
try (Stream<Path> stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) {
if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file))))
return;
} catch (IOException e) {
LOG.warning("Failed to visit config folder", e);
}
}
Locale locale = Locale.getDefault();
if (Locales.isEnglish(locale))
return;
/*
1.0- 没有语言选项遇到这些版本时不设置
1.1 ~ 5 zh_CN 时正常zh_cn 时崩溃最后两位字母必须大写否则将会 NPE 崩溃
1.6 ~ 10 zh_CN 时正常zh_cn 时自动切换为英文
1.11 ~ 12zh_cn 时正常zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
1.13+ zh_cn 时正常zh_CN 时自动切换为英文
* 1.0 : No language option, do not set for these versions
* 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash)
* 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0)
return;
String lang;
if (I18n.isUseChinese()) {
lang = "zh_CN";
} else if (Locale.getDefault().getLanguage().equals("lzh")) {
lang = "lzh";
} else {
String lang = normalizedLanguageTag(locale, gameVersion);
if (lang.isEmpty())
return;
}
if (gameVersion.compareTo("1.11") >= 0) {
if (gameVersion.compareTo("1.11") >= 0)
lang = lang.toLowerCase(Locale.ROOT);
}
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
Files.createDirectories(optionsFile.getParent());
Files.writeString(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
}
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
String language = locale.getLanguage();
String region = locale.getCountry();
switch (language) {
case "zh":
case "cmn":
if (Locales.isSimplifiedChinese(locale))
return "zh_CN";
if (gameVersion.compareTo("1.16") >= 0
&& (region.equals("HK") || region.equals("MO")))
return "zh_HK";
return "zh_TW";
case "ru":
return "ru_RU";
case "uk":
return "uk_UA";
case "es":
return "es_ES";
case "ja":
return "ja_JP";
case "lzh":
return gameVersion.compareTo("1.16") >= 0
? "lzh"
: "";
default:
return "";
}
return false;
}
@Override

View File

@ -186,6 +186,9 @@ public final class Config implements Observable {
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@SerializedName("disableAutoGameOptions")
private BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false);
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@ -631,6 +634,18 @@ public final class Config implements Observable {
this.addedLittleSkin.set(addedLittleSkin);
}
public BooleanProperty disableAutoGameOptionsProperty() {
return disableAutoGameOptions;
}
public boolean isDisableAutoGameOptions() {
return disableAutoGameOptions.get();
}
public void setDisableAutoGameOptions(boolean disableAutoGameOptions) {
this.disableAutoGameOptions.set(disableAutoGameOptions);
}
public int getConfigVersion() {
return configVersion.get();
}

View File

@ -70,6 +70,8 @@ public final class SettingsPage extends SettingsView {
// ==== Languages ====
cboLanguage.getItems().setAll(Locales.LOCALES);
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
// ====
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());

View File

@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
@ -50,6 +51,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final OptionToggleButton disableAutoGameOptionsPane;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate;
@ -193,6 +195,13 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(languagePane);
}
{
disableAutoGameOptionsPane = new OptionToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
BorderPane debugPane = new BorderPane();

View File

@ -76,15 +76,15 @@ public final class Locales {
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE);
/**
* Traditional Chinese
*/
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant"));
/**
* Simplified Chinese
* Chinese (Simplified)
*/
public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", Locale.forLanguageTag("zh-Hans"));
/**
* Chinese (Traditional)
*/
public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant"));
/**
* Wenyan (Classical Chinese)
*/
@ -133,7 +133,27 @@ public final class Locales {
}
public static boolean isChinese(Locale locale) {
return locale.getLanguage().equals("zh") || locale.getLanguage().equals("lzh");
switch (locale.getLanguage()) {
case "zh":
case "lzh":
case "cmn":
return true;
default:
return false;
}
}
public static boolean isSimplifiedChinese(Locale locale) {
if (locale.getLanguage().equals("zh") || locale.getLanguage().equals("cmn")) {
String script = locale.getScript();
if (script.isEmpty()) {
String region = locale.getCountry();
return region.isEmpty() || region.equals("CN") || region.equals("SG") || region.equals("MY");
} else
return script.equals("Hans");
} else {
return false;
}
}
@JsonAdapter(SupportedLocale.TypeAdapter.class)
@ -166,26 +186,15 @@ public final class Locales {
ResourceBundle bundle = resourceBundle;
if (resourceBundle == null) {
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() {
resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() {
@Override
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
if (locale.getLanguage().equals("zh")) {
boolean simplified;
String script = locale.getScript();
String region = locale.getCountry();
if (script.isEmpty())
simplified = region.equals("CN") || region.equals("SG");
else
simplified = script.equals("Hans");
if (simplified) {
return List.of(
Locale.SIMPLIFIED_CHINESE,
Locale.CHINESE,
Locale.ROOT
);
}
if (isSimplifiedChinese(locale)) {
return List.of(
Locale.SIMPLIFIED_CHINESE,
Locale.CHINESE,
Locale.ROOT
);
}
if (locale.getLanguage().equals("lzh")) {
@ -203,7 +212,6 @@ public final class Locales {
return super.getCandidateLocales(baseName, locale);
}
});
resourceBundle = bundle;
}
return bundle;

View File

@ -1338,6 +1338,7 @@ settings.launcher=Launcher Settings
settings.launcher.appearance=Appearance
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
settings.launcher.debug=Debug
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Threads
settings.launcher.download.threads.auto=Automatically Determine

View File

@ -1057,6 +1057,7 @@ settings.launcher=啟設
settings.launcher.appearance=驛式
settings.launcher.common_path.tooltip=啟者將諸遊戲資源及相依庫檔聚於此。若遊戲目錄自有,則不用公庫。
settings.launcher.debug=
settings.launcher.disable_auto_game_options=不易戲文
settings.launcher.download=
settings.launcher.download.threads=執緒數
settings.launcher.download.threads.auto=從宜

View File

@ -1128,6 +1128,7 @@ settings.launcher=啟動器設定
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。
settings.launcher.debug=除錯
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
settings.launcher.download=下載
settings.launcher.download.threads=執行緒數
settings.launcher.download.threads.auto=自動選取執行緒數

View File

@ -1138,6 +1138,7 @@ settings.launcher=启动器设置
settings.launcher.appearance=外观
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。
settings.launcher.debug=调试
settings.launcher.disable_auto_game_options=不自动切换游戏语言
settings.launcher.download=下载
settings.launcher.download.threads=线程数
settings.launcher.download.threads.auto=自动选择线程数