领地记录物品名多源翻译:有道+简心+Google三级降级
This commit is contained in:
@@ -0,0 +1,310 @@
|
|||||||
|
package Sdf1_login;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多源在线翻译工具 —— 将MC英文材料名翻译为中文。
|
||||||
|
* 优先使用国内接口(有道/简心),失败后降级到Google。
|
||||||
|
* 结果缓存避免重复请求。
|
||||||
|
*/
|
||||||
|
public class MaterialTranslator {
|
||||||
|
|
||||||
|
private static final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
|
private static final HttpClient http = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(3))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 翻译源优先级:国内接口优先
|
||||||
|
private static final List<TranslationSource> SOURCES = Arrays.asList(
|
||||||
|
TranslationSource.YOUDAO,
|
||||||
|
TranslationSource.JIANXIN,
|
||||||
|
TranslationSource.GOOGLE
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译源枚举
|
||||||
|
*/
|
||||||
|
private enum TranslationSource {
|
||||||
|
YOUDAO("有道翻译"),
|
||||||
|
JIANXIN("简心翻译"),
|
||||||
|
GOOGLE("Google翻译");
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
TranslationSource(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将材料名(如 GRASS_BLOCK)转为可读英文(Grass Block)
|
||||||
|
*/
|
||||||
|
public static String toReadable(String materialName) {
|
||||||
|
if (materialName == null || materialName.isEmpty()) return "";
|
||||||
|
return Arrays.stream(materialName.split("_"))
|
||||||
|
.map(w -> w.substring(0, 1).toUpperCase() + w.substring(1).toLowerCase())
|
||||||
|
.reduce((a, b) -> a + " " + b)
|
||||||
|
.orElse(materialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步翻译单个材料名(有缓存则直接返回)
|
||||||
|
*/
|
||||||
|
public static String translate(String materialName) {
|
||||||
|
if (materialName == null || materialName.isEmpty()) return "";
|
||||||
|
String key = materialName.toUpperCase();
|
||||||
|
if (cache.containsKey(key)) return cache.get(key);
|
||||||
|
|
||||||
|
String readable = toReadable(key);
|
||||||
|
String zh = translateWithFallback(readable);
|
||||||
|
if (zh != null && !zh.isEmpty()) {
|
||||||
|
cache.put(key, zh);
|
||||||
|
return zh;
|
||||||
|
}
|
||||||
|
return readable; // fallback 可读英文
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量异步翻译,返回 future map
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<Map<String, String>> translateBatch(Collection<String> materials) {
|
||||||
|
Set<String> unique = new LinkedHashSet<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null && !m.isEmpty() && !cache.containsKey(m.toUpperCase())) {
|
||||||
|
unique.add(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unique.isEmpty()) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null) result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||||
|
for (String m : unique) {
|
||||||
|
futures.add(CompletableFuture.runAsync(() -> translate(m), executor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||||
|
.thenApply(v -> {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (String m : materials) {
|
||||||
|
if (m != null) {
|
||||||
|
result.put(m, cache.getOrDefault(m.toUpperCase(), toReadable(m)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多源翻译,按优先级尝试,失败自动降级
|
||||||
|
*/
|
||||||
|
private static String translateWithFallback(String text) {
|
||||||
|
for (TranslationSource source : SOURCES) {
|
||||||
|
try {
|
||||||
|
String result = null;
|
||||||
|
switch (source) {
|
||||||
|
case YOUDAO:
|
||||||
|
result = translateViaYoudao(text);
|
||||||
|
break;
|
||||||
|
case JIANXIN:
|
||||||
|
result = translateViaJianxin(text);
|
||||||
|
break;
|
||||||
|
case GOOGLE:
|
||||||
|
result = translateViaGoogle(text);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (result != null && !result.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 该源失败,尝试下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 翻译源实现 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有道翻译API
|
||||||
|
* URL: https://v.api.aa1.cn/api/api-fanyi-yd/index.php
|
||||||
|
* 参数: msg=翻译内容, type=1(中→英) / 2(英→中)
|
||||||
|
*/
|
||||||
|
private static String translateViaYoudao(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
// type=2 表示英→中
|
||||||
|
String url = "https://v.api.aa1.cn/api/api-fanyi-yd/index.php?msg=" + encoded + "&type=2";
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 返回: {"type":"中英互译","desc":"中文翻译英文","text":"翻译结果"}
|
||||||
|
return parseJsonField(resp.body(), "text");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简心翻译API
|
||||||
|
* URL: https://api.qvqa.cn/api/fanyi
|
||||||
|
* 参数: text=翻译内容, source=en, target=zh
|
||||||
|
*/
|
||||||
|
private static String translateViaJianxin(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
String url = "https://api.qvqa.cn/api/fanyi?text=" + encoded + "&source=en&target=zh";
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 返回: {"code":200,"message":"OK","data":{"sourceText":"...","targetText":"翻译结果",...}}
|
||||||
|
return parseNestedJsonField(resp.body(), "data", "targetText");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Translate 免费接口
|
||||||
|
*/
|
||||||
|
private static String translateViaGoogle(String text) {
|
||||||
|
try {
|
||||||
|
String encoded = URLEncoder.encode(text, StandardCharsets.UTF_8);
|
||||||
|
String url = "https://clients5.google.com/translate_a/single"
|
||||||
|
+ "?client=gtx&sl=en&tl=zh-CN&dt=t&q=" + encoded;
|
||||||
|
|
||||||
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("User-Agent", "Mozilla/5.0")
|
||||||
|
.timeout(Duration.ofSeconds(3))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (resp.statusCode() == 200) {
|
||||||
|
// 响应格式: [["翻译结果","原文",...],...]
|
||||||
|
String body = resp.body();
|
||||||
|
int firstQuote = body.indexOf("[[\"");
|
||||||
|
if (firstQuote >= 0) {
|
||||||
|
int endQuote = body.indexOf("\"", firstQuote + 3);
|
||||||
|
if (endQuote > firstQuote) {
|
||||||
|
return body.substring(firstQuote + 3, endQuote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 静默降级
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== JSON解析工具 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单JSON字段解析(单层)
|
||||||
|
* 从 {"key":"value"} 中提取 value
|
||||||
|
*/
|
||||||
|
private static String parseJsonField(String json, String field) {
|
||||||
|
try {
|
||||||
|
String pattern = "\"" + field + "\"";
|
||||||
|
int idx = json.indexOf(pattern);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
// 找到冒号后的引号
|
||||||
|
int colon = json.indexOf(':', idx + pattern.length());
|
||||||
|
if (colon < 0) return null;
|
||||||
|
|
||||||
|
int quoteStart = json.indexOf('"', colon + 1);
|
||||||
|
if (quoteStart < 0) return null;
|
||||||
|
|
||||||
|
int quoteEnd = json.indexOf('"', quoteStart + 1);
|
||||||
|
if (quoteEnd < 0) return null;
|
||||||
|
|
||||||
|
return json.substring(quoteStart + 1, quoteEnd);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 嵌套JSON字段解析
|
||||||
|
* 从 {"outer":{"inner":"value"}} 中提取 value
|
||||||
|
*/
|
||||||
|
private static String parseNestedJsonField(String json, String outerField, String innerField) {
|
||||||
|
try {
|
||||||
|
// 找到外层对象
|
||||||
|
String outerPattern = "\"" + outerField + "\"";
|
||||||
|
int outerIdx = json.indexOf(outerPattern);
|
||||||
|
if (outerIdx < 0) return null;
|
||||||
|
|
||||||
|
// 找到外层对象的开始大括号
|
||||||
|
int braceStart = json.indexOf('{', outerIdx);
|
||||||
|
if (braceStart < 0) return null;
|
||||||
|
|
||||||
|
// 从外层对象内部解析内层字段
|
||||||
|
int depth = 1;
|
||||||
|
int i = braceStart + 1;
|
||||||
|
while (i < json.length() && depth > 0) {
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '{') depth++;
|
||||||
|
else if (c == '}') depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
String innerJson = json.substring(braceStart, i);
|
||||||
|
return parseJsonField(innerJson, innerField);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存(可选)
|
||||||
|
*/
|
||||||
|
public static void clearCache() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前使用的翻译源信息(调试用)
|
||||||
|
*/
|
||||||
|
public static String getSourceInfo() {
|
||||||
|
StringBuilder sb = new StringBuilder("翻译源优先级: ");
|
||||||
|
for (int i = 0; i < SOURCES.size(); i++) {
|
||||||
|
TranslationSource src = SOURCES.get(i);
|
||||||
|
sb.append(i + 1).append(".").append(src.name);
|
||||||
|
if (i < SOURCES.size() - 1) sb.append(" > ");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user