在 LocaleUtils 中添加更多工具方法 (#4390)

This commit is contained in:
Glavo
2025-09-04 15:51:17 +08:00
committed by GitHub
parent 3d63523f25
commit 7576bf6a01
7 changed files with 247 additions and 92 deletions

View File

@@ -30,4 +30,5 @@ dependencies {
compileOnlyApi(libs.jetbrains.annotations)
testImplementation(libs.jna.platform)
testImplementation(libs.jimfs)
}

View File

@@ -17,34 +17,44 @@
*/
package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class LocaleUtils {
public final class LocaleUtils {
public static final Locale SYSTEM_DEFAULT = Locale.getDefault();
public static final Locale LOCALE_ZH_HANS = Locale.forLanguageTag("zh-Hans");
public static final Locale LOCALE_ZH_HANT = Locale.forLanguageTag("zh-Hant");
public static final String DEFAULT_LANGUAGE_KEY = "default";
/// Convert a locale to the language key.
///
/// The language key is in the format of BCP 47 language tag.
/// If the locale is the default locale (language is empty), "default" will be returned.
public static String toLanguageKey(Locale locale) {
if (locale.getLanguage().isEmpty())
return "default";
else
return locale.toLanguageTag();
return locale.getLanguage().isEmpty()
? DEFAULT_LANGUAGE_KEY
: locale.stripExtensions().toLanguageTag();
}
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
}
public static String getScript(Locale locale) {
/// Get the script of the locale. If the script is empty and the language is Chinese,
/// the script will be inferred based on the language, the region and the variant.
public static @NotNull String getScript(Locale locale) {
if (locale.getScript().isEmpty()) {
if (isChinese(locale)) {
if (CHINESE_LATN_VARIANTS.contains(locale.getVariant()))
@@ -59,6 +69,99 @@ public class LocaleUtils {
return locale.getScript();
}
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
return DefaultResourceBundleControl.INSTANCE.getCandidateLocales("", locale);
}
public static <T> @Nullable T getByCandidateLocales(Map<String, T> map, List<Locale> candidateLocales) {
for (Locale locale : candidateLocales) {
String key = toLanguageKey(locale);
if (map.containsKey(key))
return map.get(key);
}
return null;
}
/// Find all localized files in the given directory with the given base name and extension.
/// The file name should be in the format of `baseName[_languageTag].ext`.
///
/// @return A map of language key to file path.
public static @NotNull @Unmodifiable Map<String, Path> findAllLocalizedFiles(Path dir, String baseName, String ext) {
if (Files.isDirectory(dir)) {
String suffix = "." + ext;
String defaultName = baseName + suffix;
String noDefaultPrefix = baseName + "_";
try (Stream<Path> list = Files.list(dir)) {
var result = new LinkedHashMap<String, Path>();
list.forEach(file -> {
if (Files.isRegularFile(file)) {
String fileName = file.getFileName().toString();
if (fileName.equals(defaultName)) {
result.put(DEFAULT_LANGUAGE_KEY, file);
} else if (fileName.startsWith(noDefaultPrefix) && fileName.endsWith(suffix)) {
String languageKey = fileName.substring(noDefaultPrefix.length(), fileName.length() - suffix.length())
.replace('_', '-');
if (!languageKey.isEmpty())
result.put(languageKey, file);
}
}
});
return result;
} catch (IOException e) {
LOG.warning("Failed to list files in directory " + dir, e);
}
}
return Map.of();
}
/// Find all localized files in the given directory with the given base name and extensions.
/// The file name should be in the format of `baseName[_languageTag].ext`.
///
/// @return A map of language key to a map of extension to file path.
public static @NotNull @Unmodifiable Map<String, Map<String, Path>> findAllLocalizedFiles(Path dir, String baseName, Collection<String> exts) {
if (Files.isDirectory(dir)) {
try (Stream<Path> list = Files.list(dir)) {
var result = new LinkedHashMap<String, Map<String, Path>>();
list.forEach(file -> {
if (Files.isRegularFile(file)) {
String fileName = file.getFileName().toString();
if (!fileName.startsWith(baseName))
return;
String ext = StringUtils.substringAfterLast(fileName, '.');
if (!exts.contains(ext))
return;
String languageKey;
int defaultFileNameLength = baseName.length() + ext.length() + 1;
if (fileName.length() == defaultFileNameLength)
languageKey = DEFAULT_LANGUAGE_KEY;
else if (fileName.length() > defaultFileNameLength + 1 && fileName.charAt(baseName.length()) == '_')
languageKey = fileName.substring(baseName.length() + 1, fileName.length() - ext.length() - 1)
.replace('_', '-');
else
return;
result.computeIfAbsent(languageKey, key -> new HashMap<>())
.put(ext, file);
}
});
return result;
} catch (IOException e) {
LOG.warning("Failed to list files in directory " + dir, e);
}
}
return Map.of();
}
// ---
public static boolean isEnglish(Locale locale) {

View File

@@ -17,10 +17,17 @@
*/
package org.jackhuang.hmcl.util.i18n;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@@ -113,4 +120,58 @@ public final class LocaleUtilsTest {
assertEquals("Latn", LocaleUtils.getScript(Locale.forLanguageTag("zh-pinyin")));
}
@Test
public void testFindAllLocalizedFiles() throws IOException {
try (var testFs = Jimfs.newFileSystem(Configuration.unix())) {
Path testDir = testFs.getPath("/test-dir");
Files.createDirectories(testDir);
Files.createFile(testDir.resolve("meow.json"));
Files.createFile(testDir.resolve("meow_zh.json"));
Files.createFile(testDir.resolve("meow_zh_CN.json"));
Files.createFile(testDir.resolve("meow_zh_Hans.json"));
Files.createFile(testDir.resolve("meow_zh_Hans_CN.json"));
Files.createFile(testDir.resolve("meow_en.json"));
Files.createFile(testDir.resolve("meow_en.toml"));
Files.createFile(testDir.resolve("meow_.json"));
Files.createFile(testDir.resolve("meowmeow.json"));
Files.createFile(testDir.resolve("woem.json"));
Files.createFile(testDir.resolve("meow.txt"));
Files.createDirectories(testDir.resolve("subdir"));
Files.createDirectories(testDir.resolve("meow_en_US.json"));
Path notExistsDir = testFs.getPath("/not-exists");
Path emptyDir = testFs.getPath("/empty");
Files.createDirectories(emptyDir);
assertEquals(Map.of(), LocaleUtils.findAllLocalizedFiles(emptyDir, "meow", "json"));
assertEquals(Map.of(), LocaleUtils.findAllLocalizedFiles(emptyDir, "meow", Set.of("json", "toml")));
assertEquals(Map.of(), LocaleUtils.findAllLocalizedFiles(notExistsDir, "meow", "json"));
assertEquals(Map.of(), LocaleUtils.findAllLocalizedFiles(notExistsDir, "meow", Set.of("json", "toml")));
assertEquals(Map.of(
"default", testDir.resolve("meow.json"),
"zh", testDir.resolve("meow_zh.json"),
"zh-CN", testDir.resolve("meow_zh_CN.json"),
"zh-Hans", testDir.resolve("meow_zh_Hans.json"),
"zh-Hans-CN", testDir.resolve("meow_zh_Hans_CN.json"),
"en", testDir.resolve("meow_en.json")
),
LocaleUtils.findAllLocalizedFiles(testDir, "meow", "json"));
assertEquals(Map.of(
"default", Map.of("json", testDir.resolve("meow.json")),
"zh", Map.of("json", testDir.resolve("meow_zh.json")),
"zh-CN", Map.of("json", testDir.resolve("meow_zh_CN.json")),
"zh-Hans", Map.of("json", testDir.resolve("meow_zh_Hans.json")),
"zh-Hans-CN", Map.of("json", testDir.resolve("meow_zh_Hans_CN.json")),
"en", Map.of(
"json", testDir.resolve("meow_en.json"),
"toml", testDir.resolve("meow_en.toml")
)
),
LocaleUtils.findAllLocalizedFiles(testDir, "meow", Set.of("json", "toml")));
}
}
}