使用Python生成Xcode的Localizable.strings文件

本文介绍了一个使用Python编写的脚本,用于自动化生成iOS项目的Localizable.strings文件,以简化国际化过程。脚本读取项目目录,根据指定文件类型和国际化前缀,生成内容并保存到Localizable.strings文件。通过命令行调用,用户可以自定义项目根目录、文件类型列表和国际化前缀。脚本包括文件内容读取、目录检查、文件处理和统计信息等功能,提高工作效率。
部署运行你感兴趣的模型镜像

众所周知,iOS的国际化是需要一个一个字符串写入到Localizable.strings文件中,在一个项目中,肯定会有N多个这样的字符串要去手动添加,这样做真的很操作。于是就用Python写了一点点代码,自动生成strings文件。

起因

最近项目急于上线,忙的手忙脚乱,两个月时间内,App从一个文件都没有,到现在有上百个代码文件,也不容易啊。由于App分成中文和英文版本,之前在搞这个的时候靠的都是自己手动添加,通在在Xcode里搜索国际化前缀,然后一个一个去copy和paste,充分发挥了写代码靠百度的精神。但是东西实在太多,用了一天的时间来搞这个东西,还是没搞完。没办法了,才打算抽点时间写这么个小工具出来。这样比自己傻不愣登一个一个去添加省时省力多了。现在把代码发出来,希望也能对有需要的朋友有所帮助。

使用方法

该脚本也很简单,只要传入一个项目根目录,一个文件类型列表,一个国际化前缀,就会在根目录下生成一个Localizable.strings文件,然后可以将里面的内容Copy到Xcode中,或者直接手动替换文件即可。至于为什么不直接自动替换文件,我的想法是如果直接替换,要是脚本出现了什么问题,搞坏了别人的项目,那我的罪过就大了。打开终端,使用格式如下:

python check.py [ROOTDIR] [EXTENSION,...] [PREFIX]

效果展示

使用过后的效果如下图所示,生成的Localizable.strings文件在根目录下:
效果展示

生成的Localizable.strings文件就位于命令行中提供的根目录下。

代码说明

用python写这种小工具真的是很爽,而且又很简单,再配上VSCode,简直不要太爽。我越来越喜欢VSCode这个编辑器了,上个美图先。
VSCode

使用方法

在使用的时候,可以传入指定的一些参数其中,必传的参数包括项目根目录和国际化前缀。还可以指定文件扩展列表,用于限定只读取指定扩展类型的文件。。于是就有下面一段代码:
程序开始
首先判断,用户输入的参数是否是合乎规则的命令,参数值最小是3个,所以当输入不合法,告知用户使用方法。
用法展示

读取已有内容

首先,定义了三个全局变量,用于存储一些信息,包括原文件中已有的内容,新添加的部分以及检查的文件数目。
全局变量的定义
首先,我们确定两个国际化文件的所在路径(我们将文件建立在项目的根路径下):
文件路径
然后,我们开始读取已有数据,用于与以后新检测到的数据做对比,如下所示:
填充已有数据
我们知道,Localizable.strings文件中的内容,是一种这样的格式:

"文件内容" = "文件内容";

我们当然不至于那么智能,能够把中文输入变成英文或其他语言的输出,在此,建立的文件仅是一个中文的Localizable.strings文件,左边的内容是等于右边的内容的,于是就有了这样一个正则表达式:

'\\"(.+?)\\" = \\".+?\\";'

其中读取文件内容的readContentsWithPattern,包括两个参数,一个fileHandle表示文件句柄,一个pattern表示编译过的正则表达式。其代码如下所示:
读取文件内容
从代码可以看出,该函数的作用是根据正则表达式,匹配出合适的内容,并存于一个列表。

检查目录

读取完已有内容之后,我们就可以开始检查项目根目录下的所有内容了。这是一个递归的过程,如下图所示:
检查目录
这个过程有以下几步:
1. 如果当前检查项是一个目录,记录原来所在的路径,并切换到新目录下
2. 读取该目录下的所有内容,包括文件和目录
3. 过滤掉隐藏文件(文件名第一个字符是”.”)
4. 遍历文件列表,获取文件的绝对路径,如果当前检查项是一个目录,重复1〜3步
5. 如果当前检查项是一个文件,那么文件数+1, 开始处理文件。
6. 文件处理结束,切换回原来的路径

在这里可能有个问题,为什么要切换回原来的路径呢?从代码中我们可以知道,我们的检查是按深度遍历的,即假如有以下目录结构:
(A(B(C(D, E)))),那么,我们最先进入的目录应该是D,当处理D目录结束之后,如果不切换回原来的路径,那么我们读取到的E的绝对路径将不会是A-B-C-E,而是A-B-C-D-E,显然,这是不正确的。

文件处理

文件处理包括以下几个步骤:
1. 首先,我们过滤掉.strings文件,因为该文件中的内容全部会在检查过程中被检查出来。
2. 检查文件类型,如果符合用户指定的文件类型,则继续下列步骤
3. 建立正则表达式,打开对应文件,读取内容
4. 如果读取到的内容在已有列表中已存在,那么不记录该项,否则将该项添加到新添加的列表中。
5. 读取结束,关闭文件。

搞iOS开发的人都知道,我们使用国际化的格式是固定的

NSLocalizedString(@"内容", nil);

一般,第二个参数都是nil,所以在此只做此特殊处理,不考虑别的情况。毕竟只是一个自用的小工具罢了。于是就有以下的正则表达式

sys.argv[len(sys.argv) - 1] + '\\(@\\"(.+?)\\"' + ', nil\\)'

文件处理的全过程如下所示:
文件处理

