Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ public String getDisplayName(SupportedLocale inLocale) {
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,7 +30,7 @@
/// - 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
Expand All @@ -42,67 +41,8 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
public DefaultResourceBundleControl() {
}

private static List<Locale> ensureEditable(List<Locale> list) {
return list instanceof ArrayList<?>
? list
: new ArrayList<>(list);
}

@Override
public List<Locale> 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<Locale> 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);
}
}
154 changes: 139 additions & 15 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/util/i18n/LocaleUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
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;
Expand All @@ -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.
Expand All @@ -52,18 +64,24 @@ public static String toLanguageKey(Locale locale) {
: 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,
Expand All @@ -83,10 +101,108 @@ public static boolean isISO3Language(String language) {
return locale.getScript();
}

private static final ConcurrentMap<Locale, List<Locale>> CANDIDATE_LOCALES = new ConcurrentHashMap<>();

public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
return CANDIDATE_LOCALES.computeIfAbsent(locale, LocaleUtils::createCandidateLocaleList);
}

// -------------

private static List<Locale> 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<String> variants = locale.getVariant().isEmpty()
? List.of()
: List.of(locale.getVariant().split("[_\\-]"));

ArrayList<Locale> result = new ArrayList<>();
do {
List<String> 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<Locale> list,
List<String> languages,
String script,
String region,
List<String> 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 <T> @Nullable T getByCandidateLocales(Map<String, T> map, List<Locale> candidateLocales) {
for (Locale locale : candidateLocales) {
String key = toLanguageKey(locale);
Expand Down Expand Up @@ -178,17 +294,25 @@ else if (fileName.length() > defaultFileNameLength + 1 && fileName.charAt(baseNa

// ---

/// 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 -> "";
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public void testGetCandidateLocales() {
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"));
Expand All @@ -72,12 +73,12 @@ public void testGetCandidateLocales() {
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"));
Expand Down
24 changes: 12 additions & 12 deletions docs/Localization_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` <br/> `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 作者对相关语言并不了解,那么可以直接在评论中提出翻译请求,
Expand Down Expand Up @@ -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`

Expand Down