# 批量翻译引擎(BatchTranslationEngine)

通过实现 BatchTranslationEngine 接口,可以为翻译引擎增加批量翻译支持,显著提升在线翻译的效率。

批量翻译引擎扩展了 TranslationEngine 接口,通过一次翻译多个文本来减少网络往返次数,在翻译数百上千个词条时可以大幅提高整体翻译速度。

建议:推荐继承 BaseBatchTranslationEngine 而非直接实现 BatchTranslationEngine 接口,基类提供了 translate() 方法的自动委托实现。

# 快速开始

public class MyBatchTranslationEngine extends BaseBatchTranslationEngine {

    @NonNull
    @Override
    public String name() {
        return "批量翻译引擎";
    }

    @NonNull
    @Override
    public List<String> loadSourceLanguages() {
        return List.of("auto", "zh", "en", "ru");
    }

    @NonNull
    @Override
    public List<String> loadTargetLanguages(String sourceLanguage) {
        return List.of("zh", "en", "ru");
    }

    @Override
    public BatchingStrategy createBatchingStrategy() {
        // 单批次最多 100 条,总字符数最多 4500
        return new DefaultBatchingStrategy(100, 4500);
    }

    @NonNull
    @Override
    public String[] batchTranslate(String[] texts, String sourceLanguage, String targetLanguage) throws IOException {
        // 调用支持批量翻译的 API
        return callBatchTranslationAPI(texts, sourceLanguage, targetLanguage);
    }

    private String[] callBatchTranslationAPI(String[] texts, String sourceLanguage, String targetLanguage) throws IOException {
        // 这里调用支持批量翻译的翻译 API
        // 返回翻译后的文本数组,长度必须与输入数组相同
        return new String[texts.length];
    }
}

# 接口概览

# BatchTranslationEngine 接口

BatchTranslationEngine 继承自 TranslationEngine,新增以下方法:

方法 说明
String[] batchTranslate(String[] texts, String sourceLanguage, String targetLanguage) 批量执行文本翻译(子线程)
BatchingStrategy createBatchingStrategy() 创建分批策略

继承的 TranslationEngine 方法请参考 翻译引擎文档

# BaseBatchTranslationEngine 基类

BaseBatchTranslationEngine 提供了 BatchTranslationEngine 接口的基础实现:

  • 继承自 BaseTranslationEngine,拥有其所有默认实现
  • 提供默认的分批策略(单批次最多 50 条,总字符数最多 5000)
  • 自动将 translate() 委托给 batchTranslate() 实现
  • 子类只需实现 batchTranslate()createBatchingStrategy() 方法

# BatchingStrategy 接口

分批策略接口,用于控制翻译任务的拆分:

方法 说明
void reset() 重置状态,准备开始新批次
boolean tryAdd(String text) 尝试将文本加入当前批次

# DefaultBatchingStrategy 类

默认分批策略实现,基于词条数和数据大小进行限制:

方法 说明
DefaultBatchingStrategy(int maxCount, int maxDataSize) 构造函数
int getTextDataSize(String text) 计算文本数据大小(可重写)

# 详细说明

# batchTranslate

@NonNull
String[] batchTranslate(String[] texts, String sourceLanguage, String targetLanguage) throws IOException

批量执行文本翻译。此方法运行在 子线程,可能被 多次调用 以翻译不同批次的文本。

参数:

  • texts - 待翻译的文本数组,不为 null 且不为空
  • sourceLanguage - 源语言代码
  • targetLanguage - 目标语言代码

返回值: 翻译后的文本数组,长度必须与输入数组相同,不能为 null

异常: IOException - 翻译过程中发生的 IO 异常(如网络错误)

线程: 子线程

注意:返回的数组长度必须与输入数组完全相同,且顺序一一对应。

# createBatchingStrategy

BatchingStrategy createBatchingStrategy()

创建分批策略,用于控制翻译任务的拆分。

