org.kordamp.ikonli
diff --git a/src/main/java/cn/octopusyan/dmt/model/WordCsvItem.java b/src/main/java/cn/octopusyan/dmt/model/WordCsvItem.java
index 23efe86..aa60e95 100644
--- a/src/main/java/cn/octopusyan/dmt/model/WordCsvItem.java
+++ b/src/main/java/cn/octopusyan/dmt/model/WordCsvItem.java
@@ -15,18 +15,49 @@ import java.io.File;
public class WordCsvItem extends WordItem {
/**
- * 开始下标(csv繁体
+ * 是否规整(有些翻译列数不完整,无法正常分割)
*/
- private Integer indexTrad;
+ private boolean regular;
+
+ /**
+ * csv中Language列文本
+ *
+ * 当{@code regular}为{@code false}时,用于获取于csv原文内容,用于拼接格式化文本
+ */
+ private String header;
/**
* 原文(获取于csv繁体位置,用于替换翻译文本
*/
private String originalTrad;
- public WordCsvItem(File file, Integer lines, Integer index, String original, String chinese, Integer indexTrad, String originalTrad) {
- super(file, lines, index, original, chinese);
- this.indexTrad = indexTrad;
+ /**
+ * 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(不规整)文本对象
+ *
+ *
+ * @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;
+ }
}
diff --git a/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java b/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
index 5dad3b7..60f23c0 100644
--- a/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
+++ b/src/main/java/cn/octopusyan/dmt/utils/PBOUtil.java
@@ -5,6 +5,7 @@ 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.utils.csv.*;
import cn.octopusyan.dmt.view.ConsoleLog;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;
@@ -15,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;
@@ -154,7 +158,7 @@ public class PBOUtil {
*
* @param wordFileMap 文件对应文本map
*/
- public static void writeWords(Map> wordFileMap) {
+ public static void writeWords(Map> wordFileMap) throws IOException {
for (Map.Entry> entry : wordFileMap.entrySet()) {
@@ -165,53 +169,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 lines = new ArrayList<>();
+ List lines;
- consoleLog.info("正在写入文件[{}]", writeFile.getAbsolutePath());
+ consoleLog.info("正在写入文件 => {}", writeFile.getAbsolutePath());
try (LineIterator it = FileUtils.lineIterator(file, StandardCharsets.UTF_8.name())) {
- while (it.hasNext()) {
- lineIndex.addAndGet(1);
- String line = it.next();
- WordItem word = wordMap.get(lineIndex.get());
-
- // 当前行是否有需要替换的文本
- if (word != null && line.contains(word.getOriginal())) {
-
- if (word instanceof WordCsvItem csvItem) {
- // 繁体部分
- String trad = line.substring(csvItem.getIndexTrad(), csvItem.getIndex());
- // 简体部分
- String simp = line.substring(csvItem.getIndex());
- // 拼接
- line = line.substring(0, csvItem.getIndexTrad())
- // Pattern.quote 处理转义字符
- + trad.replaceFirst(Pattern.quote(csvItem.getOriginalTrad()), csvItem.getChinese())
- + simp.replaceFirst(Pattern.quote(csvItem.getOriginal()), csvItem.getChinese());
- } else {
- line = line.substring(0, word.getIndex()) +
- line.substring(word.getIndex()).replace(word.getOriginal(), word.getChinese());
- }
- }
-
- // 缓存行内容
- lines.add(line);
+ 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文件)
@@ -219,6 +205,99 @@ public class PBOUtil {
}
}
+ /**
+ * 写入 CPP 或 layout 文件
+ *
+ * @param it 行遍历器
+ * @param wordMap 替换文本map
+ * @return 待写入行文本列表
+ */
+ private static List writeOther(LineIterator it, Map wordMap) {
+ AtomicInteger lineIndex = new AtomicInteger(0);
+ List 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 file
+ * @param it 行遍历器
+ * @param wordMap 替换文本map
+ * @return 待写入行文本列表
+ */
+ private static List writeCsv(File file, LineIterator it, Map wordMap) {
+ AtomicInteger lineIndex = new AtomicInteger(0);
+ List 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;
+ }
+
/**
* 查找文件内可翻译文本
*
@@ -251,7 +330,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")) {
@@ -268,67 +347,39 @@ public class PBOUtil {
return Collections.emptyList();
}
- /**
- * 从csv文件中读取可翻译文本
- *
- * @param file csv文件
- * @param it 行内容遍历器
- * @return 可翻译文本列表
- */
- private static List findWordByCSV(File file, LineIterator it) {
+
+ private static List findWordByCSV(File file) {
ArrayList wordItems = new ArrayList<>();
- AtomicInteger lines = new AtomicInteger(0);
- int index = -1;
- int indexTrad = -1;
- int indexOriginal = -1;
- String line;
- while (it.hasNext()) {
- line = it.next();
- boolean contains = line.contains("\"");
- String delimit = contains ? "\",\"" : ",";
- List split = Arrays.stream(line.split(delimit)).toList();
- lines.addAndGet(1);
- if (lines.get() == 1) {
- for (int i = 0; i < split.size(); i++) {
- String colName = StringUtils.lowerCase(split.get(i));
- if (colName.contains("original")) {
- indexOriginal = i;
- } else if (colName.contains("chinesesimp")) {
- index = i;
- } else if (colName.contains("chinese")) {
- indexTrad = i;
- }
- }
- continue;
- }
-
- if (index < split.size()) {
- // 中文内容
- String chinese = split.get(index).replaceAll("\",?", "");
- // 已有中文翻译则跳过
- if (containsChinese(chinese))
- continue;
-
- // 原文
- String original = split.get(indexOriginal).replaceAll("\"", "");
- // 繁体内容
- String originalTrad = split.get(indexTrad).replaceAll("\"", "");
- // 开始下标
- String searchSr = contains ? "\",\"" : ",";
- int startIndex = StringUtils.ordinalIndexOf(line, searchSr, index);
- int startIndexTrad = StringUtils.ordinalIndexOf(line, searchSr, indexTrad);
-
- // 如果带引号
- startIndex += (contains ? 3 : 1);
- startIndexTrad += (contains ? 3 : 1);
-
- // 添加单词
- if (original.length() > 1) {
- wordItems.add(new WordCsvItem(file, lines.get(), startIndex, chinese, "", startIndexTrad, originalTrad));
- }
+ CsvReadConfig config = CsvReadConfig.defaultConfig().setTrimField(true).setContainsHeader(true);
+ CsvReader reader = CsvUtil.getReader(config);
+ CsvData data = reader.read(file);
+
+ // 读取CSV
+ List 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(11);
+
+ // 已有中文翻译,则跳过
+ if (containsChinese(chinese)) continue;
+
+ item = new WordCsvItem(file, lines, original, chinese, row.get(14));
+ } else {
+ item = new WordCsvItem(file, lines, row.getFirst(), original);
}
+ wordItems.add(item);
}
+
return wordItems;
}
diff --git a/src/main/java/cn/octopusyan/dmt/utils/csv/CsvBaseReader.java b/src/main/java/cn/octopusyan/dmt/utils/csv/CsvBaseReader.java
new file mode 100644
index 0000000..33b2937
--- /dev/null
+++ b/src/main/java/cn/octopusyan/dmt/utils/csv/CsvBaseReader.java
@@ -0,0 +1,307 @@
+package cn.octopusyan.dmt.utils.csv;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.ObjectUtil;
+
+import java.io.File;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * CSV文件读取器基础类,提供灵活的文件、路径中的CSV读取,一次构造可多次调用读取不同数据,参考:FastCSV
+ *
+ * @author Looly
+ * @since 5.0.4
+ */
+public class CsvBaseReader implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 默认编码
+ */
+ protected static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
+
+ private final CsvReadConfig config;
+
+ //--------------------------------------------------------------------------------------------- Constructor start
+
+ /**
+ * 构造,使用默认配置项
+ */
+ public CsvBaseReader() {
+ this(null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param config 配置项
+ */
+ public CsvBaseReader(CsvReadConfig config) {
+ this.config = ObjectUtil.defaultIfNull(config, CsvReadConfig::defaultConfig);
+ }
+ //--------------------------------------------------------------------------------------------- Constructor end
+
+ /**
+ * 设置字段分隔符,默认逗号','
+ *
+ * @param fieldSeparator 字段分隔符,默认逗号','
+ */
+ public void setFieldSeparator(char fieldSeparator) {
+ this.config.setFieldSeparator(fieldSeparator);
+ }
+
+ /**
+ * 设置 文本分隔符,文本包装符,默认双引号'"'
+ *
+ * @param textDelimiter 文本分隔符,文本包装符,默认双引号'"'
+ */
+ public void setTextDelimiter(char textDelimiter) {
+ this.config.setTextDelimiter(textDelimiter);
+ }
+
+ /**
+ * 设置是否首行做为标题行,默认false
+ *
+ * @param containsHeader 是否首行做为标题行,默认false
+ */
+ public void setContainsHeader(boolean containsHeader) {
+ this.config.setContainsHeader(containsHeader);
+ }
+
+ /**
+ * 设置是否跳过空白行,默认true
+ *
+ * @param skipEmptyRows 是否跳过空白行,默认true
+ */
+ public void setSkipEmptyRows(boolean skipEmptyRows) {
+ this.config.setSkipEmptyRows(skipEmptyRows);
+ }
+
+ /**
+ * 设置每行字段个数不同时是否抛出异常,默认false
+ *
+ * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常,默认false
+ */
+ public void setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) {
+ this.config.setErrorOnDifferentFieldCount(errorOnDifferentFieldCount);
+ }
+
+ /**
+ * 读取CSV文件,默认UTF-8编码
+ *
+ * @param file CSV文件
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(File file) throws IORuntimeException {
+ return read(file, DEFAULT_CHARSET);
+ }
+
+ /**
+ * 从字符串中读取CSV数据
+ *
+ * @param csvStr CSV字符串
+ * @return {@link CsvData},包含数据列表和行信息
+ */
+ public CsvData readFromStr(String csvStr) {
+ return read(new StringReader(csvStr));
+ }
+
+ /**
+ * 从字符串中读取CSV数据
+ *
+ * @param csvStr CSV字符串
+ * @param rowHandler 行处理器,用于一行一行的处理数据
+ */
+ public void readFromStr(String csvStr, CsvRowHandler rowHandler) {
+ read(parse(new StringReader(csvStr)), true, rowHandler);
+ }
+
+
+ /**
+ * 读取CSV文件
+ *
+ * @param file CSV文件
+ * @param charset 文件编码,默认系统编码
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(File file, Charset charset) throws IORuntimeException {
+ return read(Objects.requireNonNull(file.toPath(), "file must not be null"), charset);
+ }
+
+ /**
+ * 读取CSV文件,默认UTF-8编码
+ *
+ * @param path CSV文件
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(Path path) throws IORuntimeException {
+ return read(path, DEFAULT_CHARSET);
+ }
+
+ /**
+ * 读取CSV文件
+ *
+ * @param path CSV文件
+ * @param charset 文件编码,默认系统编码
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(Path path, Charset charset) throws IORuntimeException {
+ Assert.notNull(path, "path must not be null");
+ return read(FileUtil.getReader(path, charset));
+ }
+
+ /**
+ * 从Reader中读取CSV数据,读取后关闭Reader
+ *
+ * @param reader Reader
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(Reader reader) throws IORuntimeException {
+ return read(reader, true);
+ }
+
+ /**
+ * 从Reader中读取CSV数据
+ *
+ * @param reader Reader
+ * @param close 读取结束是否关闭Reader
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public CsvData read(Reader reader, boolean close) throws IORuntimeException {
+ final CsvParser csvParser = parse(reader);
+ final List rows = new ArrayList<>();
+ read(csvParser, close, rows::add);
+ final List header = config.headerLineNo > -1 ? csvParser.getHeader() : null;
+
+ return new CsvData(header, rows);
+ }
+
+ /**
+ * 从Reader中读取CSV数据,结果为Map,读取后关闭Reader。
+ * 此方法默认识别首行为标题行。
+ *
+ * @param reader Reader
+ * @return {@link CsvData},包含数据列表和行信息
+ * @throws IORuntimeException IO异常
+ */
+ public List