支持颠倒的英语 (#4527)
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle;
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputDirectory;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class CreateLanguageList extends DefaultTask {
|
||||
@InputDirectory
|
||||
public abstract DirectoryProperty getResourceBundleDir();
|
||||
|
||||
@Input
|
||||
public abstract Property<@NotNull String> getResourceBundleBaseName();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<@NotNull String> getAdditionalLanguages();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path inputDir = getResourceBundleDir().get().getAsFile().toPath();
|
||||
if (!Files.isDirectory(inputDir))
|
||||
throw new GradleException("Input directory not exists: " + inputDir);
|
||||
|
||||
|
||||
SortedSet<Locale> locales = new TreeSet<>(LocalizationUtils::compareLocale);
|
||||
locales.addAll(getAdditionalLanguages().getOrElse(List.of()).stream()
|
||||
.map(Locale::forLanguageTag)
|
||||
.toList());
|
||||
|
||||
String baseName = getResourceBundleBaseName().get();
|
||||
String suffix = ".properties";
|
||||
|
||||
try (var stream = Files.newDirectoryStream(inputDir, file -> {
|
||||
String fileName = file.getFileName().toString();
|
||||
return fileName.startsWith(baseName) && fileName.endsWith(suffix);
|
||||
})) {
|
||||
for (Path file : stream) {
|
||||
String fileName = file.getFileName().toString();
|
||||
if (fileName.length() == baseName.length() + suffix.length())
|
||||
locales.add(Locale.ENGLISH);
|
||||
else if (fileName.charAt(baseName.length()) == '_') {
|
||||
String localeName = fileName.substring(baseName.length() + 1, fileName.length() - suffix.length());
|
||||
|
||||
// TODO: Delete this if the I18N file naming is changed
|
||||
if (baseName.equals("I18N")) {
|
||||
if (localeName.equals("zh"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hant"));
|
||||
else if (localeName.equals("zh_CN"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hans"));
|
||||
else
|
||||
locales.add(Locale.forLanguageTag(localeName.replace('_', '-')));
|
||||
} else {
|
||||
if (localeName.equals("zh"))
|
||||
locales.add(Locale.forLanguageTag("zh-Hans"));
|
||||
else
|
||||
locales.add(Locale.forLanguageTag(localeName.replace('_', '-')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
Files.writeString(outputFile, locales.stream().map(locale -> '"' + locale.toLanguageTag() + '"')
|
||||
.collect(Collectors.joining(", ", "[", "]")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class CreateLocaleNamesResourceBundle extends DefaultTask {
|
||||
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getLanguagesFile();
|
||||
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
private static String mapToFileName(String base, String ext, Locale locale) {
|
||||
if (locale.equals(Locale.ENGLISH))
|
||||
return base + "." + ext;
|
||||
else if (locale.toLanguageTag().equals("zh-Hans"))
|
||||
return base + "_zh." + ext;
|
||||
else
|
||||
return base + "_" + locale.toLanguageTag().replace('-', '_') + "." + ext;
|
||||
}
|
||||
|
||||
private static final ResourceBundle.Control CONTROL = new ResourceBundle.Control() {
|
||||
};
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path languagesFile = getLanguagesFile().get().getAsFile().toPath();
|
||||
Path outputDir = getOutputDirectory().get().getAsFile().toPath();
|
||||
|
||||
if (Files.isDirectory(outputDir)) {
|
||||
Files.walkFileTree(outputDir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
|
||||
Files.deleteIfExists(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FileVisitResult postVisitDirectory(@NotNull Path dir, @Nullable IOException exc) throws IOException {
|
||||
if (!dir.equals(outputDir))
|
||||
Files.deleteIfExists(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
Files.deleteIfExists(outputDir);
|
||||
Files.createDirectories(outputDir);
|
||||
|
||||
List<Locale> supportedLanguages;
|
||||
try (var reader = Files.newBufferedReader(languagesFile)) {
|
||||
supportedLanguages = new Gson().fromJson(reader, new TypeToken<List<String>>() {
|
||||
}).stream()
|
||||
.map(Locale::forLanguageTag)
|
||||
.sorted(LocalizationUtils::compareLocale)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (!supportedLanguages.contains(Locale.ENGLISH))
|
||||
throw new GradleException("Missing english in supported languages: " + supportedLanguages);
|
||||
|
||||
// Ensure English is at the first position, this assumption will be used later
|
||||
if (!supportedLanguages.get(0).equals(Locale.ENGLISH)) {
|
||||
supportedLanguages = new ArrayList<>(supportedLanguages);
|
||||
supportedLanguages.remove(Locale.ENGLISH);
|
||||
supportedLanguages.add(0, Locale.ENGLISH);
|
||||
}
|
||||
|
||||
EnumMap<LocaleField, SortedSet<String>> names = new EnumMap<>(LocaleField.class);
|
||||
for (LocaleField field : LocaleField.values()) {
|
||||
names.put(field, supportedLanguages.stream()
|
||||
.map(field::get)
|
||||
.filter(it -> !it.isBlank())
|
||||
.collect(Collectors.toCollection(() -> new TreeSet<>(field))));
|
||||
}
|
||||
|
||||
Map<Locale, Properties> overrides = new HashMap<>();
|
||||
for (Locale currentLanguage : supportedLanguages) {
|
||||
InputStream overrideFile = CreateLocaleNamesResourceBundle.class.getResourceAsStream(
|
||||
mapToFileName("LocaleNamesOverride", "properties", currentLanguage));
|
||||
Properties overrideProperties = new Properties();
|
||||
if (overrideFile != null) {
|
||||
try (var reader = new InputStreamReader(overrideFile, StandardCharsets.UTF_8)) {
|
||||
overrideProperties.load(reader);
|
||||
}
|
||||
}
|
||||
overrides.put(currentLanguage, overrideProperties);
|
||||
}
|
||||
|
||||
Map<Locale, LocaleNames> allLocaleNames = new HashMap<>();
|
||||
|
||||
// For Upside Down English
|
||||
UpsideDownTranslate.Translator upsideDownTranslator = new UpsideDownTranslate.Translator();
|
||||
for (Locale currentLocale : supportedLanguages) {
|
||||
Properties currentOverrides = overrides.get(currentLocale);
|
||||
if (currentLocale.getLanguage().length() > 2 && currentOverrides.isEmpty()) {
|
||||
// The JDK does not provide localized texts for these languages
|
||||
continue;
|
||||
}
|
||||
|
||||
LocaleNames currentDisplayNames = new LocaleNames();
|
||||
|
||||
for (LocaleField field : LocaleField.values()) {
|
||||
SortedMap<String, String> nameToDisplayName = currentDisplayNames.getNameToDisplayName(field);
|
||||
|
||||
loop:
|
||||
for (String name : names.get(field)) {
|
||||
String displayName = currentOverrides.getProperty(name);
|
||||
|
||||
getDisplayName:
|
||||
if (displayName == null) {
|
||||
if (currentLocale.equals(UpsideDownTranslate.EN_QABS)) {
|
||||
String englishDisplayName = allLocaleNames.get(Locale.ENGLISH).getNameToDisplayName(field).get(name);
|
||||
if (englishDisplayName != null) {
|
||||
displayName = upsideDownTranslator.translate(englishDisplayName);
|
||||
break getDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
// Although it cannot correctly handle the inheritance relationship between languages,
|
||||
// we will not apply this function to sublanguages.
|
||||
List<Locale> candidateLocales = CONTROL.getCandidateLocales("", currentLocale);
|
||||
|
||||
for (Locale candidateLocale : candidateLocales) {
|
||||
Properties candidateOverride = overrides.get(candidateLocale);
|
||||
if (candidateOverride != null && candidateOverride.containsKey(name)) {
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
|
||||
displayName = field.getDisplayName(name, currentLocale);
|
||||
|
||||
// JDK does not have a built-in translation
|
||||
if (displayName.isBlank() || displayName.equals(name)) {
|
||||
continue loop;
|
||||
}
|
||||
|
||||
// If it is just a duplicate of the parent content, ignored it
|
||||
for (Locale candidateLocale : candidateLocales) {
|
||||
LocaleNames candidateLocaleNames = allLocaleNames.get(candidateLocale);
|
||||
if (candidateLocaleNames != null) {
|
||||
String candidateDisplayName = candidateLocaleNames.getNameToDisplayName(field).get(name);
|
||||
if (displayName.equals(candidateDisplayName)) {
|
||||
continue loop;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore it if the JDK falls back to English when querying the display name
|
||||
if (!currentLocale.equals(Locale.ENGLISH)
|
||||
&& displayName.equals(allLocaleNames.get(Locale.ENGLISH).getNameToDisplayName(field).get(name))) {
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
|
||||
nameToDisplayName.put(name, displayName);
|
||||
}
|
||||
}
|
||||
|
||||
allLocaleNames.put(currentLocale, currentDisplayNames);
|
||||
}
|
||||
|
||||
for (Map.Entry<Locale, LocaleNames> entry : allLocaleNames.entrySet()) {
|
||||
if (!entry.getValue().isEmpty()) {
|
||||
Path targetFile = outputDir.resolve(mapToFileName("LocaleNames", "properties", entry.getKey()));
|
||||
entry.getValue().writeTo(targetFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LocaleNames {
|
||||
private final EnumMap<LocaleField, SortedMap<String, String>> displayNames = new EnumMap<>(LocaleField.class);
|
||||
|
||||
LocaleNames() {
|
||||
for (LocaleField field : LocaleField.values()) {
|
||||
displayNames.put(field, new TreeMap<>(field));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return displayNames.values().stream().allMatch(Map::isEmpty);
|
||||
}
|
||||
|
||||
SortedMap<String, String> getNameToDisplayName(LocaleField field) {
|
||||
return displayNames.get(field);
|
||||
}
|
||||
|
||||
void writeTo(Path file) throws IOException {
|
||||
try (var writer = Files.newBufferedWriter(file, StandardOpenOption.CREATE_NEW)) {
|
||||
boolean firstBlock = true;
|
||||
|
||||
for (var entry : displayNames.entrySet()) {
|
||||
LocaleField field = entry.getKey();
|
||||
SortedMap<String, String> values = entry.getValue();
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
if (firstBlock)
|
||||
firstBlock = false;
|
||||
else
|
||||
writer.newLine();
|
||||
|
||||
writer.write("# " + field.blockHeader + "\n");
|
||||
|
||||
for (var nameToDisplay : values.entrySet()) {
|
||||
writer.write(nameToDisplay.getKey() + "=" + nameToDisplay.getValue() + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum LocaleField implements Comparator<String> {
|
||||
LANGUAGE("Languages") {
|
||||
@Override
|
||||
public String get(Locale locale) {
|
||||
return locale.getLanguage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName(String fieldValue, Locale inLocale) {
|
||||
return new Locale.Builder()
|
||||
.setLanguage(fieldValue)
|
||||
.build()
|
||||
.getDisplayLanguage(inLocale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String l1, String l2) {
|
||||
return LocalizationUtils.compareLanguage(l1, l2);
|
||||
}
|
||||
},
|
||||
SCRIPT("Scripts") {
|
||||
@Override
|
||||
public String get(Locale locale) {
|
||||
return locale.getScript();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName(String fieldValue, Locale inLocale) {
|
||||
return new Locale.Builder()
|
||||
.setScript(fieldValue)
|
||||
.build()
|
||||
.getDisplayScript(inLocale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
return LocalizationUtils.compareScript(s1, s2);
|
||||
}
|
||||
};
|
||||
|
||||
final String blockHeader;
|
||||
|
||||
LocaleField(String blockHeader) {
|
||||
this.blockHeader = blockHeader;
|
||||
}
|
||||
|
||||
public abstract String get(Locale locale);
|
||||
|
||||
public abstract String getDisplayName(String fieldValue, Locale inLocale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
|
||||
final class LocalizationUtils {
|
||||
public static final Map<String, String> subLanguageToParent;
|
||||
|
||||
static {
|
||||
InputStream input = LocalizationUtils.class.getResourceAsStream("sublanguages.csv");
|
||||
if (input == null)
|
||||
throw new GradleException("Missing sublanguages.csv file");
|
||||
|
||||
Map<String, String> map = new HashMap<>();
|
||||
try (input) {
|
||||
new String(input.readAllBytes()).lines()
|
||||
.filter(line -> !line.startsWith("#") && !line.isBlank())
|
||||
.forEach(line -> {
|
||||
String[] languages = line.split(",");
|
||||
if (languages.length < 2)
|
||||
throw new GradleException("Invalid line in sublanguages.csv: " + line);
|
||||
|
||||
String parent = languages[0];
|
||||
for (int i = 1; i < languages.length; i++) {
|
||||
map.put(languages[i], parent);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new GradleException("Failed to read sublanguages.csv", e);
|
||||
}
|
||||
subLanguageToParent = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private static List<String> resolveLanguage(String language) {
|
||||
List<String> langList = new ArrayList<>();
|
||||
|
||||
String lang = language;
|
||||
while (true) {
|
||||
langList.add(0, lang);
|
||||
|
||||
String parent = subLanguageToParent.get(lang);
|
||||
if (parent != null) {
|
||||
lang = parent;
|
||||
} else {
|
||||
return langList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int compareLanguage(String l1, String l2) {
|
||||
var list1 = resolveLanguage(l1);
|
||||
var list2 = resolveLanguage(l2);
|
||||
|
||||
int n = Math.min(list1.size(), list2.size());
|
||||
for (int i = 0; i < n; i++) {
|
||||
int c = list1.get(i).compareTo(list2.get(i));
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
return Integer.compare(list1.size(), list2.size());
|
||||
}
|
||||
|
||||
public static int compareScript(String s1, String s2) {
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
|
||||
public static int compareLocale(Locale l1, Locale l2) {
|
||||
int c = compareLanguage(l1.getLanguage(), l2.getLanguage());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = compareScript(l1.getScript(), l2.getScript());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = l1.getCountry().compareTo(l2.getCountry());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
c = l1.getVariant().compareTo(l2.getVariant());
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
return l1.toString().compareTo(l2.toLanguageTag());
|
||||
}
|
||||
|
||||
private LocalizationUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class UpsideDownTranslate extends DefaultTask {
|
||||
|
||||
static final Locale EN_QABS = Locale.forLanguageTag("en-Qabs");
|
||||
|
||||
private static final Map<String, String> PROPERTIES = Map.of(
|
||||
"datetime.format", "MMM d, yyyy, h:mm:ss a"
|
||||
);
|
||||
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getInputFile();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path inputFile = getInputFile().get().getAsFile().toPath();
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
|
||||
Properties english = new Properties();
|
||||
try (var reader = Files.newBufferedReader(inputFile)) {
|
||||
english.load(reader);
|
||||
}
|
||||
|
||||
Properties output = new Properties();
|
||||
Translator translator = new Translator();
|
||||
english.forEach((k, v) -> {
|
||||
if (PROPERTIES.containsKey(k.toString())) {
|
||||
output.setProperty(k.toString(), PROPERTIES.get(k.toString()));
|
||||
} else {
|
||||
output.put(k, translator.translate(v.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
try (var writer = Files.newBufferedWriter(outputFile)) {
|
||||
output.store(writer, "This file is automatically generated, please do not modify it manually");
|
||||
}
|
||||
}
|
||||
|
||||
static final class Translator {
|
||||
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
|
||||
|
||||
private static void putChars(char baseChar, String upsideDownChars) {
|
||||
for (int i = 0; i < upsideDownChars.length(); i++) {
|
||||
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void putChars(String baseChars, String upsideDownChars) {
|
||||
if (baseChars.length() != upsideDownChars.length()) {
|
||||
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
|
||||
}
|
||||
|
||||
for (int i = 0; i < baseChars.length(); i++) {
|
||||
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
|
||||
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
|
||||
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
|
||||
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
|
||||
}
|
||||
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("^%(\\d\\$)?(\\d+)?(\\.\\d+)?([sdf])");
|
||||
private static final Pattern XML_TAG_PATTERN = Pattern.compile("^<(?<tag>[a-zA-Z]+)( href=\"[^\"]*\")?>");
|
||||
|
||||
private final StringBuilder resultBuilder = new StringBuilder();
|
||||
|
||||
private void appendToLineBuilder(String input) {
|
||||
for (int i = 0; i < input.length(); ) {
|
||||
int ch = input.codePointAt(i);
|
||||
|
||||
if (ch == '%') {
|
||||
Matcher matcher = FORMAT_PATTERN.matcher(input).region(i, input.length());
|
||||
if (matcher.find()) {
|
||||
String formatString = matcher.group();
|
||||
resultBuilder.insert(0, formatString);
|
||||
i += formatString.length();
|
||||
continue;
|
||||
}
|
||||
} else if (ch == '<') {
|
||||
Matcher matcher = XML_TAG_PATTERN.matcher(input).region(i, input.length());
|
||||
if (matcher.find()) {
|
||||
String beginTag = matcher.group();
|
||||
String endTag = "</" + matcher.group(1) + ">";
|
||||
|
||||
int endTagOffset = input.indexOf(endTag, i + beginTag.length());
|
||||
if (endTagOffset > 0) {
|
||||
resultBuilder.insert(0, endTag);
|
||||
appendToLineBuilder(input.substring(i + beginTag.length(), endTagOffset));
|
||||
resultBuilder.insert(0, beginTag);
|
||||
|
||||
i = endTagOffset + endTag.length();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int udCh = MAPPER.getOrDefault(ch, ch);
|
||||
if (Character.isBmpCodePoint(udCh)) {
|
||||
resultBuilder.insert(0, (char) udCh);
|
||||
} else {
|
||||
resultBuilder.insert(0, Character.toChars(udCh));
|
||||
}
|
||||
|
||||
i += Character.charCount(ch);
|
||||
}
|
||||
}
|
||||
|
||||
String translate(String input) {
|
||||
resultBuilder.setLength(0);
|
||||
appendToLineBuilder(input);
|
||||
return resultBuilder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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
|
||||
lzh=Classical Chinese
|
||||
|
||||
# Scripts
|
||||
Qabs=Upside down
|
||||
@@ -0,0 +1,23 @@
|
||||
#
|
||||
# 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
|
||||
lzh=文言
|
||||
|
||||
# Scripts
|
||||
Qabs=颠倒
|
||||
@@ -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
|
||||
lzh=文言
|
||||
Reference in New Issue
Block a user