返回的 BatchingStrategy 实例可以维护内部状态(如累计字节数、Token 数等),实现增量计算以避免重复处理开销。实例会被复用,每次开始新批次时会调用 reset()

返回值: 分批策略实例

线程: 调用方线程

默认实现: BaseBatchTranslationEngine 提供的默认实现为 new DefaultBatchingStrategy(50, 5000),即单批次最多 50 条,总字符数最多 5000。

# 分批策略

# BatchingStrategy 接口

分批策略用于控制如何将大量翻译任务拆分为多个批次。实现类可以在内部维护任意状态(如累计字符数、字节数、Token 数等),在 tryAdd() 时进行增量计算。

# reset

void reset()

重置状态,准备开始新批次。此方法会在每个批次开始前被调用,实现类应清空所有累计状态。

# tryAdd

boolean tryAdd(String text)

尝试将文本加入当前批次。

实现类应根据自身的限制条件(如最大词条数、最大字符数、最大字节数等)判断是否可以容纳新文本。如果可以容纳,应更新内部状态并返回 true

参数:

  • text - 待加入的文本,不为 null

返回值:

  • true - 表示成功加入当前批次
  • false - 表示当前批次已满

MT 侧处理流程:

  1. 返回 true:将该文本加入当前批次
  2. 返回 false 且当前批次不为空:翻译当前批次,调用 reset() 开始新批次,再次使用该文本调用 tryAdd 尝试加入(即该文本会被调用两次)
  3. 返回 false 且当前批次为空:强制将该文本加入当前批次并翻译,然后调用 reset() 开始新批次(该文本仅调用一次)

# DefaultBatchingStrategy 类

默认分批策略实现,基于词条数和数据大小进行限制。

# 构造函数

public DefaultBatchingStrategy(int maxCount, int maxDataSize)

参数:

  • maxCount - 单批次最大词条数,0 或负数表示无限制
  • maxDataSize - 单批次最大数据大小,0 或负数表示无限制

# getTextDataSize

protected int getTextDataSize(String text)

计算文本的数据大小。

参数:

  • text - 待计算的文本

返回值: 文本的数据大小

默认实现: 返回 text.length()

自定义计算逻辑:

子类可重写此方法以实现自定义的计算逻辑,例如按 UTF-8 编码字节数:

@Override
protected int getTextDataSize(String text) {
    return text.getBytes(StandardCharsets.UTF_8).length;
}

# 完整示例

# 使用分割线合并文本

通用方案:对于只支持单文本翻译的 API,可以通过分割线将多个文本合并为一个请求,翻译后再拆分结果,从而实现批量翻译功能。

以下示例展示如何使用这种通用方案实现批量翻译引擎:

public class GoogleTranslationEngine extends BaseBatchTranslationEngine {
    private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
            .callTimeout(8, TimeUnit.SECONDS)
            .build();

    @NonNull
    @Override
    public String name() {
        return "{google_translator}";
    }

    @NonNull
    @Override
    public List<String> loadSourceLanguages() {
        return List.of("auto", "zh", "en", "ru");
    }

    @NonNull
    @Override
    public List<String> loadTargetLanguages(String sourceLanguage) {
        return List.of("zh", "en", "ru");
    }

    @Override
    public BatchingStrategy createBatchingStrategy() {
        // 实际限制5000,留点余量
        return new DefaultBatchingStrategy(100, 4500) {
            @Override
            protected int getTextDataSize(String text) {
                return text.length() + 10; // 预留分割线大小
            }
        };
    }

