@@ -110,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.getISO2Language(locale)) {
|
return switch (LocaleUtils.getRootLanguage(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";
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import java.time.temporal.TemporalAccessor;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
@@ -117,11 +118,16 @@ public final class SupportedLocale {
|
|||||||
Locale currentLocale = this.getLocale();
|
Locale currentLocale = this.getLocale();
|
||||||
|
|
||||||
String language = currentLocale.getLanguage();
|
String language = currentLocale.getLanguage();
|
||||||
String script = currentLocale.getScript();
|
String subLanguage;
|
||||||
|
{
|
||||||
// Currently, HMCL does not support any locales with regions or variants, so they are not handled for now
|
String parentLanguage = LocaleUtils.getParentLanguage(language);
|
||||||
// String region = currentLocale.getCountry();
|
if (parentLanguage != null) {
|
||||||
// String variant = currentLocale.getDisplayVariant();
|
subLanguage = language;
|
||||||
|
language = parentLanguage;
|
||||||
|
} else {
|
||||||
|
subLanguage = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ResourceBundle localeNames = inLocale.getLocaleNamesBundle();
|
ResourceBundle localeNames = inLocale.getLocaleNamesBundle();
|
||||||
|
|
||||||
@@ -132,19 +138,21 @@ public final class SupportedLocale {
|
|||||||
LOG.warning("Failed to get localized name for language " + language, e);
|
LOG.warning("Failed to get localized name for language " + language, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (script.isEmpty()) {
|
// Currently, HMCL does not support any locales with regions or variants, so they are not handled for now
|
||||||
return languageDisplayName;
|
List<String> subTags = Stream.of(subLanguage, currentLocale.getScript())
|
||||||
}
|
.filter(it -> !it.isEmpty())
|
||||||
|
.map(it -> {
|
||||||
String scriptDisplayName = script;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scriptDisplayName = localeNames.getString(script);
|
return localeNames.getString(it);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LOG.warning("Failed to get localized name for script " + script, e);
|
LOG.warning("Failed to get localized name of " + it, e);
|
||||||
}
|
}
|
||||||
|
return it;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
return languageDisplayName + " (" + scriptDisplayName + ")";
|
return subTags.isEmpty()
|
||||||
|
? languageDisplayName
|
||||||
|
: languageDisplayName + " (" + String.join(", ", subTags) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceBundle getResourceBundle() {
|
public ResourceBundle getResourceBundle() {
|
||||||
@@ -245,8 +253,8 @@ public final class SupportedLocale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSameLanguage(SupportedLocale other) {
|
public boolean isSameLanguage(SupportedLocale other) {
|
||||||
return LocaleUtils.getISO2Language(this.getLocale())
|
return LocaleUtils.getRootLanguage(this.getLocale())
|
||||||
.equals(LocaleUtils.getISO2Language(other.getLocale()));
|
.equals(LocaleUtils.getRootLanguage(other.getLocale()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
||||||
|
|||||||
@@ -116,24 +116,26 @@ public final class LocaleUtils {
|
|||||||
: locale.stripExtensions().toLanguageTag();
|
: locale.stripExtensions().toLanguageTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull String getISO2Language(Locale locale) {
|
public static @NotNull String getRootLanguage(Locale locale) {
|
||||||
String language = locale.getLanguage();
|
return getRootLanguage(locale.getLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - If `language` is a sublanguage of a [macrolanguage](https://en.wikipedia.org/wiki/ISO_639_macrolanguage),
|
||||||
|
/// return the macrolanguage;
|
||||||
|
/// - If `language` is an ISO 639 alpha-3 language code and there is a corresponding ISO 639 alpha-2 language code, return the ISO 639 alpha-2 code;
|
||||||
|
/// - If `language` is empty, return `en`;
|
||||||
|
/// - Otherwise, return the `language`.
|
||||||
|
public static @NotNull String getRootLanguage(String language) {
|
||||||
if (language.isEmpty()) return "en";
|
if (language.isEmpty()) return "en";
|
||||||
if (language.length() <= 2)
|
if (language.length() <= 2)
|
||||||
return language;
|
return language;
|
||||||
|
|
||||||
String lang = language;
|
String iso2 = mapToISO2Language(language);
|
||||||
while (lang != null) {
|
if (iso2 != null)
|
||||||
if (lang.length() <= 2)
|
return iso2;
|
||||||
return lang;
|
|
||||||
else {
|
String parent = getParentLanguage(language);
|
||||||
String iso1 = mapToISO2Language(lang);
|
return parent != null ? parent : language;
|
||||||
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,
|
/// Get the script of the locale. If the script is empty and the language is Chinese,
|
||||||
@@ -180,66 +182,52 @@ public final class LocaleUtils {
|
|||||||
|
|
||||||
ArrayList<Locale> result = new ArrayList<>();
|
ArrayList<Locale> result = new ArrayList<>();
|
||||||
do {
|
do {
|
||||||
List<String> languages;
|
String currentLanguage;
|
||||||
|
|
||||||
if (language.isEmpty()) {
|
if (language.length() <= 2) {
|
||||||
result.add(Locale.ROOT);
|
currentLanguage = language;
|
||||||
break;
|
|
||||||
} else if (language.length() <= 2) {
|
|
||||||
languages = List.of(language);
|
|
||||||
} else {
|
} else {
|
||||||
String iso1Language = mapToISO2Language(language);
|
String iso2 = mapToISO2Language(language);
|
||||||
languages = iso1Language != null
|
currentLanguage = iso2 != null
|
||||||
? List.of(language, iso1Language)
|
? iso2
|
||||||
: List.of(language);
|
: language;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCandidateLocales(result, languages, script, region, variants);
|
addCandidateLocales(result, currentLanguage, script, region, variants);
|
||||||
} while ((language = getParentLanguage(language)) != null);
|
} while ((language = getParentLanguage(language)) != null);
|
||||||
|
|
||||||
|
result.add(Locale.ROOT);
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addCandidateLocales(ArrayList<Locale> list,
|
private static void addCandidateLocales(ArrayList<Locale> list,
|
||||||
List<String> languages,
|
String language,
|
||||||
String script,
|
String script,
|
||||||
String region,
|
String region,
|
||||||
List<String> variants) {
|
List<String> variants) {
|
||||||
if (!variants.isEmpty()) {
|
if (!variants.isEmpty()) {
|
||||||
for (String v : variants) {
|
for (String v : variants) {
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, script, region, v));
|
list.add(getInstance(language, script, region, v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!region.isEmpty()) {
|
if (!region.isEmpty()) {
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, script, region, ""));
|
list.add(getInstance(language, script, region, ""));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!script.isEmpty()) {
|
if (!script.isEmpty()) {
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, script, "", ""));
|
list.add(getInstance(language, script, "", ""));
|
||||||
}
|
|
||||||
if (!variants.isEmpty()) {
|
if (!variants.isEmpty()) {
|
||||||
for (String v : variants) {
|
for (String v : variants) {
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, "", region, v));
|
list.add(getInstance(language, "", region, v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!region.isEmpty()) {
|
if (!region.isEmpty()) {
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, "", region, ""));
|
list.add(getInstance(language, "", region, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (String language : languages) {
|
|
||||||
list.add(getInstance(language, "", "", ""));
|
list.add(getInstance(language, "", "", ""));
|
||||||
}
|
|
||||||
|
|
||||||
if (languages.contains("zh")) {
|
if (language.equals("zh")) {
|
||||||
if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) {
|
if (list.contains(LocaleUtils.LOCALE_ZH_HANT) && !list.contains(Locale.TRADITIONAL_CHINESE)) {
|
||||||
int chineseIdx = list.indexOf(Locale.CHINESE);
|
int chineseIdx = list.indexOf(Locale.CHINESE);
|
||||||
if (chineseIdx >= 0)
|
if (chineseIdx >= 0)
|
||||||
@@ -353,25 +341,26 @@ public final class LocaleUtils {
|
|||||||
// ---
|
// ---
|
||||||
|
|
||||||
/// Map ISO 639 alpha-3 language codes to ISO 639 alpha-2 language codes.
|
/// Map ISO 639 alpha-3 language codes to ISO 639 alpha-2 language codes.
|
||||||
|
/// Returns `null` if there is no corresponding ISO 639 alpha-2 language code.
|
||||||
public static @Nullable String mapToISO2Language(String iso3Language) {
|
public static @Nullable String mapToISO2Language(String iso3Language) {
|
||||||
return iso3To2.get(iso3Language);
|
return iso3To2.get(iso3Language);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If `language` is a sublanguage of a [macrolanguage](https://en.wikipedia.org/wiki/ISO_639_macrolanguage),
|
||||||
|
/// return the macrolanguage; otherwise, return `null`.
|
||||||
public static @Nullable String getParentLanguage(String language) {
|
public static @Nullable String getParentLanguage(String language) {
|
||||||
return !language.isEmpty()
|
return subLanguageToParent.get(language);
|
||||||
? subLanguageToParent.getOrDefault(language, "")
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEnglish(Locale locale) {
|
public static boolean isEnglish(Locale locale) {
|
||||||
return "en".equals(getISO2Language(locale));
|
return "en".equals(getRootLanguage(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(getISO2Language(locale));
|
return "zh".equals(getRootLanguage(locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocaleUtils() {
|
private LocaleUtils() {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
zh,cmn,lzh,cdo,cjy,cpx,czh,gan,hak,hsn,mnp,nan,wuu,yue
|
zh,cdo,cjy,cmn,cnp,cpx,csp,czh,czo,gan,hak,hnm,hsn,luh,lzh,mnp,nan,sjc,wuu,yue
|
||||||
|
@@ -62,7 +62,7 @@ public final class LocaleUtilsTest {
|
|||||||
assertCandidateLocales("zh-Latn", List.of("zh-Latn", "zh", "zh-CN", "und"));
|
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-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("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("zho", List.of("zh-Hans", "zh-CN", "zh", "und"));
|
||||||
assertCandidateLocales("lzh", List.of("lzh-Hant", "lzh", "zh-Hant", "zh-TW", "zh", "zh-CN", "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-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"));
|
assertCandidateLocales("lzh-Hans", List.of("lzh-Hans", "lzh", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
@@ -71,23 +71,23 @@ public final class LocaleUtilsTest {
|
|||||||
assertCandidateLocales("yue", List.of("yue-Hans", "yue", "zh-Hans", "zh-CN", "zh", "und"));
|
assertCandidateLocales("yue", List.of("yue-Hans", "yue", "zh-Hans", "zh-CN", "zh", "und"));
|
||||||
|
|
||||||
assertCandidateLocales("ja", List.of("ja", "und"));
|
assertCandidateLocales("ja", List.of("ja", "und"));
|
||||||
|
assertCandidateLocales("jpn", List.of("ja", "und"));
|
||||||
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
|
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
|
||||||
assertCandidateLocales("jpn", List.of("jpn", "ja", "und"));
|
assertCandidateLocales("jpn-JP", List.of("ja-JP", "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("eng", List.of("en", "und"));
|
||||||
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
|
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
|
||||||
assertCandidateLocales("eng", List.of("eng", "en", "und"));
|
assertCandidateLocales("eng-US", List.of("en-US", "en", "und"));
|
||||||
assertCandidateLocales("eng-US", List.of("eng-US", "en-US", "eng", "en", "und"));
|
|
||||||
|
|
||||||
assertCandidateLocales("es", List.of("es", "und"));
|
assertCandidateLocales("es", List.of("es", "und"));
|
||||||
assertCandidateLocales("spa", List.of("spa", "es", "und"));
|
assertCandidateLocales("spa", List.of("es", "und"));
|
||||||
|
|
||||||
assertCandidateLocales("ru", List.of("ru", "und"));
|
assertCandidateLocales("ru", List.of("ru", "und"));
|
||||||
assertCandidateLocales("rus", List.of("rus", "ru", "und"));
|
assertCandidateLocales("rus", List.of("ru", "und"));
|
||||||
|
|
||||||
assertCandidateLocales("uk", List.of("uk", "und"));
|
assertCandidateLocales("uk", List.of("uk", "und"));
|
||||||
assertCandidateLocales("ukr", List.of("ukr", "uk", "und"));
|
assertCandidateLocales("ukr", List.of("uk", "und"));
|
||||||
|
|
||||||
assertCandidateLocales("und", List.of("en", "und"));
|
assertCandidateLocales("und", List.of("en", "und"));
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ public final class LocaleUtilsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMapToISO2Language() throws IOException {
|
public void testMapToISO2Language() {
|
||||||
assertEquals("en", LocaleUtils.mapToISO2Language("eng"));
|
assertEquals("en", LocaleUtils.mapToISO2Language("eng"));
|
||||||
assertEquals("es", LocaleUtils.mapToISO2Language("spa"));
|
assertEquals("es", LocaleUtils.mapToISO2Language("spa"));
|
||||||
assertEquals("ja", LocaleUtils.mapToISO2Language("jpn"));
|
assertEquals("ja", LocaleUtils.mapToISO2Language("jpn"));
|
||||||
@@ -207,4 +207,17 @@ public final class LocaleUtilsTest {
|
|||||||
assertNull(LocaleUtils.mapToISO2Language("lzh"));
|
assertNull(LocaleUtils.mapToISO2Language("lzh"));
|
||||||
assertNull(LocaleUtils.mapToISO2Language("tlh"));
|
assertNull(LocaleUtils.mapToISO2Language("tlh"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetParentLanguage() {
|
||||||
|
assertEquals("zh", LocaleUtils.getParentLanguage("cmn"));
|
||||||
|
assertEquals("zh", LocaleUtils.getParentLanguage("yue"));
|
||||||
|
assertEquals("zh", LocaleUtils.getParentLanguage("lzh"));
|
||||||
|
|
||||||
|
assertNull(LocaleUtils.getParentLanguage(""));
|
||||||
|
assertNull(LocaleUtils.getParentLanguage("en"));
|
||||||
|
assertNull(LocaleUtils.getParentLanguage("eng"));
|
||||||
|
assertNull(LocaleUtils.getParentLanguage("zh"));
|
||||||
|
assertNull(LocaleUtils.getParentLanguage("zho"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Languages
|
# Languages
|
||||||
lzh=Classical Chinese
|
lzh=Classical
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
Qabs=Upside down
|
Qabs=Upside Down
|
||||||
|
|||||||
@@ -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
|
||||||
|
en=英吉利文
|
||||||
@@ -178,16 +178,11 @@ HMCL 的维护者会替你完成其他步骤。
|
|||||||
对于能够混合的资源 (例如 `.properties` 文件),HMCL 会根据此列表的优先级混合资源;
|
对于能够混合的资源 (例如 `.properties` 文件),HMCL 会根据此列表的优先级混合资源;
|
||||||
对于难以混合的资源 (例如字体文件),HMCL 会根据此列表加载找到的最高优先级的资源。
|
对于难以混合的资源 (例如字体文件),HMCL 会根据此列表加载找到的最高优先级的资源。
|
||||||
|
|
||||||
如果当前语言使用 ISO 639-3 标准的三字符标签,那么 HMCL 也会尝试搜索其对应的 ISO 639-1 两字符标签所对应的资源。
|
如果当前语言使用 ISO 639 标准的三字符标签,但同时也存在对应的两字符标签,那么 HMCL 会将其映射至两字符后再搜索资源。
|
||||||
如果一个语言没有两字符标签,但其对应的宏语言存在两字符标签,那么 HMCL 会搜索对应的宏语言的两字符标签的资源。
|
|
||||||
|
|
||||||
例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会根据以下列表的顺序搜索对应的本地化资源:
|
例如,如果当前环境的语言标签为 `eng-US`,那么 HMCL 会将其映射至 `en-US` 后再根据上述规则搜索本地化资源。
|
||||||
|
|
||||||
1. `eng-US`
|
如果当前语言是一个 [ISO 639 宏语言](https://en.wikipedia.org/wiki/ISO_639_macrolanguage)的子语言,那么 HMCL 也会搜索宏语言对应的资源。
|
||||||
2. `en-US`
|
|
||||||
3. `eng`
|
|
||||||
4. `en`
|
|
||||||
5. `und`
|
|
||||||
|
|
||||||
### 对于中文的额外规则
|
### 对于中文的额外规则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user