From bd9ae189f83e33a6977bbe056774c851e96fe0a7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 21 Sep 2025 15:15:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=9C=AC=E5=9C=B0=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20(#4525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/util/i18n/Locales.java | 4 +- .../i18n/DefaultResourceBundleControl.java | 64 +------- .../jackhuang/hmcl/util/i18n/LocaleUtils.java | 154 ++++++++++++++++-- .../hmcl/util/i18n/LocaleUtilsTest.java | 5 +- docs/Localization_zh.md | 24 +-- 5 files changed, 158 insertions(+), 93 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java index 2333f462e..e0bfe99f8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java @@ -142,9 +142,9 @@ public final class Locales { } Locale inJavaLocale = inLocale.getLocale(); - if (LocaleUtils.isISO3Language(inJavaLocale.getLanguage())) { + if (inJavaLocale.getLanguage().length() > 2) { String iso1 = LocaleUtils.getISO1Language(inJavaLocale); - if (LocaleUtils.isISO1Language(iso1)) { + if (iso1.length() <= 2) { Locale.Builder builder = new Locale.Builder() .setLocale(inJavaLocale) .setLanguage(iso1); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java index 259b47159..75d8880a9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/DefaultResourceBundleControl.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.util.i18n; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; @@ -31,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, /// `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 [supported][LocaleUtils#toISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.), +/// - For all [supported][LocaleUtils#mapToISO1Language(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. /// /// @author Glavo @@ -42,67 +41,8 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control { public DefaultResourceBundleControl() { } - private static List ensureEditable(List list) { - return list instanceof ArrayList - ? list - : new ArrayList<>(list); - } - @Override public List getCandidateLocales(String baseName, Locale locale) { - if (locale.getLanguage().isEmpty()) - return getCandidateLocales(baseName, Locale.ENGLISH); - else if (LocaleUtils.isChinese(locale)) { - String script = locale.getScript(); - - if (script.isEmpty()) { - script = LocaleUtils.getScript(locale); - if (!script.isEmpty()) - return getCandidateLocales(baseName, new Locale.Builder() - .setLocale(locale) - .setScript(script) - .build()); - } - } - - String language = locale.getLanguage(); - - List locales = super.getCandidateLocales(baseName, locale); - - // Is ISO 639-3 language tag - if (language.length() == 3) { - String iso1 = LocaleUtils.toISO1Language(locale.getLanguage()); - - if (iso1.length() == 2) { - locales = ensureEditable(locales); - locales.removeIf(it -> !it.getLanguage().equals(language)); - - locales.addAll(getCandidateLocales(baseName, new Locale.Builder() - .setLocale(locale) - .setLanguage(iso1) - .build())); - } - } else if (language.equals("zh")) { - if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) { - locales = ensureEditable(locales); - int chineseIdx = locales.indexOf(Locale.CHINESE); - if (chineseIdx >= 0) - locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE); - } - - if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) { - int chineseIdx = locales.indexOf(Locale.CHINESE); - - if (chineseIdx >= 0) { - locales = ensureEditable(locales); - if (locales.contains(LocaleUtils.LOCALE_ZH_HANS)) - locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE); - else - locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE); - } - } - } - - return locales; + return LocaleUtils.getCandidateLocales(locale); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java index 796845304..2caeba500 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -42,6 +44,16 @@ public final class LocaleUtils { public static final String DEFAULT_LANGUAGE_KEY = "default"; + private static Locale getInstance(String language, String script, String region, + String variant) { + Locale.Builder builder = new Locale.Builder(); + if (!language.isEmpty()) builder.setLanguage(language); + if (!script.isEmpty()) builder.setScript(script); + if (!region.isEmpty()) builder.setRegion(region); + if (!variant.isEmpty()) builder.setVariant(variant); + return builder.build(); + } + /// Convert a locale to the language key. /// /// The language key is in the format of BCP 47 language tag. @@ -52,18 +64,24 @@ public final class LocaleUtils { : locale.stripExtensions().toLanguageTag(); } - public static boolean isISO1Language(String language) { - return language.length() == 2; - } - - public static boolean isISO3Language(String language) { - return language.length() == 3; - } - public static @NotNull String getISO1Language(Locale locale) { String language = locale.getLanguage(); if (language.isEmpty()) return "en"; - return isISO3Language(language) ? toISO1Language(language) : language; + if (language.length() <= 2) + return language; + + String lang = language; + while (lang != null) { + if (lang.length() <= 2) + return lang; + else { + String iso1 = mapToISO1Language(lang); + if (iso1 != null) + return iso1; + } + lang = getParentLanguage(lang); + } + return language; } /// Get the script of the locale. If the script is empty and the language is Chinese, @@ -83,10 +101,108 @@ public final class LocaleUtils { return locale.getScript(); } + private static final ConcurrentMap> CANDIDATE_LOCALES = new ConcurrentHashMap<>(); + public static @NotNull List getCandidateLocales(Locale locale) { - return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale); + return CANDIDATE_LOCALES.computeIfAbsent(locale, LocaleUtils::createCandidateLocaleList); } + // ------------- + + private static List createCandidateLocaleList(Locale locale) { + String language = locale.getLanguage(); + if (language.isEmpty()) + return List.of(Locale.ENGLISH, Locale.ROOT); + + String script = getScript(locale); + String region = locale.getCountry(); + List variants = locale.getVariant().isEmpty() + ? List.of() + : List.of(locale.getVariant().split("[_\\-]")); + + ArrayList result = new ArrayList<>(); + do { + List languages; + + if (language.isEmpty()) { + result.add(Locale.ROOT); + break; + } else if (language.length() <= 2) { + languages = List.of(language); + } else { + String iso1Language = mapToISO1Language(language); + languages = iso1Language != null + ? List.of(language, iso1Language) + : List.of(language); + } + + addCandidateLocales(result, languages, script, region, variants); + } while ((language = getParentLanguage(language)) != null); + + return List.copyOf(result); + } + + private static void addCandidateLocales(ArrayList list, + List languages, + String script, + String region, + List variants) { + if (!variants.isEmpty()) { + for (String v : variants) { + for (String language : languages) { + list.add(getInstance(language, script, region, v)); + } + } + } + if (!region.isEmpty()) { + for (String language : languages) { + list.add(getInstance(language, script, region, "")); + } + } + if (!script.isEmpty()) { + for (String language : languages) { + list.add(getInstance(language, script, "", "")); + } + if (!variants.isEmpty()) { + for (String v : variants) { + for (String language : languages) { + list.add(getInstance(language, "", region, v)); + } + } + } + if (!region.isEmpty()) { + for (String language : languages) { + list.add(getInstance(language, "", region, "")); + } + } + } + + for (String language : languages) { + list.add(getInstance(language, "", "", "")); + } + + if (languages.contains("zh")) { + if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) { + int chineseIdx = list.indexOf(Locale.CHINESE); + if (chineseIdx >= 0) + list.add(chineseIdx, Locale.TRADITIONAL_CHINESE); + } + + if (!list.contains(Locale.SIMPLIFIED_CHINESE)) { + int chineseIdx = list.indexOf(Locale.CHINESE); + + if (chineseIdx >= 0) { + if (list.contains(LocaleUtils.LOCALE_ZH_HANS)) + list.add(chineseIdx, Locale.SIMPLIFIED_CHINESE); + else + list.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE); + } + } + } + } + + // ------------- + public static @Nullable T getByCandidateLocales(Map map, List candidateLocales) { for (Locale locale : candidateLocales) { String key = toLanguageKey(locale); @@ -178,17 +294,25 @@ public final class LocaleUtils { // --- - /// Try to convert ISO 639-3 language codes to ISO 639-1 language codes. - public static String toISO1Language(String languageTag) { - return switch (languageTag) { + /// Map ISO 639-3 language codes to ISO 639-1 language codes. + public static @Nullable String mapToISO1Language(String iso3Language) { + return switch (iso3Language) { case "eng" -> "en"; case "spa" -> "es"; case "jpa" -> "ja"; case "rus" -> "ru"; case "ukr" -> "uk"; - case "zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh", + case "zho" -> "zh"; + default -> null; + }; + } + + public static @Nullable String getParentLanguage(String language) { + return switch (language) { + case "cmn", "lzh", "cdo", "cjy", "cpx", "czh", "gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh"; - default -> languageTag; + case "" -> null; + default -> ""; }; } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java index 18210d4f1..2e4678368 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/i18n/LocaleUtilsTest.java @@ -62,6 +62,7 @@ public final class LocaleUtilsTest { assertCandidateLocales("zh-Latn", List.of("zh-Latn", "zh", "zh-CN", "und")); assertCandidateLocales("zh-Latn-CN", List.of("zh-Latn-CN", "zh-Latn", "zh-CN", "zh", "und")); assertCandidateLocales("zh-pinyin", List.of("zh-Latn-pinyin", "zh-Latn", "zh-pinyin", "zh", "zh-CN", "und")); + assertCandidateLocales("zho", List.of("zho-Hans", "zh-Hans", "zho", "zh-CN", "zh", "und")); assertCandidateLocales("lzh", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und")); assertCandidateLocales("lzh-Hant", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "und")); assertCandidateLocales("lzh-Hans", List.of("lzh-Hans", "lzh", "zh-Hans", "zh-CN", "zh", "und")); @@ -72,12 +73,12 @@ public final class LocaleUtilsTest { assertCandidateLocales("ja", List.of("ja", "und")); assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und")); assertCandidateLocales("jpa", List.of("jpa", "ja", "und")); - assertCandidateLocales("jpa-JP", List.of("jpa-JP", "jpa", "ja-JP", "ja", "und")); + assertCandidateLocales("jpa-JP", List.of("jpa-JP", "ja-JP", "jpa", "ja", "und")); assertCandidateLocales("en", List.of("en", "und")); assertCandidateLocales("en-US", List.of("en-US", "en", "und")); assertCandidateLocales("eng", List.of("eng", "en", "und")); - assertCandidateLocales("eng-US", List.of("eng-US", "eng", "en-US", "en", "und")); + assertCandidateLocales("eng-US", List.of("eng-US", "en-US", "eng", "en", "und")); assertCandidateLocales("es", List.of("es", "und")); assertCandidateLocales("spa", List.of("spa", "es", "und")); diff --git a/docs/Localization_zh.md b/docs/Localization_zh.md index e8b3b1a7e..59b821db4 100644 --- a/docs/Localization_zh.md +++ b/docs/Localization_zh.md @@ -10,16 +10,16 @@ HMCL 为多种语言提供本地化支持。 目前,HMCL 为这些语言提供支持: -| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | 支持状态 | 志愿者 | -|---------|-----------|------------|-----------|--------|-------------------------------------------| -| 英语 | `en` | (空) | `default` | **主要** | [Glavo](https://github.com/3gf8jv4dv) | -| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | **主要** | [Glavo](https://github.com/3gf8jv4dv) | -| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | **主要** | [Glavo](https://github.com/3gf8jv4dv) | -| 中文 (文言) | `lzh` | `_lzh` | `lzh` | 次要 | | -| 日语 | `ja` | `_ja` | `ja` | 次要 | | -| 西班牙语 | `es` | `_es` | `es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) | -| 俄语 | `ru` | `_ru` | `ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) | -| 乌克兰语 | `uk` | `_uk` | `uk` | 次要 | | +| 语言 | 语言标签 | 首选本地化文件后缀 | 首选本地化键 | [游戏语言文件](https://minecraft.wiki/w/Language) | 支持状态 | 志愿者 | +|---------|-----------|------------|-----------|---------------------------------------------|--------|-------------------------------------------| +| 英语 | `en` | (空) | `default` | `en_us` | **主要** | [Glavo](https://github.com/Glavo) | +| 中文 (简体) | `zh-Hans` | `_zh` | `zh` | `zh_cn` | **主要** | [Glavo](https://github.com/Glavo) | +| 中文 (繁体) | `zh-Hant` | `_zh_Hant` | `zh-Hant` | `zh_tw`
`zh_hk` | **主要** | [Glavo](https://github.com/Glavo) | +| 中文 (文言) | `lzh` | `_lzh` | `lzh` | `lzh` | 次要 | | +| 日语 | `ja` | `_ja` | `ja` | `ja_jp` | 次要 | | +| 西班牙语 | `es` | `_es` | `es` | `es_es` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) | +| 俄语 | `ru` | `_ru` | `ru` | `ru_ru` | 次要 | [3gf8jv4dv](https://github.com/3gf8jv4dv) | +| 乌克兰语 | `uk` | `_uk` | `uk` | `uk_ua` | 次要 | | HMCL 会要求所有 Pull Request 在更新文档和本地化资源时同步更新所有**主要**支持的语言对应的资源。 如果 PR 作者对相关语言并不了解,那么可以直接在评论中提出翻译请求, @@ -136,8 +136,8 @@ HMCL 的维护者会替你完成其他步骤。 例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会根据以下列表的顺序搜索对应的本地化资源: 1. `eng-US` -2. `eng` -3. `en-US` +2. `en-US` +3. `eng` 4. `en` 5. `und`