到此,这个小工具就已经基本完成了, 但是还差点功夫,内容并没有写入到对应的文件中去。于是加个文件写入的代码:
文件写入

将文件内容写入到根目录下的Localizable.strings和newly-Localizable.string文件中,其中在原文件中我们加入一行注释,用于区分原内容与新添加的内容。最后,输出统计信息,关闭文件。

统计信息

统计信息包括以下几个信息:
1. 共有多少个条目
2. 新增加了多少个条目
3. 检查的文件总数

统计信息

至此,这个小工具就编写完毕了。该工具仅为了方便工作而编写,没有考虑脚本的健壮性,扩展性及设计上的一些问题,只用于个人。最后把全部代码按顺序贴出来

#!/usr/bin/python 
# -*- coding:utf-8 -*-
# Filename: check.py
# Author: WangLuofan

import os;
import sys;
import re;

contentsList = [];                  # 原文件中已有内容
newlyAddedList = [];                # 新添加部分内容
fileCheckedCount = 0;               # 文件数目

def showUsage():
    print("usage: python " + sys.argv[0] + " [ROOTDIR] ([EXTENSION, ...]) [PREFIX]");

def fillDataFromExistedLocalizedFile(filePath):
    global contentsList;

    if(os.path.exists(filePath) == False):
        return ;

    pattern = re.compile('\\"(.+?)\\" = \\".+?\\";');
    fileHandle = open(filePath, "r");
    if(fileHandle != None):
        contentsList = readContentsWithPattern(fileHandle, pattern);
    fileHandle.close();

    return ;

def handleFile(fileName):
    global contentsList;
    global newlyAddedList;

    # 过滤掉.strings文件
    if(fileName.endswith('strings') == True):
        return ;

    isValidFile = False;
    for extension in fileExtensions():
        if str.endswith(fileName, extension) == True:
            isValidFile = True;
            break ;

    if isValidFile == False and len(sys.argv) >= 4:
        return ;

    pattern = re.compile(sys.argv[len(sys.argv) - 1] + '\\(@\\"(.+?)\\"' + ', nil\\)')

    filePath = os.path.abspath(fileName);
    file = open(filePath, mode='r');
    if(file == None):
        print("Open File " + filePath + " Failed!");
        return ;

    print("Checking File: " + filePath);
    itemList = readContentsWithPattern(file, pattern);

    for item in itemList:
        if(item in contentsList):
            continue ;
        newlyAddedList.append(item);

    # 关闭文件
    file.close();
    return ;

def readContentsWithPattern(fileHandle, pattern):
    # 读取文件内容
    fileContent = "";

    for line in fileHandle:
        fileContent += line;

    groups = re.findall(pattern, fileContent);

    itemList = [];
    for value in groups:
        itemList.append(value);

    return itemList;

def checkDir(path):
    global fileCheckedCount;

    print;
    print("Current Directory: " + path);
    if(os.path.isdir(path) == False):
        return ;

    # 记录原来的路径
    currentDIR = os.getcwd();

    # 切换到新路径
    os.chdir(path);
    files = os.listdir(path);

    for file in files:

        # 过滤掉隐藏文件
        if(str.startswith(file, '.') == True):
            continue ;

        filePath = os.path.abspath(file);
        if(os.path.isdir(filePath) == True):
            checkDir(filePath);
        else:
            fileCheckedCount += 1;
            handleFile(file);

    # 切换回原路径
    os.chdir(currentDIR);
    return ;

def fileExtensions():
    if(len(sys.argv) < 4):
        return [];

    extensions = sys.argv[2];
    extensions = extensions.replace(' ', '');

    return str.split(extensions, ',');

def startInternal():
    global contentsList;
    global newlyAddedList;

    rootPath = os.path.expanduser(sys.argv[1]);             # 项目根目录
    localizedFilePath = os.path.abspath(os.path.join(rootPath, 'Localizable.strings')); # 国际化文件目录
    newlyFilePath = os.path.abspath(os.path.join(rootPath, 'newly-Localizable.strings'));

    # 建立国际化文件
    fillDataFromExistedLocalizedFile(localizedFilePath);

    # 检查所有目录
    checkDir(os.path.abspath(rootPath));

    newlyAddedList = list(set(newlyAddedList));

    # 打开国际化文件
    localizedFile = open(localizedFilePath, "w");
    newlyFile = open(newlyFilePath, "w");

    if(localizedFile != None):
        localizedFile.write('//This File Generated Automatic, and you cannot modify this file manually.' + os.linesep);

        for item in contentsList:
            content = '"' + item + '" = "' + item + '";' + os.linesep;
            localizedFile.write(content);

        localizedFile.write('//--------Newly Added--------' + os.linesep);
        for item in newlyAddedList:
            content = '"' + item + '" = "' + item + '";' + os.linesep;
            newlyFile.write(content);
            localizedFile.write(content);

        print ;
        print 'Localizable.string Created At: ' + localizedFilePath;

        statistic();

    # 关闭国际化文件
    if(localizedFile != None):
        localizedFile.close();
    if(newlyFile != None):
        newlyFile.close();
    return ;

def statistic():
    print ;
    print ;
    print '--------------------STATISTIC--------------------';
    print ;
    print 'Total Items: ' + str(len(contentsList) + len(newlyAddedList)),
    print '  Newly Added: ' + str(len(newlyAddedList)),
    print '  Files Checked: ' + str(fileCheckedCount);
    print ;
    print '--------------------STATISTIC--------------------';
    print ;
    print ;

def start():
    if(len(sys.argv) < 3):
        showUsage();
        return ;

    startInternal();

    return ;

if __name__ == "__main__":
    start();

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值