    /**
     * 由于我们使用的谷歌翻译接口只能翻译单个文本,因此这里使用分割线组合文本,翻译完再拆分
     */
    @NonNull
    @Override
    public String[] batchTranslate(String[] texts, String sourceLanguage, String targetLanguage) throws IOException {
        // 生成一个分割线,确保原文里面没有
        String divider = "--------";
        while (containsDivider(texts, divider)) {
            divider += "--";
        }

        boolean tryAgain = true;

        while (true) {
            // 拼接所有文本
            String mergedText = Arrays.stream(texts).collect(Collectors.joining("\n" + divider + "\n"));

            // 调用单文本翻译接口
            String translatedText = translate(mergedText, sourceLanguage, targetLanguage);

            // 分割翻译结果
            String[] array = translatedText.split("\n" + divider + "\n");

            // 确保前后数量一致
            if (array.length == texts.length) {
                return array;
            }

            // 如果不一致,增加分割线长度再试一次
            if (tryAgain) {
                tryAgain = false;
                //noinspection StringConcatenationInLoop
                divider += "--";
                continue;
            }

            // 输出错误数据
            getContext().log("Original(" + texts.length + "):\n" + mergedText);
            getContext().log("Translated(" + array.length + "):\n" + translatedText);
            if (translatedText.length() > 100) {
                translatedText = translatedText.substring(0, 99) + "...";
            }
            throw new IOException("Translation failed: " + translatedText);
        }
    }

    private static boolean containsDivider(String[] texts, String divider) {
        for (String s : texts) {
            if (s.contains(divider)) {
                return true;
            }
        }
        return false;
    }

    @NonNull
    @Override
    public String translate(String text, String sourceLanguage, String targetLanguage) throws IOException {
        // 中国大陆不支持直接访问 translate.googleapis.com,这里使用 IP 直连
        // 142.250.0.160  142.250.0.161  142.250.0.183
        // 142.250.0.184  172.217.4.108  142.250.4.160
        String url = "http://142.250.0.160/translate_a/single";
        FormBody formBody = new FormBody.Builder()
                .add("client", "gtx")
                .add("dt", "t")
                .add("sl", sourceLanguage)
                .add("tl", targetLanguage)
                .add("q", text)
                .build();
        Request request = new Request.Builder()
                .post(formBody)
                .url(url)
                .header("Host", "translate.googleapis.com")
                .header("User-Agent", "Mozilla/5.0 (Linux; Android 6.0;)")
                .build();
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("HTTP response code: " + response.code());
            }
            ResponseBody body = response.body();
            if (body == null) {
                throw new IOException("Body is null");
            }
            return getResult(body.string());
        }
    }

    private static String getResult(String string) throws IOException {
        if (!string.startsWith("[[[")) {
            throw new IOException("Parse result failed: " + string);
        }
        JSONArray array = new JSONArray(string).getJSONArray(0);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < array.size(); i++) {
            sb.append(array.getJSONArray(i).getString(0));
        }
        return sb.toString();
    }
}

# 自定义分批策略

以下示例展示如何实现基于 Token 数的自定义分批策略:

public class TokenBasedBatchingStrategy implements BatchingStrategy {
    private final int maxTokens;
    private int currentTokens;

    public TokenBasedBatchingStrategy(int maxTokens) {
        this.maxTokens = maxTokens;
    }

    @Override
    public void reset() {
        currentTokens = 0;
    }

    @Override
    public boolean tryAdd(String text) {
        int tokens = estimateTokenCount(text);
        if (currentTokens > 0 && currentTokens + tokens > maxTokens) {
            return false;
        }
        currentTokens += tokens;
        return true;
    }

    private int estimateTokenCount(String text) {
        // 简单估算:英文按空格分词,中文按字符数
        if (text.matches(".*[\\u4e00-\\u9fa5]+.*")) {
            return text.length(); // 中文字符数
        } else {
            return text.split("\\s+").length; // 英文单词数
        }
    }
}

使用自定义策略:

@Override
public BatchingStrategy createBatchingStrategy() {
    return new TokenBasedBatchingStrategy(2000);
}

# 接口配置

所有实现的批量翻译引擎接口都必须在模块的 build.gradle 中注册:

mtPlugin {
    pluginID = "com.example.myplugin"
    versionCode = 1
    versionName = "v1.0"
    name = "翻译插件"

    interfaces = [
        "com.example.myplugin.GoogleTranslationEngine",
    ]
}

# 相关接口