16 Commits

13 changed files with 294 additions and 110 deletions

View File

@ -8,9 +8,6 @@
<br>
[![license](https://img.shields.io/github/license/octopusYan/dayz-mod-translator)](https://github.com/octopusYan/dayz-mod-translator)
![commit](https://img.shields.io/github/commit-activity/m/octopusYan/dayz-mod-translator?color=%23ff69b4)
<br>
![stars](https://img.shields.io/github/stars/octopusYan/dayz-mod-translator?style=social)
![GitHub all releases](https://img.shields.io/github/downloads/octopusYan/dayz-mod-translator/total?style=social)
<br>

View File

@ -130,6 +130,14 @@
<version>${jackson.version}</version>
</dependency>
<!-- Hutool -->
<!-- https://hutool.cn -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.34</version>
</dependency>
<!-- https://kordamp.org/ikonli/ -->
<dependency>
<groupId>org.kordamp.ikonli</groupId>

View File

@ -1,7 +1,7 @@
package cn.octopusyan.dmt.common.manager.http;
import cn.octopusyan.dmt.common.enums.ProxySetup;
import cn.octopusyan.dmt.common.manager.http.response.BodyHandler;
import cn.octopusyan.dmt.common.manager.http.response.DownloadBodyHandler;
import cn.octopusyan.dmt.common.util.JsonUtil;
import cn.octopusyan.dmt.model.ProxyInfo;
import com.fasterxml.jackson.databind.JsonNode;
@ -130,7 +130,7 @@ public class HttpUtil {
}
// 下载处理器
var handler = BodyHandler.create(
var handler = DownloadBodyHandler.create(
Path.of(savePath),
StandardOpenOption.CREATE, StandardOpenOption.WRITE
);

View File

@ -21,16 +21,16 @@ import java.util.function.Consumer;
* @author octopus_yan
*/
@Slf4j
public class BodyHandler implements HttpResponse.BodyHandler<Path> {
public class DownloadBodyHandler implements HttpResponse.BodyHandler<Path> {
private final HttpResponse.BodyHandler<Path> handler;
private BiConsumer<Long, Long> consumer;
private BodyHandler(HttpResponse.BodyHandler<Path> handler) {
private DownloadBodyHandler(HttpResponse.BodyHandler<Path> handler) {
this.handler = handler;
}
public static BodyHandler create(Path directory, OpenOption... openOptions) {
return new BodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
public static DownloadBodyHandler create(Path directory, OpenOption... openOptions) {
return new DownloadBodyHandler(HttpResponse.BodyHandlers.ofFileDownload(directory, openOptions));
}
@Override

View File

@ -89,6 +89,9 @@ public class MainController extends BaseController<MainViewModel> {
fileChooser.getExtensionFilters().add(extFilter);
}
private File historySelectFolder;
private File historySaveFolder;
@Override
public Pane getRootPanel() {
return root;
@ -150,6 +153,8 @@ public class MainController extends BaseController<MainViewModel> {
* 打开文件选择器
*/
public void selectFile() {
if (historySelectFolder != null)
fileChooser.setInitialDirectory(historySelectFolder);
selectFile(fileChooser.showOpenDialog(getWindow()));
}
@ -247,11 +252,15 @@ public class MainController extends BaseController<MainViewModel> {
public void onPackOver(File packFile) {
// 选择文件保存地址
fileChooser.setInitialFileName(packFile.getName());
if (historySaveFolder != null)
fileChooser.setInitialDirectory(historySaveFolder);
File file = fileChooser.showSaveDialog(getWindow());
if (file == null)
return;
historySaveFolder = file.getParentFile();
if (file.exists()) {
//文件已存在,则删除覆盖文件
FileUtils.deleteQuietly(file);
@ -322,6 +331,10 @@ public class MainController extends BaseController<MainViewModel> {
* 打开文件
*/
private void selectFile(File file) {
if (file != null) {
// 设置选择文件记录
historySelectFolder = file.getParentFile();
}
viewModel.selectFile(file);
viewModel.unpack();
}

View File

@ -0,0 +1,63 @@
package cn.octopusyan.dmt.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.File;
/**
* csv
*
* @author octopus_yan
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WordCsvItem extends WordItem {
/**
* 是否规整(有些翻译列数不完整,无法正常分割)
*/
private boolean regular;
/**
* csv中Language列文本
* <p>
* 当{@code regular}为{@code false}时用于获取于csv原文内容用于拼接格式化文本
*/
private String header;
/**
* 原文(获取于csv繁体位置用于替换翻译文本
*/
private String originalTrad;
/**
* csv(规整)文本对象
*
* @param file 文件
* @param lines 行数
* @param original 原文
* @param chinese 中文位置对应的文本
* @param originalTrad 繁体中文位置对应的文本
*/
public WordCsvItem(File file, Integer lines, String original, String chinese, String originalTrad) {
super(file, lines, 0, original, chinese);
this.regular = true;
this.originalTrad = originalTrad;
}
/**
* csv(不规整)文本对象
* <p>
*
* @param file 文件
* @param lines 行数
* @param header Language列对应名称用于拼接格式化文本
* @param original 原文
*/
public WordCsvItem(File file, Integer lines, String header, String original) {
super(file, lines, null, original, "");
this.regular = false;
this.header = header;
}
}

View File

@ -34,6 +34,8 @@ public class WordItem {
*/
private StringProperty chineseProperty = new SimpleStringProperty();
public WordItem() {}
public WordItem(File file, Integer lines, Integer index, String original, String chinese) {
this.file = file;
this.lines = lines;

View File

@ -37,11 +37,6 @@ public class FreeBaiduTranslateProcessor extends AbstractTranslateProcessor {
return "https://fanyi.baidu.com/transapi";
}
@Override
public int qps() {
return source().getDefaultQps();
}
/**
* 翻译处理
*

View File

@ -25,11 +25,6 @@ public class FreeGoogleTranslateProcessor extends AbstractTranslateProcessor {
return "https://translate.googleapis.com/translate_a/single";
}
@Override
public int qps() {
return source().getDefaultQps();
}
/**
* 翻译处理
*

View File

@ -1,8 +1,10 @@
package cn.octopusyan.dmt.utils;
import cn.hutool.core.text.csv.*;
import cn.octopusyan.dmt.common.config.Constants;
import cn.octopusyan.dmt.common.config.Context;
import cn.octopusyan.dmt.common.util.ProcessesUtil;
import cn.octopusyan.dmt.model.WordCsvItem;
import cn.octopusyan.dmt.model.WordItem;
import cn.octopusyan.dmt.view.ConsoleLog;
import org.apache.commons.io.FileUtils;
@ -14,7 +16,10 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Matcher;
@ -141,6 +146,10 @@ public class PBOUtil {
return wordItems;
List<File> files = new ArrayList<>(FileUtils.listFiles(file, FILE_NAME_LIST, true));
List<String> names = files.stream().map(File::getName).toList();
if(names.contains("config.bin") && names.contains("config.cpp")) {
files = files.stream().filter(item -> "config.cpp".equals(item.getName())).toList();
}
for (File item : files) {
wordItems.addAll(findWordByFile(item));
}
@ -153,7 +162,7 @@ public class PBOUtil {
*
* @param wordFileMap 文件对应文本map
*/
public static void writeWords(Map<File, List<WordItem>> wordFileMap) {
public static void writeWords(Map<File, List<WordItem>> wordFileMap) throws IOException {
for (Map.Entry<File, List<WordItem>> entry : wordFileMap.entrySet()) {
@ -164,41 +173,35 @@ public class PBOUtil {
// 需要转bin文件时写入bak目录下cpp文件
boolean hasBin = new File(outFilePath(file, ".bin")).exists();
// 写入TMP下文件
String writePath = file.getAbsolutePath().replace(Constants.BAK_DIR_PATH, Constants.TMP_DIR_PATH);
File writeFile = hasBin ? file : new File(writePath);
AtomicInteger lineIndex = new AtomicInteger(0);
List<String> lines = new ArrayList<>();
List<String> lines;
consoleLog.info("正在写入文件[{}]", writeFile.getAbsolutePath());
consoleLog.info("正在写入文件 => {}", writeFile.getAbsolutePath());
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
while (it.hasNext()) {
String line = it.next();
WordItem word = wordMap.get(lineIndex.get());
// 当前行是否有需要替换的文本
// TODO 是否替换空文本
if (word != null && line.contains(word.getOriginal())) {
line = line.substring(0, word.getIndex()) +
line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
}
// 缓存行内容
lines.add(line);
lineIndex.addAndGet(1);
if (FILE_NAME_STRING_TABLE.equals(file.getName())) {
// 写入 CSV 文件
lines = writeCsv(file, it, wordMap);
} else {
// 写入 CPP 或 layout 文件
lines = writeOther(it, wordMap);
}
} catch (IOException e) {
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]读取出错", e);
throw e;
}
// 写入文件
try {
// 写入文件
String charsets = writeFile.getName().endsWith(".layout") ? FileUtil.getCharsets(writeFile) : StandardCharsets.UTF_8.name();
FileUtils.writeLines(writeFile, charsets, lines);
} catch (IOException e) {
consoleLog.error(STR."文件(\{writeFile.getAbsoluteFile()})写入失败", e);
consoleLog.error(STR."文件[\{file.getAbsoluteFile()}]写入出错", e);
throw e;
}
// CPP转BIN (覆盖TMP下BIN文件)
@ -206,6 +209,98 @@ public class PBOUtil {
}
}
/**
* 写入 CPP 或 layout 文件
*
* @param it 行遍历器
* @param wordMap 替换文本map
* @return 待写入行文本列表
*/
private static List<String> writeOther(LineIterator it, Map<Integer, WordItem> wordMap) {
AtomicInteger lineIndex = new AtomicInteger(0);
List<String> lines = new ArrayList<>();
while (it.hasNext()) {
lineIndex.addAndGet(1);
String line = it.next();
WordItem word = wordMap.get(lineIndex.get());
if (word != null && line.contains(word.getOriginal())) {
line = line.substring(0, word.getIndex()) +
line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
}
lines.add(line);
}
return lines;
}
/**
* 写入 CSV 文件
*
* @param it 行遍历器
* @param wordMap 替换文本map
* @return 待写入行文本列表
*/
private static List<String> writeCsv(File file, LineIterator it, Map<Integer, WordItem> wordMap) {
AtomicInteger lineIndex = new AtomicInteger(0);
List<String> lines = new ArrayList<>();
CsvReader reader = CsvUtil.getReader(CsvReadConfig.defaultConfig());
CsvData data = reader.read(file);
var rowMap = data.getRows().stream()
.collect(Collectors.toMap(CsvRow::getOriginalLineNumber, Function.identity()));
while (it.hasNext()) {
lineIndex.addAndGet(1);
String line = it.next();
WordCsvItem word = (WordCsvItem) wordMap.get(lineIndex.get());
// 以 , 开头的行(视为内容带换行符,跳过)
// ,,开头视为空值行(不跳过,尽量还原文本结构
if (word == null && line.startsWith(",") && !line.startsWith(",,")) {
continue;
}
// 判断当前行是否有需要替换的文本
if (word != null && line.contains(word.getOriginal())) {
// 是否规整(可简单读取的)
if (word.isRegular()) {
CsvRow strings = rowMap.get(Integer.valueOf(lineIndex.get()).longValue() - 1L);
// 替换翻译文本
strings.set(11, word.getChinese());// 繁体
strings.set(14, word.getChinese());// 简体
line = strings.stream().map(item -> STR."\"\{item}\"").collect(Collectors.joining(","));
// 处理带换行符文本
var length = line.split("\r\n|\r|\n").length;
for (int i = 1; i < length; i++) {
lineIndex.addAndGet(1);
String next = it.next();
consoleLog.debug(STR."next => \"\{next}\"");
}
} else {
// 不规整的直接原文填充
// Language,original,english,czech,german,russian,polish,hungarian,italian,spanish,french,chinese,japanese,portuguese,chinesesimp
StringBuilder sb = new StringBuilder();
sb.append("\"").append(word.getHeader()).append("\"");
for (int i = 1; i < 15; i++) {
String str = (i == 11 || i == 14) ? word.getChinese() : word.getOriginal();
sb.append(",\"").append(str).append("\"");
}
line = sb.toString();
}
}
lines.add(line);
}
return lines;
}
/**
* 查找文件内可翻译文本
*
@ -238,7 +333,7 @@ public class PBOUtil {
}
// CSV
if (FILE_NAME_STRING_TABLE.equals(file.getName())) {
return findWordByCSV(file, it);
return findWordByCSV(file);
}
// layout
if (file.getName().endsWith(".layout")) {
@ -255,37 +350,39 @@ public class PBOUtil {
return Collections.emptyList();
}
/**
* 从csv文件中读取可翻译文本
*
* @param file csv文件
* @param it 行内容遍历器
* @return 可翻译文本列表
*/
private static List<WordItem> findWordByCSV(File file, LineIterator it) {
private static List<WordItem> findWordByCSV(File file) {
ArrayList<WordItem> wordItems = new ArrayList<>();
AtomicInteger lines = new AtomicInteger(0);
int index = -1;
String line;
while (it.hasNext()) {
line = it.next();
List<String> split = Arrays.stream(line.split(",")).toList();
if (lines.get() == 0) {
index = split.indexOf("\"chinese\"");
} else if (index < split.size()) {
// 原文
String original = split.get(index).replaceAll("\"", "");
// 开始下标
Integer startIndex = line.indexOf(original);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
CsvReadConfig config = CsvReadConfig.defaultConfig().setTrimField(true).setContainsHeader(true);
CsvReader reader = CsvUtil.getReader(config);
CsvData data = reader.read(file);
// 读取CSV
List<CsvRow> rows = data.getRows();
for (CsvRow row : rows) {
WordItem item;
int lines = (int) (row.getOriginalLineNumber() + 1);
String original = row.get(1);
// 跳过原文为空的行
if (StringUtils.isEmpty(original)) continue;
// 是否可格式化读取
if (row.size() >= 15) {
String chinese = row.get(14);
// 已有中文翻译,则跳过
if (containsChinese(chinese)) continue;
item = new WordCsvItem(file, lines, original, chinese, row.get(11));
} else {
item = new WordCsvItem(file, lines, row.getFirst(), original);
}
lines.addAndGet(1);
wordItems.add(item);
}
return wordItems;
}
@ -302,11 +399,17 @@ public class PBOUtil {
String line;
Matcher matcher;
while (it.hasNext()) {
lines.addAndGet(1);
line = it.next();
matcher = LAYOUT_PATTERN.matcher(line);
if (StringUtils.isNoneEmpty(line) && matcher.matches()) {
// 原文
String original = matcher.group(1);
if (StringUtils.isEmpty(line) || !matcher.matches())
continue;
// 原文
String original = matcher.group(1);
if (!original.startsWith("#")) {
// 开始下标
Integer startIndex = line.indexOf(original);
// 添加单词
@ -314,8 +417,6 @@ public class PBOUtil {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
lines.addAndGet(1);
}
return wordItems;
}
@ -332,24 +433,23 @@ public class PBOUtil {
AtomicInteger lines = new AtomicInteger(0);
while (it.hasNext()) {
String line = it.next();
Matcher matcher = CPP_PATTERN.matcher(line);
if (!line.contains("$") && matcher.matches()) {
String name = matcher.group(1);
// 原始文本
int startIndex = line.indexOf(name) + name.length();
String original = matcher.group(2);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
lines.addAndGet(1);
String line = it.next();
Matcher matcher = CPP_PATTERN.matcher(line);
// 不匹配 或 是变量 则跳过
if (!matcher.matches() || line.contains("$") || line.contains("#"))
continue;
String name = matcher.group(1);
// 原始文本
int startIndex = line.indexOf(name) + name.length();
String original = matcher.group(2);
// 添加单词
if (original.length() > 1) {
wordItems.add(new WordItem(file, lines.get(), startIndex, original, ""));
}
}
return wordItems;
}
@ -414,4 +514,14 @@ public class PBOUtil {
private static String outFilePath(File file, String suffix) {
return file.getParentFile().getAbsolutePath() + File.separator + FileUtil.mainName(file) + suffix;
}
/**
* 给定字符串是否含有中文
*
* @param str 需要判断的字符串
* @return 是否含有中文
*/
public static boolean containsChinese(String str) {
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
}
}

View File

@ -64,10 +64,7 @@ public class EditButtonTableCell extends TableCell<WordItem, WordItem> {
if (empty) {
setGraphic(null);
} else {
/*
* TODO 添加多个操作按钮
* setGraphic(Hbox(btn1,btn2));
*/
// 添加多个操作按钮
setGraphic(new HBox(edit, translate));
}
}

View File

@ -10,13 +10,15 @@ import cn.octopusyan.dmt.task.UnpackTask;
import cn.octopusyan.dmt.task.listener.DefaultTaskListener;
import cn.octopusyan.dmt.translate.DelayWord;
import cn.octopusyan.dmt.translate.TranslateUtil;
import cn.octopusyan.dmt.utils.PBOUtil;
import cn.octopusyan.dmt.view.ConsoleLog;
import cn.octopusyan.dmt.view.alert.AlertUtil;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.scene.control.ProgressIndicator;
import org.apache.commons.lang3.StringUtils;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
@ -24,7 +26,6 @@ import java.io.File;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* 主界面
@ -103,12 +104,14 @@ public class MainViewModel extends BaseViewModel<MainController> {
* 开始翻译
*/
public void startTranslate() {
if(wordItems.isEmpty()) return;
if (wordItems.isEmpty()) return;
if (translateTask == null) {
List<WordItem> words = wordItems.stream().filter(item -> StringUtils.isEmpty(item.getChinese())).toList();
List<WordItem> words = wordItems.stream().filter(item -> !PBOUtil.containsChinese(item.getChinese())).toList();
delayQueue = TranslateUtil.getDelayQueue(words);
translateTask = createTask();
translateTask.execute();
return;
}
if (!translateTask.isRunning()) {
@ -135,7 +138,7 @@ public class MainViewModel extends BaseViewModel<MainController> {
* 打包
*/
public void pack() {
if(wordItems.isEmpty()) return;
if (wordItems.isEmpty()) return;
PackTask packTask = new PackTask(wordItems, unpackPath);
packTask.onListen(new PackTask.PackListener() {
@ -149,6 +152,13 @@ public class MainViewModel extends BaseViewModel<MainController> {
Platform.runLater(() -> controller.onPackOver(file));
}
@Override
public void onFailed(Throwable throwable) {
super.onFailed(throwable);
Platform.runLater(() -> {
AlertUtil.getInstance().exception(new RuntimeException(throwable)).show();
});
}
});
packTask.execute();
}
@ -192,19 +202,12 @@ public class MainViewModel extends BaseViewModel<MainController> {
private void resetProgress() {
translateTask = null;
controller.translate.setGraphic(startIcon);
Styles.toggleStyleClass(controller.translateProgress, Styles.SMALL);
ObservableList<String> styleClass = controller.translateProgress.getStyleClass();
if (!styleClass.contains(Styles.SMALL)) {
Styles.toggleStyleClass(controller.translateProgress, Styles.SMALL);
}
controller.translateProgress.progressProperty().unbind();
controller.translateProgress.setProgress(0);
controller.translateProgress.setVisible(false);
}
/**
* 给定字符串是否含有中文
*
* @param str 需要判断的字符串
* @return 是否含有中文
*/
private boolean containsChinese(String str) {
return Pattern.compile("[\u4e00-\u9fa5]").matcher(str).find();
}
}

View File

@ -18,6 +18,7 @@ module cn.octopusyan.dmt {
requires org.kordamp.ikonli.javafx;
requires org.kordamp.ikonli.feather;
requires java.management;
requires cn.hutool.core;
exports cn.octopusyan.dmt;
exports cn.octopusyan.dmt.model to com.fasterxml.jackson.databind;