From 7992260575b06f19463dbf2cc15517cdc8ea142d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 21 Sep 2025 16:40:41 +0800 Subject: [PATCH] update --- .../hmcl/gradle/l10n/UpsideDownTranslate.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 buildSrc/src/main/java/org/jackhuang/hmcl/gradle/l10n/UpsideDownTranslate.java diff --git a/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/l10n/UpsideDownTranslate.java b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/l10n/UpsideDownTranslate.java new file mode 100644 index 000000000..ecfd698c4 --- /dev/null +++ b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/l10n/UpsideDownTranslate.java @@ -0,0 +1,159 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui 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 . + */ +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.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/// @author Glavo +public abstract class UpsideDownTranslate extends DefaultTask { + + @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) -> output.put(k, translator.translate(v.toString()))); + + try (var writer = Files.newBufferedWriter(outputFile)) { + output.store(writer, "This file is automatically generated, please do not modify it manually"); + } + } + + private static final class Translator { + private static final Map 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ɥıظʞןɯuuodbɹ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\\$)?[sd]"); + private static final Pattern XML_TAG_PATTERN = Pattern.compile("^<(?[a-zA-Z]+)( href=\"[^\"]*\")?>"); + + private final StringBuilder lineBuilder = 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(); + lineBuilder.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 = ""; + + int endTagOffset = input.indexOf(endTag, i + beginTag.length()); + if (endTagOffset > 0) { + lineBuilder.insert(0, endTag); + appendToLineBuilder(input.substring(i + beginTag.length(), endTagOffset)); + lineBuilder.insert(0, beginTag); + + i = endTagOffset + endTag.length(); + continue; + } + } + } + + Integer udCh = mapper.getOrDefault(ch, ch); + if (Character.isBmpCodePoint(udCh)) { + lineBuilder.insert(0, udCh); + } else { + lineBuilder.insert(0, Character.toChars(udCh)); + } + + i += Character.charCount(ch); + } + } + + private final StringBuilder resultBuilder = new StringBuilder(); + + String translate(String input) { + resultBuilder.setLength(0); + + for (int i = 0; i < input.length(); ) { + int idx = input.indexOf('\n'); + String line = input.substring(0, idx < 0 ? input.length() : idx); + + this.lineBuilder.setLength(0); + appendToLineBuilder(line); + resultBuilder.append(lineBuilder); + + if (idx < 0) { + break; + } else { + resultBuilder.append('\n'); + i = idx + 1; + } + } + + return resultBuilder.toString(); + } + } +}