Android多语言开发自动化生成工具

在做 Android 开发的过程中,经常会遇到多语言开发的场景,尤其在车载项目中,多语言开发更为常见。对应多语言开发,通常都是在中文版本的基础上开发其他国家语言,这里我们会拿到中-外语言对照表,这里的工作难度其实并不高,但是工作量却是非常的大,而且都是复制/粘贴的无聊操作,如何能快速的完成这种简单重复的操作呢?这里我们就来简单实现一下。

一、准备工作

1、多语言需求

假如我们需要中文、英文和俄文三种语言的开发,同时我们拿到了多语言的翻译表格:

模块 name zh en ru
SystemUI cancel 取消 Cancel отмен
SystemUI save 保存 Save сохран
SystemUI file_name 文件名称 File Name Имя файла
Launcher save_path 保存路径 Saved To Путь сохранения.
Launcher text_error_tip 字数超过限制 The number of words exceeds the limit. Число слов превышает предел

可以看到,对于车载开发来说,多语言开发肯定是所以应用都需要修改的,这里以 SystemUI 和 Launcher 为例。其中 name 表示在 strings.xml 中的 name 字段,zh、en 和 ru 分别表示中文、英文和俄文的简写。

2、制作xls表格

这里我们用的是 xls(暂时只支持 xls 格式的表格解析)的表格来罗列国际化的语言字段,形如下表这样的 translation.xls。

这里使用两个 Sheet 分别存在 SystemUI 和 Launcher 多语言数据,多模块继续增加 Sheet 即可(这里的 Sheet 其实就是 string.mxl 的数量)。再看一下 Launcher 的表格数据:

表格转化

在实际的操作中,我们创建的表格都是 xlsx 格式的表格文件,直接转换一下即可,操作如下:

1)点击表格左上角的文件。

2)这里选择另存为或导出都可以。

3)另存为在保存文件是将格式修改为 Excel 97-2003 工作簿即可。

同样选择导出时也是同样的选择:

二、功能实现

这里我们选择使用 Android 项目来实现多语言 strings.xml 的自动化生成工作,所以先创建一个 Android 项目,然后按照下面的步骤一步一步实现即可。

1、依赖引用

我们是基于 jxl 进行,所以还需要依赖一个 jxl。

bash 复制代码
dependencies{
 	implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
}

2、解析工具类

实现 xls 文件解析及生成 strings.xml 的工具类。

java 复制代码
import org.jxls.reader.XLSReader;
import jxl.Workbook;
import jxl.Sheet;
import jxl.Cell;
import java.io.File;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Map;

public class TranslationHandler {

    private static final HashMap<Integer, String> codeMap = new HashMap<>();
    private static final HashMap<String, ArrayList<TranslationBean>> keyAndValueMap = new HashMap<>();

    private static final String DIR = "/storage/emulated/0/Download/";
    private static final String XLS_PATH = DIR + "translation.xls";

    private static final File xlsFile = new File(XLS_PATH);
    private static final WorkbookSettings workbookSettings = new WorkbookSettings();

    static {
        if (!codeMap.isEmpty()) codeMap.clear();
        if (!keyAndValueMap.isEmpty()) keyAndValueMap.clear();

        // 设置编码防止其他国字乱码
        workbookSettings.setEncoding("ISO-8859-1");
    }

    // 开始入口函数
    public static void startAnalyze(){
        int sheetNums = 0;
        try{
            sheetNums = Workbook.getWorkbook(xlsFile, worlkbookSettings).getNumberOfSheets();
            for (int sheetNum = 0; sheetNum < sheetNums; sheetNUm++)
                // 第2列(column=B)开始国际化,一共有3列是需要国网示化
                handleXlsExcel(sheetNum, startColumn: 1, 3);
            }
        } catch (Exception e){
            e.printStackTrace();
        }

    /**
     * @param sheetNum: 表示sheet页数量(0表示第1张sheet)
     * @param startColumn: 从0开始,第几列开始是国际化
     * @param columnCount: 一共有多少列是国际化
     */
    private static void handleXlsExcel(int sheetNum, int startColumn, int columnCount) throws Exception {
        // workbook 与 sheet 是一对一
        Workbook workbook = Workbook.getWorkbook(xlsFile, workbookSettings);
        Sheet sheet = workbook.getSheet(sheetNum);

        System.out.println("sheet0 = " + sheet.getName());
        // 表示从第1行开始读取
        for (int row = 0; row < sheet.getRows(); row++) {
            if (row == 0) {
                for (int column = 0; column < columnCount; column++) {
                    int columnIndex = startColumn + column;
                    // B1, C1, D1单元格的内容表示国家代码
                    String code = sheet.getCell(columnIndex, row).getContents();
                    codeMap.put(columnIndex, code);
                    keyAndValueMap.put(code, new ArrayList<>());
                }
            } else {
                // A1 ~ A[num] 单元格
                String key = sheet.getCell(0, row).getContents();
                if (key == null || "".equals(key)) break;
                for (int column = 0; column < columnCount; column++) {
                    int columnIndex = startColumn + column;
                    String code = codeMap.getOrDefault(columnIndex, "null");
                    TranslationBean bean = new TranslationBean(
                            key,
                            sheet.getCell(columnIndex, row).getContents()
                    );
                    ArrayList<TranslationBean> translationList = keyAndValueMap.get(code);
                    if (translationList != null) {
                        translationList.add(bean);
                    }
                }
            }
        }
        workbook.close();
        File dir = new File(DIR, sheet.getName());
        if (!dir.exists()) dir.mkdir();
        // 使用 try-with-resources 确保文件流正确关闭
		for (String code : keyAndValueMap.keySet()) {
			File defaultDir = new File(dir, "values-" + code);
			if (!defaultDir.exists() && !defaultDir.mkdirs()) {
				System.err.println("Failed to create directory: " + defaultDir.getAbsolutePath());
				continue;
			}
			File file = new File(defaultDir, "strings.xml");
			try {
				if (!file.exists() && !file.createNewFile()) {
					System.err.println("Failed to create file: " + file.getAbsolutePath());
					continue;
				}
				System.out.println("Creating or updating file at: " + file.getAbsolutePath());

				try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
					bw.write("<resources>");
					bw.newLine();
					for (TranslationBean value : keyAndValueMap.get(code)) {
						bw.write("\t<string name=\"" + escapeXml(value.getKey()) + "\">" + escapeXml(value.getValue()) + "</string>");
						bw.newLine();
					}
					bw.write("</resources>");
				}
				System.out.println("成功生成文件: " + file.getAbsolutePath());
			} catch (Exception e) {
				e.printStackTrace();
				System.err.println("Error writing to file: " + file.getAbsolutePath());
			}
		}
    }

    private static String escapeXml(String input) {
        return input.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "'");
    }

    static class TranslationBean {
        private final String key;
        private final String value;

        public TranslationBean(String key, String value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public String getValue() {
            return value;
        }
    }
}

这里只需要调用 startAnalyze() 方法就可以开始自动化生成多语言 strings.xml 文件,但是需要有一个前提,那就是在对应目录中放置上面的 .xls 文件,这里我们放置在 /storage/emulated/0/Download/ 下,文件名为 translation.xls。

2、开始接口调用

如果是调用开始接口是很简单的,在我们的 Activity 中直接调用或者增加一个按钮再点击事件中调用 TranslationTools.startAnalyze() 方法即可。但是在 Android 文件读写是需要相关权限的,这里我就直接上代码了,对于代码的理解部分可以参考《Android 开发中的权限申请》

添加静态权限

XML 复制代码
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

增加动态权限

java 复制代码
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M
        && context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {//请求权限
    ((Activity)context).requestPermissions(new String[]{
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}

外部权限设置

java 复制代码
public static boolean checkStorageManagerPermission(Context context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
        Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
        context.startActivity(intent);
        return false;
    }
    return true;
}

在拿到所有权限后就可以调用上面的 startAnalyze() 开始接口了。

三、文件生成/导出

1、文件生成

在执行上面的 startAnalyze() 方法后,会在同目录 /storage/emulated/0/Download/ 下生成如下结构的文件:

这里 translation.xls 是我们最开始保存的 .xls 文件,而 SystemUI 和 Launcher 文件夹及下面的文件则是我们执行完代码自动生成的。下面我们简单看一下其中的文件。

可以看到这里的 SystemUI 对应的多语言 strings.xml 文件都是与上面的 .xls 表格对应的。

2、文件导出

1)在 Studio 中选择 View > Tool Windows > Device File Explorer,就会显示虚拟机的存储信息。

2)在虚拟机的存储设备中,找到 mnt > sdcard > Download,就会看到上面生成文件列表。

3)选择对应文件保存到制定路径即可。

相关推荐
用户20187928316711 小时前
AMS和app通信的小秘密
android
用户20187928316711 小时前
ThreadPoolExecutor之市场雇工的故事
android
诺诺Okami11 小时前
Android Framework-Launcher-InvariantDeviceProfile
android
Antonio91512 小时前
【音视频】Android NDK 与.so库适配
android·音视频
sun00770021 小时前
android ndk编译valgrind
android
AI视觉网奇1 天前
android studio 断点无效
android·ide·android studio
jiaxi的天空1 天前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet1 天前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin1 天前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo030519871 天前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin