AndroidResTranslator
介绍
AndroidResTranslator是使用Java/IDEA写的一个安卓多国语言转换控制台程序。主要功能包括:
- 一键翻译:以某一种语言转另一种语言,支持批量转其他语言
- 支持配置多种翻译api:防止A厂商的翻译api在不支持翻译时,可以使用B厂商进行翻译
- 转xls表格:根据string的name属性作为key整合所有xml语言文件转换xls表格形式
- xls转xml:翻译者将xls翻译之后需要写入xml当中后,程序根据key-value对比并替换xml内容
- 多种翻译类型:支持不同类型的翻译:
- 全量翻译(根据源语言的文字翻译并全部替换掉目标xml语言文件当中的所有值
- 填充翻译(根据源语言文件的k-v值与目标语言文件当中的k-v值进行对比,若目标语言文件当中不包含源语言当中的key值,则将源语言的此key值的string进行翻译并添加到目标语言文件当中)
语言转换步骤
第一步:在as当中创建需要翻译的国家的语言文件
在此举例使用的是小日子语言,源语言为英语。
源语言内容:
新建语言:
注意:这个步骤并不是必须的。以上的工作只不过是使用as创建了一个 values-ja 文件夹和一个 strings.xml 文件。若你清楚的知道想要创建的目标语言简写,可以直接在res下以此对应规则创建对应语言文件夹和文件,工程依然认同此翻译文件。
第二步:你随意选择一个地方创建一个文件夹,并将所有 values 开头的文件夹copy一份到你新创建的文件夹当中
注意:请不要将
values-night
文件粘贴进去。翻译仅支持语言文件。
第三步:将AndroidResTranslator项目down到本地
注意:默认的values文件夹的语言为en。你可以自行在程序当中更改。
第四步:配置后运行
注意:在进行此步前需要对程序当中的翻译厂商进行配置,当前流程是基于配置完成后进行的步骤。未进行配置前程序无法运行。具体配置见后面章节:关于配置厂商
查看Dom4j.java
文件。main
方法当中,仅调用了一个方法:
public static void main(String[] args) {
Dom4j dom4j = new Dom4j();
dom4j.translationEntireValueFolder("C:\\Users\\hp\\Desktop\\values", "en", new FilledTranslationConsumer());
}
/**
* 全量翻译(但不准确,取决于 translationType 参数
* 逻辑是:给一个安卓res文件夹,程序获取文件夹下所有“values-xx”文件夹,解析“values-xx”文件夹下所有string.xml。执行翻译逻辑
*
* @param valueFolderPath 安卓res文件夹
* @param sourceCountry 源语言国家简写。如英语为“en”。翻译时是根据“values-xx”文件夹命名的国家简写识别的。如果“value”文件夹没有国家后缀,默认为英语en
* @param translationType 执行翻译的实际类。不同实现执行的逻辑不一样
*/
public void translationEntireValueFolder(String valueFolderPath, String sourceCountry, TranslationConsumer translationType) {
//...
}
你需要替换源程序当中translationEntireValueFolder
当中的参数值。
一参:是从安卓程序当中copy文件到新建的文件夹的文件地址
二参:是想要从A语言翻译到B语言或其他语言,A语言的语言缩写
三参:翻译类型。稍后我们会讲到,暂且使用写好的FilledTranslationConsumer
然后点击运行即可。
可以在控制台等待程序运行结束
程序运行结束会在程序根目录下出现一个新的名为values
文件夹,此文件夹内包含了所有的翻译结果。无论是单对单或是单对多翻译结果。
将这个生成的文件夹粘贴回安卓程序资源文件夹内即可。注意as提示文件更改时要点击替换文件。
注意:建议在粘贴前检查一遍翻译结果文件。由于是程序是对厂商Api调用翻译的原因,不可避免地受到网络超时、厂商Api参数更改等其他问题。可以查看控制台Log是否出现
“未翻译成功”
相关字样。
实际上,生成的语言文件取决于程序读取的文件夹内以values开头的文件数量。即,有多少个原始的values文件,即生成对应名称与数量的翻译文件。
翻译类型
以上演示的是 填充式 的翻译:即,在res/vlaues
(也即默认是 values-en)文件夹内的 strings.xml 文件,以所有string条目的name
属性作为key,在进行翻译时,翻译生成的文件内若不包含此key的string条目,则进行翻译并添加到翻译文件内。因此它被称作“填充式”翻译。
除“填充式”翻译以外,还有 替换式 翻译。即无论生成的翻译文件内是否包含对应key值的string条目,都将删除此条目后,将源语言的文字进行翻译后重新填充到生成的文件当中。
填充式翻译:
本例当中含有两个语言文件:
- 源语言文件
values/strings.xml
(英语 左) - 目标翻译文件
values-ja/strings.xml
(日语 右)
很明显,values-ja/strings.xml
文件内相对values/strings.xml
少了name为:tips、hello 的两条string。
回到Dom4j.java
程序,在程序当中translationEntireValueFolder
的第三个参数使用的是FilledTranslationConsumer
:
public static void main(String[] args) {
Dom4j dom4j = new Dom4j();
dom4j.translationEntireValueFolder("C:\\Users\\hp\\Desktop\\values", "en", new FilledTranslationConsumer());
}
FilledTranslationConsumer 代表的是 填充式翻译。
运行程序,我们来看结果:
控制台显示仅翻译了两条。values-ja/strings.xml
在values\strings.xml
当中没有的string条目将会翻译后填充到文件当中。
我们再尝试替换式翻译
替换式翻译:
我们看到:在values-ja
文件夹下的strings.xml
的内容包含了错误的英语部分,但两者包含相同name属性的string条目。回到程序,我们使用AllReplaceTranslationConsumer
:
public static void main(String[] args) {
Dom4j dom4j = new Dom4j();
dom4j.translationEntireValueFolder("C:\\Users\\hp\\Desktop\\values", "en", new AllReplaceTranslationConsumer());
}
运行程序,看看结果:
所有的string都重新翻译并且替换了。
关于配置厂商
上面的例子全部都是基于 配置完毕 才可以运行的。如果你没有进行配置,程序无法进行翻译。
翻译配置适配了一部分主流厂商,目前程序当中含有的厂商:
当然,你如果有更好的选择,你也可以自行扩充翻译程序。目前先尝试配置我们的第一个翻译厂商:lingvanex
声明:本人并非给此翻译厂商打广告。此厂商配置仅作为教程流程示例,读者是否使用不做干预。
为什么使用lingvanex:在我尝试写这个程序时,浏览器在我看了几个翻译厂商都觉得各有不满意的地方,特别是在接入时对ak、sk等安全验证参数进行加密请求非常繁琐(这点在后面章节 假如当前使用的厂商不支持我想要翻译的语言很突出)。当时注册lingvanex控制台账号,甚至连注册的邮箱都无需验证,并且api的请求也只需要一个token参数。因此,这个程序第一个翻译厂商就是lingvanex。
尝试注册账号
账号注册仅需要一个邮箱,邮箱也无需验证。
在遇到让你填写国家和zip-code(邮编)的时候,随便填几个数字就行了,也不会进行验证。然后让你填信用卡号,直接原地返回键就行。账号就注册完成了。
填入token
复制你的 Api Key,回到程序,在程序根目录下找到 config.properties
文件。把拿到的Api Key放进去就行了。
恭喜,到此,配置就已经完成了。
假如翻译显示“未找到有效的服务器进行翻译”
2024/09/10 由于架构更新,此情况只会出现在继承了
MapLangSupportHandler
的翻译类当中。
假如你需要新增西班牙语。但是程序显示未找到有效的服务器进行翻译。
java.lang.Exception: 未找到有效的服务器进行翻译 ---》 es
at org.example.translation.TranslationManager.translation(TranslationManager.java:60)
at org.example.translation.AbsTranslationConsumer.onElementNeedTranslation(AbsTranslationConsumer.java:221)
at org.example.translation.AbsTranslationConsumer.accept(AbsTranslationConsumer.java:39)
at org.example.dom4j.Dom4j.translationEntireValueFolder(Dom4j.java:127)
at org.example.dom4j.Dom4j.main(Dom4j.java:35)
那么你需要在lingvanex翻译语言支持列表当中找到 Spanish :
注意 这取决于你使用的翻译类是属于哪一个导致的。由于目前程序当中仅有一个继承于
MapLangSupportHandler
类,可配置项仅为LingvanexTranslation.java
。此后若您扩展此类,请注意配置此映射Map。
回到程序,在LingvanexTranslation.java
文件找到initSupportComparisonTable
方法:
@Override
protected void initSupportComparisonTable(Map<String, String> countryComparisonTable) {
countryComparisonTable.put("en", "en_US");
countryComparisonTable.put("cs", "cs_CZ");
countryComparisonTable.put("de", "de_DE");
countryComparisonTable.put("el", "el_GR");
countryComparisonTable.put("fi", "fi_FI");
countryComparisonTable.put("fr", "fr_CA");
countryComparisonTable.put("hr", "hr_HR");
countryComparisonTable.put("in", "id_ID");
countryComparisonTable.put("it", "it_IT");
countryComparisonTable.put("ja", "ja_JP");
countryComparisonTable.put("ko", "ko_KR");
countryComparisonTable.put("nl", "nl_NL");
countryComparisonTable.put("pt", "pt_PT");
countryComparisonTable.put("ro-rRO", "ro_RO");
countryComparisonTable.put("ru", "ru_RU");
countryComparisonTable.put("sl", "sl_SI");
countryComparisonTable.put("sr", "sr-Cyrl_RS");
countryComparisonTable.put("zh", "zh-Hans_CN");
countryComparisonTable.put("zh-rHK", "zh-Hant_TW");
countryComparisonTable.put("zh-rMO", "zh-Hant_TW");
countryComparisonTable.put("zh-rTW", "zh-Hant_TW");
//添加的部分
countryComparisonTable.put("es", "es_ES");
}
填入:
countryComparisonTable.put("es", "es_ES");
第一个参数是安卓程序语言文件名称的values-xx文件在 - 后的后缀
第二个参数是刚刚在lingvanex语言支持网站查到的Language-code参数
写入完成后重新启动程序即可。
为什么要这么做?我们不能确定厂商是否支持对应翻译国家语言。因此,所有支持翻译的列表需要查询后填入。还有一个原因就是,安卓程序多语言的语言缩写是国际通用的,例如 中文为zh、英语为en、小日子语为ja。但对应部分翻译api来说,比如正在用的 lingvanex 的语言支持列表,其Language-code并不对应正常写法,如 中文为zh-Hans_CN、英语为en_US。对于api的参数请求上也需要对应的转换配置,于是这个map承担了这种对应的映射工作。
假如当前使用的厂商不支持我想要翻译的语言
在浏览器就可以查到很多翻译厂商,如果程序当中已经有对应厂商的翻译类,那和上一步基本一致,只需要新增对应支持的语言列表就行了。
但是如果程序当中不包含你查询到的厂商,那就稍微麻烦一点。需要你自己去适配对应厂商的接口。
以下流程是实操流程,接入失败并不是剧本
注册新厂商的账号、查看翻译API、查看支持的语言列表
比如我百度到一堆翻译厂商,其中 火山翻译 还没有接入,假设你就死活认定需要这个厂商,那就先进网站注册个账号,然后实名认证,然后开通控制台,然后查接入文档,账号注册之类的按照人家指引即可。
编写实体翻译类
先建个火山翻译的包,把翻译实体类写上:
翻译实体类继承于LangSupportHandler
目前这个类除了继承的两个方法空空如也。
initSupportComparisonTable
方法当中填入的是支持翻译语言的列表,这个要查了他们文档才晓得,先不搞。
requestTranslation
方法是实际请求服务器翻译的方法,那么我们直接火山翻译接口文档:
非常好。字都认识,但是一句话都没看懂。
于是找到他们的示例代码。cv大法。
复制粘贴的老毛病了,百度出来这玩意是java17的玩意。于是原地改java版本:(我额外瞄了一眼issue,果然有人骂作者。笑死,java8永远不变)
根据示例代码整合一下:
package org.example.huoshan_translation;
import com.google.gson.Gson;
import okhttp3.*;
import org.example.translation.LangSupportHandler;
import org.example.translation.PropValueSupplier;
import org.example.translation.TranslationPostBean;
import org.example.translation.TranslationResponseBean;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
public class HuoshanTranslation extends LangSupportHandler {
private String SecretAccessKey;
private String AccessKeyID;
// 请求地址
private String endpoint = "iam.volcengineapi.com";
private String path = "/"; // 路径,不包含 Query// 请求接口信息
private String service = "iam";
private String region = "cn-beijing";
private String schema = "https";
private final Sign sign;
private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
private final Gson gson = new Gson();
public HuoshanTranslation(PropValueSupplier propValueSupplier) {
super(propValueSupplier);
SecretAccessKey = propValueSupplier.get("huoshan.secretAccessKey");
AccessKeyID = propValueSupplier.get("huoshan.accessKeyID");
sign = new Sign(region, service, schema, endpoint, path, AccessKeyID, SecretAccessKey);
}
@Override
protected void initSupportComparisonTable(Map<String, String> countryComparisonTable) {
countryComparisonTable.put("ja", "ja");
}
@Override
public TranslationResponseBean requestTranslation(TranslationPostBean postBean) {
String action = "ListPolicies";
String version = "2018-01-01";
Date date = new Date();
HashMap<String, String> queryMap = new HashMap<>() {{
put("Limit", "1");
}};
byte[] body = null;
if (body == null) {
body = new byte[0];
}
String xContentSha256 = null;
try {
xContentSha256 = Sign.hashSHA256(body);
} catch (Exception e) {
throw new RuntimeException(e);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
String xDate = sdf.format(date);
String shortXDate = xDate.substring(0, 8);
String contentType = "application/x-www-form-urlencoded";
String signHeader = "host;x-date;x-content-sha256;content-type";
SortedMap<String, String> realQueryList = new TreeMap<>(queryMap);
realQueryList.put("Action", action);
realQueryList.put("Version", version);
StringBuilder querySB = new StringBuilder();
for (String key : realQueryList.keySet()) {
querySB.append(sign.signStringEncoder(key)).append("=").append(sign.signStringEncoder(realQueryList.get(key))).append("&");
}
querySB.deleteCharAt(querySB.length() - 1);
String canonicalStringBuilder = "POST" + "\n" + path + "\n" + querySB + "\n" +
"host:" + endpoint + "\n" +
"x-date:" + xDate + "\n" +
"x-content-sha256:" + xContentSha256 + "\n" +
"content-type:" + contentType + "\n" +
"\n" +
signHeader + "\n" +
xContentSha256;
String hashcanonicalString = null;
try {
hashcanonicalString = Sign.hashSHA256(canonicalStringBuilder.getBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}
String credentialScope = shortXDate + "/" + region + "/" + service + "/request";
String signString = "HMAC-SHA256" + "\n" + xDate + "\n" + credentialScope + "\n" + hashcanonicalString;
byte[] signKey = new byte[0];
try {
signKey = sign.genSigningSecretKeyV4(SecretAccessKey, shortXDate, region, service);
} catch (Exception e) {
throw new RuntimeException(e);
}
String signature = null;
try {
signature = HexFormat.of().formatHex(Sign.hmacSHA256(signKey, signString));
} catch (Exception e) {
throw new RuntimeException(e);
}
HashMap<String, String> postBody = new HashMap<>();
postBody.put("SourceLanguage", postBean.getSourceCountry());
postBody.put("TargetLanguage", postBean.getTargetCountry());
postBody.put("TextList", postBean.getSourceString());
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), gson.toJson(postBody));
Request request = new Request.Builder()
.url(schema + "://" + endpoint + path + "?" + querySB)
.header("Host", endpoint)
.header("X-Date", xDate)
.header("X-Content-Sha256", xContentSha256)
.header("Content-Type", contentType)
.header("Authorization", "HMAC-SHA256" +
" Credential=" + AccessKeyID + "/" + credentialScope +
", SignedHeaders=" + signHeader +
", Signature=" + signature)
.post(requestBody)
.build();
try {
Response execute = okHttpClient.newCall(request).execute();
String responseStr = execute.body().string();
RespBean respBean = gson.fromJson(responseStr, RespBean.class);
if (respBean != null && respBean.getResponseMetadata().getError() == null && !respBean.getTranslationList().isEmpty()) {
return new TranslationResponseBean()
.setSourceCountry(respBean.getTranslationList().get(0).getDetectedSourceLanguage())
.setSourceString(postBean.getSourceString())
.setTargetCountry(postBean.getTargetCountry())
.setTargetResult(respBean.getTranslationList().get(0).getTranslation());
} else {
RuntimeException runtimeException = new RuntimeException("server response:" + responseStr +
"\nSourceCountry:" + postBean.getSourceCountry() +
"\nTargetCountry:" + postBean.getTargetCountry() +
"\nContent:" + postBean.getSourceString());
runtimeException.printStackTrace();
translation(postBean);
}
} catch (IOException e) {
e.printStackTrace();
}
return new TranslationResponseBean()
.setTargetResult("未完成翻译")
.setSourceString(postBean.sourceString)
.setSourceCountry(postBean.sourceCountry)
.setTargetCountry(postBean.targetCountry);
}
}
写半天结果运行之后服务器报错。我尝试直接运行复制过来的Sign.java。还是报错。我谢谢你,火山。
{"ResponseMetadata":{"RequestId":"202408081750047AEBBA99E7E1594EEECF","Action":"ListPolicies","Version":"2024-08-08","Error":{"CodeN":100024,"Code":"InvalidAuthorization","Message":"Invalid 'Authorization' header, Pls check your authorization header."}}}
Process finished with exit code 0
那没办法了,只能用人家的SDK了(我们java厨师比较潦草哈:
先找到readme瞄一眼,雀氏挺不错的:
由于人家仓库示例是maven,咱项目是gradle。得maven转gradle哈。按照人家说的,加个依赖:
dependencies {
// https://mvnrepository.com/artifact/net.sourceforge.jexcelapi/jxl
implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
// https://mvnrepository.com/artifact/dom4j/dom4j
implementation 'dom4j:dom4j:1.6.1'
// https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java
implementation 'com.tencentcloudapi:tencentcloud-sdk-java:3.1.1008'
// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation 'com.google.code.gson:gson:2.10'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
//火山翻译sdk
implementation 'com.volcengine:volc-sdk-java:1.0.177'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
写完记得sync一下。
又到了写代码环节,但是这次比较简单,毕竟SDK都用上了,按照人家说的做就行。
public class HuoshanTranslation extends LangSupportHandler {
private String SecretAccessKey;
private String AccessKeyID;
private final ITranslateService translateService = TranslateServiceImpl.getInstance();
public HuoshanTranslation(PropValueSupplier propValueSupplier) {
super(propValueSupplier);
SecretAccessKey = propValueSupplier.get("huoshan.secretAccessKey");
AccessKeyID = propValueSupplier.get("huoshan.accessKeyID");
translateService.setAccessKey(AccessKeyID);
translateService.setSecretKey(SecretAccessKey);
}
@Override
protected void initSupportComparisonTable(Map<String, String> countryComparisonTable) {
countryComparisonTable.put("ja", "ja");
}
@Override
public TranslationResponseBean requestTranslation(TranslationPostBean postBean) {
TranslateTextRequest translateTextRequest = new TranslateTextRequest();
translateTextRequest.setSourceLanguage(postBean.getSourceCountry()); // 不设置表示自动检测
translateTextRequest.setTargetLanguage(postBean.getTargetCountry());
translateTextRequest.setTextList(Collections.singletonList(postBean.getSourceString()));
try {
TranslateTextResponse translateText = translateService.translateText(translateTextRequest);
if (translateText.getResponseMetadata().getError() == null &&
!translateText.getTranslationList().isEmpty()) {
return new TranslationResponseBean()
.setTargetResult(translateText.getTranslationList().get(0).getTranslation())
.setSourceString(postBean.getSourceString())
.setSourceCountry(postBean.getSourceCountry())
.setTargetCountry(postBean.getTargetCountry());
} else {
requestTranslation(postBean);
}
} catch (Exception e) {
e.printStackTrace();
}
return new TranslationResponseBean()
.setTargetResult("未完成翻译")
.setSourceString(postBean.sourceString)
.setSourceCountry(postBean.sourceCountry)
.setTargetCountry(postBean.targetCountry);
}
}
配置火山翻译AK、SK
注意:各个翻译厂商的翻译请求的安全验证都不一样。并不是都用一个token或者都用ak、sk参数。很多时候,请求的参数千奇百怪,加密方式也是一遍又一遍。火山翻译使用的ak、sk参数与lingvanex翻译的token建议全部配置在项目根目录的
config.properties
文件内。至于配置的ak、sk名字你随意填写都可,而后在你的HuoShanTranslation.java
的构造函数当中,可以使用PropValueSupplier
对象的get
方法取出对应的ak、sk。
替换当前翻译链
写完了记得去TranslationManager.java
把翻译调用链首个改成现在这个火山翻译:
package org.example.translation;
import org.example.baidu_translation.BaiduTranslation;
import org.example.huoshan_translation.HuoshanTranslation;
import org.example.lingvanex_translation.Translation;
import org.example.youdao_translation.YoudaoTranslation;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
public class TranslationManager implements TranslationAble {
private final LangSupportHandler translationHeader;
private final TranslationKeyReader keyReader = new TranslationKeyReader();
public TranslationManager() {
keyReader.read();
//先使用lingvanex翻译 找不到就用 有道翻译
//translationHeader = new Translation(keyReader).setNext(new YoudaoTranslation(keyReader));
//只使用火山翻译
translationHeader = new HuoshanTranslation(keyReader);
}
@Override
public @Nullable TranslationResponseBean translation(TranslationPostBean postBean) {
LangSupportHandler langSupportHandler = translationHeader.process(new LangSupportRequest(postBean.targetCountry));
if (langSupportHandler == null) {
new Exception("未找到有效的服务器进行翻译 ---》 " + postBean.targetCountry).printStackTrace();
return null;
} else {
return langSupportHandler.translation(postBean);
}
}
public String getValueFileCountry(String fileName, Function<String, String> translation) {
return translation.apply(fileName);
}
}
这里额外说一下这个 翻译链 怎么搞。我们刚刚新建的火山翻译类HuoshanTranslation
继承的LangSupportHandler
是一个 责任链模式
,具体表现为(简写此类):
public abstract class LangSupportHandler implements TranslationAble{
//下一个翻译者
protected LangSupportHandler next;
//设置下一个翻译者
public LangSupportHandler setNext(LangSupportHandler next) {
this.next = next;
return this;
}
//查询翻译者是否支持对应语言的翻译,若查询不到,就查询下一个翻译者是否支持,一直到某一个翻译者支持或者全部不支持抛出异常
public LangSupportHandler process(@NotNull LangSupportRequest supportRequest){
String targetCountry = getSupportLang(supportRequest.getTargetCountry());
if (targetCountry != null){
return this;
}else {
if (next == null){
return null;
}else {
return next.process(supportRequest);
}
}
}
}
如果当你有点强迫症,把所有翻译者(七龙珠)全部配置好(集齐)。你可以在TranslationManager
的构造当中这么写:
translationHeader = new HuoshanTranslation(keyReader)//火山翻译
.setNext(new LingvanexTranslation(keyReader)//lingvanex翻译
.setNext(new BaiduTranslation(keyReader)//百度翻译
.setNext(new YoudaoTranslation(keyReader)//有道翻译
.setNext(new TencentTranslation(keyReader)))));//腾讯翻译
运行程序的时候它就会一个一个查询下来。
现在可以跑一下dom4j.java
了:
基本的就到此为止了。
【新版本新增】我需要在填充式翻译当中强行翻译部分指定的条目
在使用填充式翻译时,可能出现部分条目需要强行翻译,在FilledTranslationConsumer.java
的构造当中填入强制翻译的name
属性即可:
new FilledTranslationConsumer(Arrays.asList("all_sport","tips"))
注意。强制翻译只有
FilledTranslationConsumer.java
有。AllReplaceTranslationConsumer.java
没有
转XLS表格
打开java/org/example/dom4j/StringXmlOutput.java
public static void main(String[] args) {
try {
outputAll("C:\\Users\\hp\\Desktop\\values");
} catch (DocumentException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (WriteException e) {
throw new RuntimeException(e);
}
}
在main方法当中输入你翻译的文档地址就行了,运行程序之后,会在项目根目录下生成output.xls
,所有的语言都会集合到一张表当中:
程序当中额外标注了对应列的语言类型,是由key值为“lang”对应的。但是dom4j的排序似乎有点问题,lang的位置总是乱跑。可以搜索一下,然后手动置顶即可:
翻译后的XLS表格重新填充到XML当中
注意:此填充是基于上一个部分的xls表格格式进行的。
注意:翻译后的xml表格必须包含
lang
字段,并且此行必须基于所有翻译行的顶部
用上一个xls表格举例子,我们现在得到的表格内包含en
和ja
两种语言。我们将此文件送给专业人士进行日语翻译,并且翻译人士翻译之后,在文档标注出带颜色的部分是修改部分,例如这样:
打开Jxl2Dom4j.java
:
public static void main(String[] args) {
try {
xls2Xml("C:\\Users\\hp\\IdeaProjects\\demo1\\untitled1\\output.xls",
"C:\\Users\\hp\\Desktop\\values\\values-ja\\strings.xml",
0,"ja","C:\\Users\\hp\\IdeaProjects\\demo1\\untitled1\\output.xml");
} catch (IOException e) {
throw new RuntimeException(e);
} catch (BiffException e) {
throw new RuntimeException(e);
} catch (DocumentException e) {
throw new RuntimeException(e);
}
}
参数标注:
/**
* xls转xml
* @param translationXlsPath 已经翻译的xls表格
* @param xml 翻译语言的对应的xml文件
* @param titleRow lang 出现在的第几行
* @param witchLang 转换的是哪个语言
* @param outputFilePath 结果输出位置
* @throws IOException
* @throws BiffException
* @throws DocumentException
*/
private static void xls2Xml(String translationXlsPath,String xml,int titleRow,String witchLang,String outputFilePath) throws IOException, BiffException, DocumentException {
//...
}
运行即可:
把生成的这个文件的内容复制粘贴到对应语言文件即可。
鸣谢: