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)选择对应文件保存到制定路径即可。

相关推荐
xvch6 小时前
Kotlin 2.1.0 入门教程(八)
android·kotlin
limingade7 小时前
手机app如何跳过无障碍权限实现弹框自动点击-ADB连接专题
android·adb·智能手机·蓝牙电话·手机提取通话声音
limingade7 小时前
如何跨互联网adb连接到远程手机-蓝牙电话集中维护
android·arm开发·adb·智能手机·信息与通信·蓝牙电话
dal118网工任子仪9 小时前
79,【3】BUUCTF WEB [GXYCTF2019]BabysqliV3.0
android·前端
东京老树根9 小时前
Android - 通过Logcat Manager简单获取Android手机的Log
android·智能手机
天才奇男子9 小时前
数据库用户管理
android·数据库·adb
aerror17 小时前
Macos下交叉编译安卓的paq8px压缩算法
android·macos
zhangphil18 小时前
Android BitmapShader简洁实现马赛克,Kotlin(二)
android·kotlin
我的青春不太冷18 小时前
在Android中通过JNI实现Java与C++的交互:Hello World示例
android·java·开发语言·c++·经验分享·程序人生
ansondroider18 小时前
Android GLSurfaceView 覆盖其它控件问题 (RK平台)
android·overlay·glsurfaceview·surfaceview