├── .gitignore ├── .idea ├── .gitignore ├── dictionaries │ └── Administrator.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── stringCovertTool.iml └── vcs.xml ├── Constant.py ├── Export.py ├── ExportTestUnit.py ├── Import.py ├── ImportTestUnit.py ├── LogUtils.py ├── ParseUtils.py ├── ReadMe.md ├── TkExport.py ├── TkImport.py ├── image ├── export xml.png └── import xml.png ├── pick_me.py └── test ├── App Native.xlsx ├── Output.xls ├── strings_me.xml ├── strings_moment.xml ├── values-de ├── strings_me.xml └── strings_moment.xml ├── values-zh ├── strings_me.xml └── strings_moment.xml └── values ├── strings_me.xml └── strings_moment.xml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Default ignored files 3 | /workspace.xml -------------------------------------------------------------------------------- /.idea/dictionaries/Administrator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/stringCovertTool.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Constant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import os 5 | import io 6 | import sys 7 | 8 | SUCCESS = 0 9 | ERROR_DIR_NOT_EXIST = 1 # type: int #目标目录不存在 10 | ERROR_IMPORT_INPUT = 2 # type: int #输入不合法 11 | ERROR_KEY_NOT_FOUND = 3 # 没找到 key 12 | ERROR_MODULE_NOT_FOUND = 4 # 没找到 key 13 | EXCEPTION_EXL_FILE = 5 # 文件为空或者第一行没有内容 14 | ERROR_EXCEL_NOT_EXIST = 6 # 文件为空或者第一行没有内容 15 | ERROR_XML_FILE_NOT_EXIST = 7 # 文件不存在 16 | 17 | class Error: 18 | desc = None # type: str 19 | code = -1 # type: int 20 | 21 | def __init__(self, code, desc=""): 22 | self.desc = desc 23 | self.code = code 24 | 25 | def isError(self): 26 | return self.code != SUCCESS 27 | 28 | def get_desc(self): 29 | des = "" 30 | if self.code == SUCCESS: 31 | des = "操作成功!" 32 | elif self.code == ERROR_DIR_NOT_EXIST: 33 | des = "目录不存在" 34 | elif self.code == ERROR_IMPORT_INPUT: 35 | des = "输入错误,请查看以下项:\n1. 同时输入目标语言和文件,或者输入目标目录;\n2. 表格中不存在目标语言" 36 | elif self.code == ERROR_KEY_NOT_FOUND: 37 | des = "表格结构错误:\n没有检索到 key 列,key 列需命名为 Android keyName" 38 | elif self.code == ERROR_MODULE_NOT_FOUND: 39 | des = "表格结构错误:\n没有检索到 Module 列,key 列需命名为 Android module" 40 | elif self.code == EXCEPTION_EXL_FILE: 41 | des = "表格结构错误:\n表格为空或者没有检索到标题行,标题列为第一行" 42 | elif self.code == ERROR_EXCEL_NOT_EXIST: 43 | des = "请输入表格文件" 44 | elif self.code == ERROR_XML_FILE_NOT_EXIST: 45 | des = "xml 文件不存在" 46 | if self.desc and des: 47 | self.desc = "%s\nMessage:%s" % (des, self.desc) 48 | elif des: 49 | self.desc = des 50 | return self.desc 51 | 52 | def get_desc_en(self): 53 | des = "" 54 | if self.code == SUCCESS: 55 | des = "Success!" 56 | elif self.code == ERROR_DIR_NOT_EXIST: 57 | des = "Directory does not exist" 58 | elif self.code == ERROR_IMPORT_INPUT: 59 | des = "input error " \ 60 | "\n1. input the target language and file,or target directory;" \ 61 | "\n2. The excel file does not contain the target language." 62 | elif self.code == ERROR_KEY_NOT_FOUND: 63 | des = "Table structure error:" \ 64 | "\nNo key column was retrieved. The key column should be named Android keyName" 65 | elif self.code == ERROR_MODULE_NOT_FOUND: 66 | des = "Table structure error:" \ 67 | "\nThe Module column was not retrieved and the key column needs to be named Android Module" 68 | elif self.code == EXCEPTION_EXL_FILE: 69 | des = "Table structure error:" \ 70 | "\nThe table is empty or no header row is retrieved, and the header column is the first row" 71 | elif self.code == ERROR_EXCEL_NOT_EXIST: 72 | des = "Please enter the form file" 73 | elif self.code == ERROR_XML_FILE_NOT_EXIST: 74 | des = "The XML file does not exist" 75 | if self.desc and des: 76 | self.desc = "%s\nMessage:%s" % (des, self.desc) 77 | elif des: 78 | self.desc = des 79 | return self.desc 80 | 81 | 82 | class Config: 83 | def __init__(self): 84 | pass 85 | 86 | keyTitle = "Android keyName" # key 名(Android 字符串 name) 87 | moduleTitle = "Android module" # module 名(xml 文件名) 88 | import_start_col = 2 # 从第几列开始导入 89 | 90 | export_excel_name = "Output.xls" # 导出的 excel 文件名 91 | export_base_dir = "values-zh" # 导出基准文件夹 92 | export_base_title = "zh" # 导出基准 title 93 | 94 | export_only_zh = False # 是否仅导出中文字符 95 | 96 | # 在 Android 中,如果字符串使用了 translatable 比如 97 | # English 98 | # 下方属性为 true 时,会忽略那条文案 99 | export_apply_translatable = True # 对 translatable 处理 -------------------------------------------------------------------------------- /Export.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import re 3 | 4 | import Constant 5 | from ParseUtils import * 6 | import pyExcelerator 7 | 8 | 9 | # 单个文件:获取文件 key - value 分别存放在 excel表格指定列 10 | 11 | 12 | class ExportUtils: 13 | 14 | def __init__(self): 15 | pass 16 | 17 | def xml2xls(self, xls_dir, input_dir): 18 | # type: (str, str) -> Constant.Error 19 | if not xls_dir or not os.path.exists(xls_dir): 20 | Log.error(Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "excel dir").get_desc_en()) 21 | return Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "excel dir") 22 | if not input_dir or not os.path.exists(input_dir): 23 | Log.error(Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "xml dir").get_desc_en()) 24 | return Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "xml dir") 25 | 26 | xlsPath = os.path.join(xls_dir, Constant.Config.export_excel_name) 27 | workbook = pyExcelerator.Workbook() 28 | ws = workbook.add_sheet('Sheet1') 29 | # row col content 30 | ws.write(0, 0, Constant.Config.moduleTitle) 31 | ws.write(0, 1, Constant.Config.keyTitle) 32 | ws.write(0, 2, Constant.Config.export_base_title) 33 | 34 | # 获取某个文件夹的所有文件,作为标准 这里是 value-zh 35 | base_dir = os.path.join(input_dir, Constant.Config.export_base_dir) 36 | if not os.path.exists(base_dir): 37 | Log.error(Constant.Error(Constant.ERROR_DIR_NOT_EXIST, 38 | "base_dir\nU can change base dir in Constant-->Config").get_desc_en()) 39 | return Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "base_dir\nU can change base dir in Constant-->Config") 40 | # os.walk(path)返回三个值: 41 | # parent, 表示path的路径、 42 | # dirnames, path路径下的文件夹的名字 43 | # filenames path路径下文件夹以外的其他文件。 44 | Log.info(input_dir) 45 | sub_dir_names = [] 46 | for _, dir_names, _ in os.walk(input_dir): 47 | if dir_names: 48 | sub_dir_names = dir_names 49 | break 50 | Log.info(sub_dir_names) 51 | 52 | row = 1 53 | # 文件夹下所有文件 54 | files = os.listdir(base_dir) 55 | for filename in files: 56 | module_name = getModuleName(filename) 57 | if not module_name: 58 | continue 59 | file_path = os.path.join(base_dir, filename) # 文件路径 60 | base_dict = XMLParse.get_value_and_key(file_path) 61 | 62 | col = 3 # base_dic 占用 0 和 1 2 63 | for dir_name in sub_dir_names: 64 | cur_dir_path = os.path.join(input_dir, dir_name) 65 | if cur_dir_path == base_dir: 66 | continue # 标准文件夹不处理 67 | 68 | # 当前文件夹的语言 69 | lan = getDirLan(input_dir, cur_dir_path) 70 | Log.info(lan) 71 | if not lan: # 文件夹爱不符合规范不处理(values-lan 或 values) 72 | continue 73 | 74 | # 获取其他按文件夹下的该文件路径 75 | cur_file = os.path.join(cur_dir_path, filename) 76 | if not os.path.exists(cur_file): 77 | # 路径不存在,不处理,跳过 78 | continue 79 | 80 | # 写标题 81 | ws.write(0, col, lan) 82 | cur_dict = XMLParse.get_value_and_key(cur_file) 83 | (base_dict, cur_dict) = sortDic(base_dict, cur_dict) 84 | writeDict(ws, cur_dict, row, col, None, False) # 仅写 value 85 | col += 1 # 写完非标准文件的内容,坐标右移(列+1) 86 | 87 | # 最后写 标准文件的 key(0)-values(1) 88 | writeDict(ws, base_dict, row, 0, module_name, True) 89 | 90 | row += len(base_dict) 91 | Log.info("row = %s" % row) 92 | 93 | workbook.save(xlsPath) 94 | return Constant.Error(Constant.SUCCESS) 95 | 96 | def xml2xls_single(self, xls_dir, input_file_path): 97 | # type: (str, str) -> object 98 | if not xls_dir or not os.path.exists(xls_dir): 99 | Log.error(Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "excel dir").get_desc_en()) 100 | return Constant.Error(Constant.ERROR_DIR_NOT_EXIST, "excel dir") 101 | if not input_file_path or not os.path.exists(input_file_path): 102 | Log.error(Constant.Error(Constant.ERROR_XML_FILE_NOT_EXIST).get_desc_en()) 103 | return Constant.Error(Constant.ERROR_XML_FILE_NOT_EXIST) 104 | xlsPath = os.path.join(xls_dir, Constant.Config.export_excel_name) 105 | workbook = pyExcelerator.Workbook() 106 | ws = workbook.add_sheet('Sheet1') 107 | # row col content 108 | ws.write(0, 0, Constant.Config.keyTitle) 109 | dic = XMLParse.get_value_and_key(input_file_path) 110 | writeDict(ws, dic, 1, 0, None, True) 111 | workbook.save(xlsPath) 112 | Log.info(Constant.Error(Constant.SUCCESS).get_desc_en()) 113 | return Constant.Error(Constant.SUCCESS) 114 | 115 | 116 | def getModuleName(xml_file_name): 117 | m = re.search('(.*?).xml', xml_file_name) 118 | module_name = '' 119 | if m: 120 | module_name = m.group(1) 121 | # print module_name 122 | return module_name 123 | 124 | 125 | def getDirLan(input_dir, dir_path): 126 | """ 127 | 子文件夹为 values 时默认为 en 128 | :param input_dir: 目标文件夹(包含这 values等文件夹的路径) 129 | :param dir_path: 子文件夹路径 一般 是 path/values-zh 等 130 | :return str 文件夹表示的语言 比如 values-zh 语言为 zh 131 | """ 132 | lan = "" 133 | if dir_path == os.path.join(input_dir, "values"): 134 | lan = "en" 135 | else: 136 | dirSplit = dir_path.split('values-') 137 | if len(dirSplit) > 1: 138 | lan = dirSplit[1] 139 | else: 140 | # cur_dir_path 文件夹不符合规则 141 | pass 142 | return lan 143 | 144 | 145 | def writeDict(ws, dic, start_row, col, module, isKeepKey): 146 | row = start_row 147 | for (key, value) in dic.items(): 148 | Log.debug("%s : %s" % (key, value)) 149 | if isKeepKey: 150 | if module: 151 | ws.write(row, col, module) 152 | ws.write(row, col + 1, key) 153 | ws.write(row, col + 2, value) 154 | else: 155 | ws.write(row, col, key) 156 | ws.write(row, col + 1, value) 157 | else: 158 | ws.write(row, col, value) 159 | row += 1 160 | 161 | 162 | def sortDic(dict1, dict2): 163 | """ 164 | dic2 根据 dict1 key 的顺序排序,如果 dict2 中的key不在 dict1中存在,则添加至 dict1最后 165 | :param dict1: 目标 key 顺序 166 | :param dict2: 167 | :return: 168 | """ 169 | result_dict = collections.OrderedDict() 170 | for (key, value) in dict1.items(): 171 | isMatch = False 172 | for (temp_key, temp_value) in dict2.items(): 173 | if key == temp_key: 174 | isMatch = True 175 | result_dict[key] = temp_value 176 | del dict2[key] 177 | if not isMatch: # 循环结束,没有找到 178 | result_dict[key] = "" 179 | isMatch = False 180 | if len(dict2) != 0: 181 | for (key, value) in dict2.items(): 182 | result_dict[key] = value 183 | dict1[key] = "" 184 | return dict1, result_dict 185 | -------------------------------------------------------------------------------- /ExportTestUnit.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from Export import ExportUtils 3 | 4 | 5 | def main(): 6 | exportUtils = ExportUtils() 7 | 8 | xls_dir = "E:\\stringCovertTool\\test" 9 | input_dir = "E:\\stringCovertTool\\test" 10 | file_path = "E:\\stringCovertTool\\test\\strings_me.xml" 11 | # exportUtils.xml2xls(xls_dir, input_dir) 12 | exportUtils.xml2xls_single(xls_dir, file_path) 13 | 14 | 15 | # 读取 xls 16 | main() 17 | -------------------------------------------------------------------------------- /Import.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # 1.确认目标语言 4 | # 2.xls 中找到目标语言的 key - value(excel) 5 | # 3.xml 中的 key - value(xml) 准备好 文件名为 moduleName 6 | # 4.把 value(xml) 更新成 value(excel) 7 | # 可以输出到文件或者目录(需要约定文件 values-en) 8 | 9 | from optparse import OptionParser 10 | 11 | import Constant 12 | from LogUtils import Log 13 | from ParseUtils import * 14 | import os.path 15 | 16 | 17 | def covertTargetPath(dir_path, language): 18 | # type: (String, String) -> String 19 | """ 20 | 据目标语言拼接成真正的目标目录 21 | :param dir_path: 输入的目录路径 比如 C:/A 22 | :param language: 目标语言 zh 23 | :return: C:/A/values-zh 没有 values-en 则目标目录为 C:/A/values 24 | """ 25 | 26 | # 如果没有 values-en 去 values找 27 | targetFilePath = dir_path + "\\" + "values-" + language.lower() + "\\" 28 | if language == "en" and not os.path.exists(targetFilePath): 29 | targetFilePath = dir_path + "\\" + "values" + "\\" 30 | return targetFilePath 31 | 32 | 33 | class ImportUtils: 34 | keyTitle = Constant.Config.keyTitle 35 | moduleTitle = Constant.Config.moduleTitle # 内容为文件名 36 | targetLanguage = None # 目标语言,与 filePath 成对使用 37 | filePath = None # 目标文件路径 38 | dirPath = None # 目标目录路径 39 | fromIndex = Constant.Config.import_start_col # 从 fromIndex 开始往后列导入 40 | 41 | def __init__(self): 42 | pass 43 | 44 | def xls2xml_options(self, options): 45 | return self.xls2xml(options.input, options.targetFilePath, options.targetLanguage, options.targetDirPath) 46 | 47 | def xls2xml(self, xls_path, file_path, target_language, target_dir_path): 48 | """ 49 | :param xls_path: 表格路径 50 | :param file_path: 目标文件路径 51 | :param target_language: 目标语言 52 | :param target_dir_path: 目标文件目录 53 | """ 54 | Log.info("--- xls2xml ---") 55 | 56 | # 输入 excel 57 | if not xls_path or not os.path.exists(xls_path): 58 | Log.error(Constant.Error(Constant.ERROR_EXCEL_NOT_EXIST).get_desc_en()) 59 | return Constant.Error(Constant.ERROR_EXCEL_NOT_EXIST) 60 | 61 | xlsPath = xls_path 62 | self.filePath = file_path 63 | self.targetLanguage = target_language 64 | self.dirPath = target_dir_path 65 | 66 | # 获取 xls 对象,以及目标 sheet(这里默认为第一张表,index 从0开始) 67 | xlsParse = XLSParse() 68 | xlsParse.open_excel(xlsPath) 69 | 70 | sheet = xlsParse.sheet_by_index(0) 71 | 72 | Log.info("name = %s, rows number = %s,clos number = %s" % (sheet.name, sheet.nrows, sheet.ncols)) 73 | return self.convert(sheet) 74 | 75 | def convert(self, sheet): 76 | """ 77 | 真正转化部分 78 | :param sheet: excel 的 sheet 对象 79 | :return: ErrorConstant.Error 80 | """ 81 | Log.info("--- convert ---") 82 | keyIndex = -1 83 | moduleIndex = -1 84 | tempLanguageIndex = None 85 | # 返回由该行中所有单元格的数据组成的列表 86 | try: 87 | firstRow = sheet.row_values(0) 88 | except Exception as e: 89 | Log.error(Constant.Error(Constant.EXCEPTION_EXL_FILE, e.message).get_desc_en()) 90 | return Constant.Error(Constant.EXCEPTION_EXL_FILE, e.message) 91 | 92 | if len(firstRow) == 0: 93 | Log.error(Constant.Error(Constant.ERROR_KEY_NOT_FOUND).get_desc_en()) 94 | return Constant.Error(Constant.ERROR_KEY_NOT_FOUND) 95 | 96 | for index in range(len(firstRow)): 97 | if firstRow[index] == self.keyTitle: 98 | keyIndex = index 99 | pass 100 | elif firstRow[index] == self.moduleTitle: 101 | moduleIndex = index 102 | pass 103 | elif firstRow[index] == self.targetLanguage: 104 | tempLanguageIndex = index 105 | pass 106 | 107 | if keyIndex == -1: 108 | Log.error(Constant.Error(Constant.ERROR_KEY_NOT_FOUND).get_desc_en()) 109 | return Constant.Error(Constant.ERROR_KEY_NOT_FOUND) 110 | 111 | # 获取 key 集合,并删除 title 项 112 | xlsKeys = sheet.col_values(keyIndex) 113 | del xlsKeys[0] 114 | 115 | if self.filePath and tempLanguageIndex: # 输入是文件,指定目标语言 116 | Log.debug("keyIndex = %s moduleIndex = %s languageIndex = %s" % (keyIndex, moduleIndex, tempLanguageIndex)) 117 | # 获取 value 集合,并删除 title 项 118 | xlsValues = sheet.col_values(tempLanguageIndex) 119 | del xlsValues[0] 120 | 121 | XMLParse.update_xml_value(self.filePath, xlsKeys, xlsValues) 122 | Log.info(Constant.Error(Constant.SUCCESS).get_desc_en()) 123 | return Constant.Error(Constant.SUCCESS) 124 | 125 | Log.debug("Not file") 126 | 127 | if moduleIndex == -1: 128 | Log.error(Constant.Error(Constant.ERROR_MODULE_NOT_FOUND).get_desc_en()) 129 | return Constant.Error(Constant.ERROR_MODULE_NOT_FOUND) 130 | 131 | if not self.dirPath: # 目录为空,返回 132 | Log.error(Constant.Error(Constant.ERROR_IMPORT_INPUT).get_desc_en()) 133 | return Constant.Error(Constant.ERROR_IMPORT_INPUT) 134 | 135 | if not os.path.exists(self.dirPath): 136 | Log.error(Constant.Error(Constant.ERROR_DIR_NOT_EXIST).get_desc_en()) 137 | return Constant.Error(Constant.ERROR_DIR_NOT_EXIST) 138 | 139 | for index, title in enumerate(firstRow): 140 | if index < self.fromIndex: 141 | continue 142 | languageIndex = index 143 | targetLanguage = title 144 | # print languageIndex 145 | # print title 146 | xlsKeys = sheet.col_values(keyIndex) 147 | del xlsKeys[0] 148 | 149 | xlsModules = sheet.col_values(moduleIndex) 150 | del xlsModules[0] 151 | 152 | xlsValues = sheet.col_values(languageIndex) 153 | del xlsValues[0] 154 | # 文件路径(子目录) 比如; value-zh 155 | # ├── android 156 | # │ ├── values-zh 157 | # │ | ├── strings_device.xml 158 | # │ | ├── strings_me.xml 159 | # │ | ├── strings_moment.xml 160 | # │ ├── values-de 161 | # │ ├── values-ko 162 | sub_dir_path = covertTargetPath(self.dirPath, targetLanguage) 163 | # print sub_dir_path 164 | if os.path.exists(sub_dir_path): 165 | XMLParse.update_multi_xml_value(sub_dir_path, xlsKeys, xlsValues, xlsModules) 166 | Log.info(Constant.Error(Constant.SUCCESS).get_desc_en()) 167 | return Constant.Error(Constant.SUCCESS) 168 | 169 | 170 | def addParser(): 171 | parser = OptionParser() 172 | parser.add_option("-i", "--input", help="excel file path") 173 | parser.add_option("-f", "--targetFilePath", help="means target output is xml file and input the file path") 174 | parser.add_option("-l", "--targetLanguage", help="target language shortname(just for output is file)") 175 | parser.add_option("-d", "--targetDirPath", help="means target output is dir contains xml file(s)") 176 | 177 | (options, args) = parser.parse_args() 178 | Log.info("options: %s, args: %s" % (options, args)) 179 | return options 180 | 181 | 182 | def main(): 183 | importUtils = ImportUtils() 184 | options = addParser() 185 | importUtils.xls2xml_options(options) 186 | 187 | 188 | if __name__ == "__main__": 189 | main() -------------------------------------------------------------------------------- /ImportTestUnit.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from Import import ImportUtils, addParser 3 | 4 | 5 | def main(): 6 | importUtils = ImportUtils() 7 | # options = addParser() 8 | # importUtils.xls2xml_options(options) 9 | 10 | xlsPath = "E:\\stringCovertTool\\test\\App Native.xlsx" 11 | filePath ="E:\\stringCovertTool\\test\\values\\strings_me.xml" 12 | dirPath = "E:\\stringCovertTool\\test" 13 | importUtils.xls2xml(xlsPath, None, None, dirPath) 14 | # importUtils.xls2xml(xlsPath, filePath, "en", dirPath) 15 | 16 | 17 | # 读取 xls 18 | main() 19 | -------------------------------------------------------------------------------- /LogUtils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import os 5 | import io 6 | import sys 7 | 8 | class Log: 9 | 'Log util' 10 | 11 | # Log info blue 12 | @staticmethod 13 | def info(msg): 14 | print('\033[34m') 15 | print msg 16 | 17 | # Log error red 18 | @staticmethod 19 | def error(msg): 20 | print('\033[31m') 21 | print msg.encode("GBK", "ignore") 22 | 23 | # Log debug white 24 | @staticmethod 25 | def debug(msg): 26 | print('\033[37m' + msg.encode("GBK", "ignore")) 27 | -------------------------------------------------------------------------------- /ParseUtils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import xlrd 4 | import sys 5 | import xml.dom.minidom 6 | import os.path 7 | 8 | import Constant 9 | from LogUtils import Log 10 | import collections 11 | 12 | 13 | class XLSParse: 14 | 15 | def __init__(self): 16 | # 解决中文编码问题 17 | reload(sys) 18 | sys.setdefaultencoding('utf-8') 19 | 20 | def open_excel(self, filePath): 21 | self.data = xlrd.open_workbook(filePath) 22 | 23 | # 根据sheet索引获取sheet内容,sheet索引从0开始 24 | def sheet_by_index(self, index): 25 | return self.data.sheet_by_index(index) 26 | 27 | # 根据sheet名称获取sheet内容 28 | def sheet_by_name(self, name): 29 | return self.data.sheet_by_name(name) 30 | 31 | 32 | class XMLParse: 33 | 34 | def __init__(self): 35 | pass 36 | 37 | @staticmethod 38 | def get_text_node_value(string_node): 39 | """ 40 | :param string_node: string 结点 41 | :return: data 类型结点 text 42 | """ 43 | if string_node.firstChild.nodeType == string_node.TEXT_NODE: 44 | # 获取某个元素节点的文本内容,先获取子文本节点,然后通过“data”属性获取文本内容 45 | if len(string_node.firstChild.data) != 0: 46 | value = string_node.firstChild.data 47 | elif string_node.firstChild.nodeType == string_node.ELEMENT_NODE: # 元素节点 48 | data_node = string_node.getElementsByTagName("Data") # 字符串样式 49 | if len(data_node) != 0 and len(data_node[0].firstChild.data) != 0: 50 | data_value = data_node[0].firstChild.data 51 | value = "" + data_value + "" 52 | return value 53 | 54 | @staticmethod 55 | def update_multi_xml_value(sub_dir_path, keys, values, modules): 56 | Log.info("\n\n" + sub_dir_path + "\n") 57 | ''' 58 | sub_dir_path: 目标子目录,比如 value-zh 59 | ''' 60 | if len(modules) == 0: 61 | return 62 | 63 | # 先排序,把 excel 中的统一 module 排到一起 64 | # 排序,分块处理 65 | current_module = modules[0] 66 | module_length_list = [] 67 | current_module_len = 0 68 | modules_new = [] 69 | values_new = [] 70 | keys_new = [] 71 | for mid, module in enumerate(modules): 72 | if module is None or module == "": 73 | continue 74 | if current_module != module: 75 | module_length_list.append(current_module_len) 76 | current_module = module 77 | current_module_len = 0 78 | 79 | modules_new.append(module) 80 | values_new.append(values[mid]) 81 | keys_new.append(keys[mid]) 82 | current_module_len += 1 83 | 84 | module_length_list.append(current_module_len) 85 | 86 | start = 0 87 | end = 0 88 | for module_len in module_length_list: 89 | end += module_len 90 | subKeys = keys_new[start:end] 91 | subValues = values_new[start:end] 92 | module = modules_new[start] 93 | start += module_len 94 | filePath = sub_dir_path + module + ".xml" 95 | 96 | XMLParse.update_xml_value(filePath, subKeys, subValues) 97 | 98 | @staticmethod 99 | def update_xml_value(file_path, keys, values): 100 | Log.info("--- updating xml... \n%s" % file_path) 101 | if not os.path.exists(file_path): 102 | return 103 | # Log.info ("--- string ---") 104 | # 读取文档 105 | xml_doc = xml.dom.minidom.parse(file_path) 106 | # filename 107 | nodes = xml_doc.getElementsByTagName('string') 108 | for node in nodes: 109 | xmlKey = node.getAttribute("name") 110 | xmlValue = "" # 改变量仅用于输出 111 | if node.firstChild is None: 112 | continue 113 | xmlValue = XMLParse.get_text_node_value(node) 114 | 115 | for index, key in enumerate(keys): 116 | if key == xmlKey and len(values[index]) != 0: 117 | node.firstChild.data = values[index] 118 | Log.debug("%s : %s -- >%s " % (xmlKey, xmlValue, node.firstChild.data)) 119 | # Log.info("--- string end ---\n") 120 | 121 | # 数组 122 | # Log.info("--- array ---") 123 | array_nodes = xml_doc.getElementsByTagName('string-array') 124 | for array_node in array_nodes: 125 | xmlKey = array_node.getAttribute('name') 126 | 127 | child_nodes = array_node.getElementsByTagName('item') 128 | for idx, child_node in enumerate(child_nodes): 129 | newKey = xmlKey + "-INDEX-" + str(idx) 130 | 131 | xmlValue = child_node.firstChild.data 132 | for index, key in enumerate(keys): 133 | if key == newKey and len(values[index]) != 0: 134 | child_node.firstChild.data = values[index] 135 | Log.debug("%s : %s --> %s" % (newKey, xmlValue, child_node.firstChild.data)) 136 | # Log.info("--- array end ---\n") 137 | writeFile = open(file_path, 'w') 138 | writeFile.write(xml_doc.toxml('utf-8')) 139 | writeFile.close() 140 | 141 | @staticmethod 142 | def get_value_and_key(file_path): 143 | """ 144 | 获取 xml 文件的 key - value 145 | :param file_path: 文件路径 146 | :return: dic[key]-value 147 | """ 148 | if not file_path or not os.path.exists(file_path): 149 | Log.error("xml 文件不存在") 150 | return 151 | xml_doc = xml.dom.minidom.parse(file_path) 152 | nodes = xml_doc.getElementsByTagName('string') 153 | dic = collections.OrderedDict() 154 | for index, node in enumerate(nodes): 155 | if node is None or node.firstChild is None: 156 | continue 157 | 158 | if Constant.Config.export_apply_translatable: 159 | # ignore translatable 160 | translatable = node.getAttribute('translatable') 161 | if translatable is not None and translatable == "false": 162 | continue 163 | 164 | key = node.getAttribute("name") 165 | 166 | value = XMLParse.get_text_node_value(node) 167 | if not Constant.Config.export_only_zh: 168 | dic[key] = value 169 | else: 170 | # 仅导出中文,是中文,则保存 171 | if is_chinese(value): 172 | dic[key] = value 173 | 174 | # Log.info("%s : %s" % (key, value)) 175 | 176 | array_nodes = xml_doc.getElementsByTagName("string-array") 177 | for array_node in array_nodes: 178 | key = array_node.getAttribute('name') 179 | child_nodes = array_node.getElementsByTagName('item') 180 | for idx, child_node in enumerate(child_nodes): 181 | newKey = key + "-INDEX-" + str(idx) 182 | value = XMLParse.get_text_node_value(child_node) 183 | if not Constant.Config.export_only_zh: 184 | dic[newKey] = value 185 | else: 186 | if is_chinese(value): 187 | dic[newKey] = value 188 | return dic 189 | 190 | 191 | def is_chinese(string): 192 | """ 193 | 检查整个字符串是否包含中文 194 | :param string: 需要检查的字符串 195 | :return: bool 196 | """ 197 | for ch in string: 198 | if u'\u4e00' <= ch <= u'\u9fff': 199 | return True 200 | return False 201 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | 2 | # 注意 3 | 本脚本后面不在更新,已迁移到 python3 环境,更新迭代 4 | https://github.com/tingtingtina/StringCovertTool3 5 | 6 | ## 目的 7 | 8 | 本项目的开发目的是由于在 Android 项目中会有多语言翻译的需求,在我们做完中文给到专业人员翻译的时候,有字符串到出的需求;反之,多国语言翻译回来之后,同时有导入的需求,手动复制粘贴一定是最低效的方式。所以这个脚本只有两个功能,一个是导出,xml -> xls ,一个是导入 xls -> xml 9 | 10 | ## 使用方法 11 | 12 | ### 环境 13 | - Python2 14 | - python库 15 | - xlrd(导入使用) 16 | - pyExcelerator(导出使用) 17 | 18 | ### 配置 19 | Config 里面默认配置了 表格的 title 等属性 20 | ```python 21 | keyTitle = "Android keyName" # key 名(Android 字符串 name) 22 | moduleTitle = "Android module" # module 名(xml 文件名) 23 | import_start_col = 2 # 从第几列开始导入 24 | 25 | export_excel_name = "Output.xls" # 导出的 excel 文件名 26 | export_base_dir = "values-zh" # 导出基准文件夹 27 | export_base_title = "zh" # 导出基准 title 28 | 29 | export_only_zh = False # 是否仅导出中文字符 30 | ``` 31 | 32 | ## 参数说明 33 | 34 | ### 导入 35 | 36 | - -i 输入excel 的路径 37 | 38 | - -f 输入目标 xml 路径 39 | 40 | - -l 输入目标语言简写,如 en zh 等,与 ```-f``` 成对使用 41 | 42 | - -d 输入目标文件夹,如 c:\android,文件目录结构如下(android 直接从工程中拷出即可) 43 | 44 | (由于默认为 en 因此文件夹支持 values-en 也支持 values) 45 | 46 | ``` 47 | ├── andorid 48 | │ ├── values-zh 49 | │ | ├── strings_device.xml 50 | │ | ├── strings_me.xml 51 | │ | ├── strings_moment.xml 52 | │ ├── values-de 53 | │ ├── values-ko 54 | ``` 55 | 56 | 示例,在脚本目录下执行,或在下面脚本前加路径 57 | 58 | - 单个文件(入参有是三个 ```-i``` ```-f``` ```-l```) 59 | 60 | ```python 61 | python Import.py -i "C:\Users\Administrator\Desktop\App Native - 1126.xlsx" -l "en" -f "C:\Users\Administrator\Desktop\p\strings_moment.xml" 62 | ``` 63 | 64 | 表示 从把表格中 zh 那一列内容,替换到 strings_moment.xml 文件中 65 | 也可以使用相对路径 66 | ```python 67 | python Import.py -i "./test/App Native.xlsx" -l en -f ./test/strings_moment.xml 68 | ``` 69 | 70 | - 文件夹 71 | 72 | ``` 73 | python Import.py -i "C:\Users\Administrator\Desktop\App Native - 1126.xlsx" -d "C:\Users\Administrator\Desktop\p" 74 | ``` 75 | ### 导出 76 | > 导出没有添加输入参数,直接支持可视化操作 77 | - 单个文件, 78 | - 输入要导出的 excel 路径,比如,选择路径为 C:\Users\Administrator\Desktop,那么会在桌面上创建一个 Output.xls 文件,完成路径为 C:\Users\Administrator\Desktop\Output.xls 79 | - 输入要导出的 xml 文件,结果就会将 xml 的 键值对导出到 excel 表中 80 | 81 | - 目录方式 82 | - 要导出的 excel 路径,同上 83 | - 要导出的 xml 目录路径,如 ..\android, android 目录下有如下文件格式 84 | 85 | 86 | ``` 87 | ├── andorid 88 | │ ├── values-zh 89 | │ | ├── strings_device.xml 90 | │ | ├── strings_me.xml 91 | │ | ├── strings_moment.xml 92 | │ ├── values-de 93 | │ ├── values-ko 94 | │ ├── values 95 | ``` 96 | 注:目录方式会默认以 values-zh 作为基准(比如 key 的顺序),可以通过修改 Config 属性 ```export_base_dir``` 和 ```export_base_title``` 来定制 97 | 98 | ### 导出效果 99 | 100 | | Android module | Android keyName | zh | en | ko | 101 | | -------------- | --------------- | ---- | ---- | ---- | 102 | | strings_me | me_1 | | | | 103 | | strings_me | me_2 | | | | 104 | | strings_me | me_3 | | | | 105 | | strings_moment | moment_1 | | | | 106 | | strings_moment | moment_2 | | | | 107 | | strings_moment | moment_3 | | | | 108 | | strings_device | device_1 | | | | 109 | ### 注意点: 110 | 111 | - xml 目录指的都是包含着 values-zh 等目录的文件夹 112 | 113 | - 这里的 module 指的是 同一种语言下的 strings.xml 文件的文件名 114 | 115 | - 如果在字符串定义有数组,比如 strings-array表示,会自定义名称,比如 下面就会生成两个键值对 116 | 117 | - gender_item-INDEX-0:男 118 | - gender_item-INDEX-1:女 119 | 120 | ```xml 121 | 122 | 123 | 124 | 125 | ``` 126 | - 关于导入列配置 ```import_start_col```,项目使用表格 是从第 2 列开始做导入(从0开始),也就是 en 和 de 需导入,在实际使用中可根据需求处理 127 | - ```export_only_zh``` 128 | 129 | > 需求来源于,我们在项目中会追加文案(默认中文),在没有多语言翻译的时候,其他的 strings 都会写成中文,为了便于仅导出为翻译的部分,添加此字段辅助。 130 | - 常用使用方法, 将导出基准设为非中文的语言,如 下面将基准设为英文,并将近导出中文支付设为 true,那么导出的就是没有翻译(仅中文)的内容 131 | - 为什么不能将基准设为中文? 如果以中文为基准,所有的字符串都会导出来啦 132 | ```python 133 | export_base_dir = "values" # 导出基准文件夹 134 | export_base_title = "en" # 导出基准 title 135 | export_only_zh = True # 是否仅导出中文字符 136 | ``` 137 | 138 | - ```export_ignore_translatable``` 139 | 140 | > 在国际化当中会有一些不需要被国际化的文案,比如 App 中的语言设置,对语言描述是不需要被国际化的。比如下面文案,不论在中文环境下还是英文或者其他语种,都不需要对其进行翻译,所以可以通过添加属性 translatable 为 false 来处理。 141 | > 142 | > ```xml 143 | > English 144 | > ``` 145 | 146 | ```export_ignore_translatable``` 为 ```True``` 时,会忽略 ```translatable``` 为 ```false``` 的文案,不做导出。 147 | 148 | 比较常用的是,配合仅导出中文属性一同使用,可以避免这些不用翻译的内容被再次导出。 149 | 150 | 151 | ## 可视化使用 152 | 153 | - 上面直接使用参数的方式还是容易出错,建议使用下面的方式,更清晰易懂 154 | 155 | ```python pick_me.py install``` 156 | 157 | **xml——>excel** 158 | 159 | ![导出](https://github.com/tingtingtina/StringCovertTool/blob/master/image/export%20xml.png) 160 | 161 | **excel——>xml** 162 | 163 | ![导入](https://github.com/tingtingtina/StringCovertTool/blob/master/image/import%20xml.png) 164 | 165 | ## 测试 166 | - ImportTestUnit.py 和 ExportTestUnit.py 用户导入导出测试 167 | 168 | ## 常用使用场景示例 169 | ### 导出英文没有翻译的内容 170 | 171 | 可以直接在 ExportTestUnit.py 写测试代码运行即可 172 | ```python 173 | def main(): 174 | exportUtils = ExportUtils() 175 | Config.export_only_zh = True 176 | Config.export_base_dir = "values" 177 | Config.export_base_title = "en" 178 | 179 | xls_dir = "C:\\Users\\Administrator\\Desktop\\test" #目标xls目录 180 | input_dir = "C:\\Users\\Administrator\\Desktop\\test" #目标文件夹,里面放英文的xml即可 181 | exportUtils.xml2xls(xls_dir, input_dir) 182 | ``` 183 | 184 | ### 导出除了中英之外没有翻译的语言 185 | - 比如导出韩语中需要翻译的内容。如果只需要对比中文,导出韩文需要翻译的用上面的方法即可 186 | - 如果要导出中英韩的话,使用目录方式。但需要修改下代码处理,最好先和中文的内容做下匹配,使得中英韩的key组数一致,避免内容缺失 187 | - 思想,保留中文,和英文文件夹下的内容 188 | - 实现:在ParseUtils中的 is_chinese(value) 的判断部分增加一个判断 file_path.find("values-en") >= 0 表示中文和英文文件夹下的内容都导出 189 | 190 | ```python 191 | if is_chinese(value) or file_path.find("values-en") >= 0: 192 | #doSomething 193 | ``` 194 | - 整理:最后可以用 excel 中的筛选功能,删除韩语中空白行数 195 | - 总结步骤 196 | 1. 修改代码 197 | 2. 使用目录导出(通用) 198 | 3. Excel整理 199 | 200 | -------------------------------------------------------------------------------- /TkExport.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from Tkinter import * 3 | import tkFileDialog 4 | import tkMessageBox 5 | import Constant 6 | 7 | import Import 8 | from Export import ExportUtils 9 | 10 | 11 | class TkExport: 12 | def __init__(self): 13 | pass 14 | 15 | @staticmethod 16 | def import_func(root): 17 | window = Toplevel(root) 18 | window.title("xml导出") 19 | # 设定窗口的大小(长 * 宽) 20 | window.geometry('600x300') # 这里的乘是小x 21 | 22 | # 标题框 23 | initStartY = 60 24 | Label(window, text='导出表格路径:').place(x=10, y=initStartY) 25 | Label(window, text='xml目录:').place(x=10, y=initStartY + 40) 26 | # Label(window, text='基准语言目录:').place(x=10, y=initStartY + 80) 27 | Label(window, text='xml文件:').place(x=10, y=initStartY + 120) 28 | 29 | # 输入框 30 | inputStartX = 100 31 | enter_width = 50 # 这个宽度可以理解成字符数呢 32 | var_excel_dir_path = StringVar() 33 | Entry(window, textvariable=var_excel_dir_path, width=enter_width).place(x=inputStartX, y=initStartY) 34 | 35 | var_xml_dir_path = StringVar() 36 | Entry(window, textvariable=var_xml_dir_path, width=enter_width).place(x=inputStartX, y=initStartY + 40) 37 | 38 | # var_base_lan_dir = StringVar() 39 | # Entry(window, textvariable=var_base_lan_dir, width=enter_width).place(x=inputStartX, y=initStartY + 80) 40 | 41 | var_xml_file_path = StringVar() 42 | Entry(window, textvariable=var_xml_file_path, width=enter_width).place(x=inputStartX, y=initStartY + 120) 43 | 44 | def enter_excel_dir(): 45 | excel_dir_path = tkFileDialog.askdirectory() 46 | print excel_dir_path 47 | var_excel_dir_path.set(excel_dir_path) 48 | 49 | def enter_xml_dir_path(): 50 | xml_dir_path = tkFileDialog.askdirectory() 51 | print xml_dir_path 52 | var_xml_dir_path.set(xml_dir_path) 53 | 54 | def enter_xml_file_path(): 55 | xml_file_path = tkFileDialog.askopenfilename(filetypes=[("xml file", "*.xml*")]) 56 | print xml_file_path 57 | var_xml_file_path.set(xml_file_path) 58 | 59 | def start_convert(): 60 | var1 = var_excel_dir_path.get() 61 | var2 = var_xml_dir_path.get() 62 | # var3 = var_base_lan_dir.get() 63 | var4 = var_xml_file_path.get() 64 | 65 | exportUtils = ExportUtils() 66 | 67 | isDir = callRB() # 导出是文件导出还是目录导出 68 | 69 | if isDir: 70 | error = exportUtils.xml2xls(var1, var2) 71 | else: 72 | error = exportUtils.xml2xls_single(var1, var4) 73 | 74 | if error is not None: 75 | if error.isError(): 76 | tkMessageBox.showerror(title='Error', message=error.get_desc(), parent=window) 77 | else: 78 | tkMessageBox.showinfo(title='congratulation', message=error.get_desc(), parent=window) 79 | 80 | # 按键 81 | inputStartX = 480 82 | Button(window, text="选择目录", command=enter_excel_dir).place(x=inputStartX, y=initStartY - 5) 83 | Button(window, text="选择目录", command=enter_xml_dir_path).place(x=inputStartX, y=initStartY + 35) 84 | Button(window, text="选择文件", command=enter_xml_file_path).place(x=inputStartX, y=initStartY + 115) 85 | Button(window, text="开始", command=start_convert).place(x=10, y=initStartY + 160) 86 | 87 | # 选择导出方式 88 | v = IntVar() 89 | # 列表中存储的是元素是元组 90 | Label(window, text="导出方式") 91 | exportFunc = [('单个文件', 0), ('目录方式', 1)] 92 | 93 | def callRB(): 94 | return v.get() == 1 95 | 96 | # for循环创建单选框 97 | for text, num in exportFunc: 98 | Radiobutton(window, text=text, value=num, command=callRB, variable=v).pack(anchor=W) 99 | pass 100 | -------------------------------------------------------------------------------- /TkImport.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from Tkinter import * 3 | import tkFileDialog 4 | import tkMessageBox 5 | import Constant 6 | 7 | import Import 8 | 9 | 10 | class TkImport: 11 | def __init__(self): 12 | pass 13 | 14 | @staticmethod 15 | def export_func(root): 16 | window = Toplevel(root) 17 | window.title("导入xml") 18 | # 设定窗口的大小(长 * 宽) 19 | window.geometry('600x300') # 这里的乘是小x 20 | 21 | # 标题框 22 | initStartY = 60 23 | Label(window, text='表格路径:').place(x=10, y=initStartY) 24 | Label(window, text='目标文件:').place(x=10, y=initStartY + 40) 25 | Label(window, text='目标语言:').place(x=10, y=initStartY + 80) 26 | Label(window, text='目标目录:').place(x=10, y=initStartY + 120) 27 | 28 | # 输入框 29 | inputStartX = 100 30 | enter_width = 50 # 这个宽度可以理解成字符数呢 31 | var_input_path = StringVar() 32 | Entry(window, textvariable=var_input_path, width=enter_width).place(x=inputStartX, y=initStartY) 33 | 34 | var_target_file_path = StringVar() 35 | Entry(window, textvariable=var_target_file_path, width=enter_width).place(x=inputStartX, y=initStartY + 40) 36 | 37 | var_target_lan = StringVar() 38 | Entry(window, textvariable=var_target_lan, width=enter_width).place(x=inputStartX, y=initStartY + 80) 39 | 40 | var_target_dir_path = StringVar() 41 | edt4 = Entry(window, textvariable=var_target_dir_path, width=enter_width) 42 | edt4.place(x=inputStartX, y=initStartY + 120) 43 | 44 | def enter_input_path(): 45 | # tkMessageBox.showerror(title='Hi', message="目录异常", parent=window) 46 | input_path = tkFileDialog.askopenfilename(filetypes=[("excel file", "*.xls*")]) 47 | print input_path 48 | var_input_path.set(input_path) 49 | 50 | def enter_target_file_path(): 51 | target_file_path = tkFileDialog.askopenfilename(filetypes=[("xml file", "*.xml*")]) 52 | print target_file_path 53 | var_target_file_path.set(target_file_path) 54 | 55 | def enter_target_dir_path(): 56 | target_dir_path = tkFileDialog.askdirectory() 57 | print target_dir_path 58 | var_target_dir_path.set(target_dir_path) 59 | 60 | def start_convert(): 61 | var1 = var_input_path.get() 62 | var2 = var_target_file_path.get() 63 | var3 = var_target_lan.get() 64 | var4 = var_target_dir_path.get() 65 | 66 | importUtils = Import.ImportUtils() 67 | 68 | isDir = callRB() # 导出是文件导出还是目录导出 69 | 70 | if isDir: 71 | error = importUtils.xls2xml(var1, None, None, var4) # type: Constant.Error 72 | else: 73 | error = importUtils.xls2xml(var1, var2, var3, None) # type: Constant.Error 74 | 75 | if error is not None: 76 | if error.isError(): 77 | tkMessageBox.showerror(title='Error', message=error.get_desc(), parent=window) 78 | else: 79 | tkMessageBox.showinfo(title='congratulation', message=error.get_desc(), parent=window) 80 | 81 | # 按键 82 | inputStartX = 480 83 | Button(window, text="选择文件", command=enter_input_path).place(x=inputStartX, y=initStartY - 5) 84 | Button(window, text="选择文件", command=enter_target_file_path).place(x=inputStartX, y=initStartY + 35) 85 | Button(window, text="选择目录", command=enter_target_dir_path).place(x=inputStartX, y=initStartY + 115) 86 | Button(window, text="开始", command=start_convert).place(x=10, y=initStartY + 160) 87 | 88 | # 选择导出方式 89 | v = IntVar() 90 | # 列表中存储的是元素是元组 91 | Label(window, text="导入方式") 92 | exportFunc = [('单个文件', 0), ('目录方式', 1)] 93 | 94 | def callRB(): 95 | return v.get() == 1 96 | 97 | # for循环创建单选框 98 | for text, num in exportFunc: 99 | Radiobutton(window, text=text, value=num, command=callRB, variable=v).pack(anchor=W) 100 | pass 101 | -------------------------------------------------------------------------------- /image/export xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tingtingtina/StringCovertTool/26a6c6e9e7ccb5bb606c97e433818e171b792c9b/image/export xml.png -------------------------------------------------------------------------------- /image/import xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tingtingtina/StringCovertTool/26a6c6e9e7ccb5bb606c97e433818e171b792c9b/image/import xml.png -------------------------------------------------------------------------------- /pick_me.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from Tkinter import * 3 | import tkMessageBox 4 | 5 | from TkExport import TkExport 6 | from TkImport import TkImport 7 | 8 | root = Tk() 9 | root.title("Android 字符串多语言导入导出工具") 10 | root.geometry('300x200') # 这里的乘是小x 11 | # 创建一个菜单栏,这里我们可以把他理解成一个容器,在窗口的上方 12 | menu_bar = Menu(root) 13 | # 创建一个File菜单项(默认不下拉,下拉内容包括导入xml,从xml导出功能项 14 | menu = Menu(root, tearoff=0) 15 | # 将上面定义的空菜单命名为File,放在菜单栏中,就是装入那个容器中 16 | menu_bar.add_cascade(label='功能', menu=menu) 17 | 18 | 19 | # 在File中加入New、Open、Save等小菜单,即我们平时看到的下拉菜单,每一个小菜单对应命令操作。 20 | def import_func(): 21 | # tkMessageBox.showinfo("开发中", message="开发中……") 22 | TkExport.import_func(root) 23 | pass 24 | 25 | 26 | def export_func(): 27 | TkImport.export_func(root) 28 | 29 | 30 | menu.add_command(label='从 xml 导出', command=import_func) 31 | menu.add_command(label='导入 xml', command=export_func) 32 | 33 | 34 | # 创建菜单栏完成后,配置让菜单栏menubar显示出来 35 | root.config(menu=menu_bar) 36 | root.mainloop() # 进入消息循环 37 | 38 | # https://blog.csdn.net/shawpan/article/details/78759199 39 | # https://www.cnblogs.com/shwee/p/9427975.html 40 | -------------------------------------------------------------------------------- /test/App Native.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tingtingtina/StringCovertTool/26a6c6e9e7ccb5bb606c97e433818e171b792c9b/test/App Native.xlsx -------------------------------------------------------------------------------- /test/Output.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tingtingtina/StringCovertTool/26a6c6e9e7ccb5bb606c97e433818e171b792c9b/test/Output.xls -------------------------------------------------------------------------------- /test/strings_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 中文简体 4 | English 5 | 6 | 7 | 设置 8 | 主页 9 | 修改资料 10 | 签到 11 | 已签到 12 | 13 | 我的数据 14 | 查看日报 15 | 任务中心 16 | 俱乐部活动 17 | 我的收藏 18 | 消息通知 19 | 服务与帮助 20 | %1$d个可完成任务 21 | %1$s个未读 22 | 23 | 这个答案对我有用 24 | 赞了这条评论 25 | 赞了这条动态 26 | 我:]]> 27 | 28 | 社区管理规范 29 | 推送设置 30 | 清除缓存 31 | 关于App 32 | 更新应用 33 | 有新版本 34 | 当前已是最新版本 35 | 36 | 查看更多 37 | 评论 38 | 点赞 39 | 系统 40 | 41 | 42 | @string/male 43 | @string/female 44 | 45 | -------------------------------------------------------------------------------- /test/strings_moment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 热门 5 | 新鲜事 6 | 关注 7 | 8 | 相册 9 | 照片与视频 10 | 拍照/摄像 11 | 没有支持视频预览的应用 12 | 视频与图片不可同时选择 13 | 照片数量已达上限 14 | 视频数量已达上限 15 | 相册内还没有图片哦 16 | 拍照错误 17 | 录制错误 18 | 视频录制时间太短 19 | 对焦失败 20 | 没有获取到可用的摄像头 21 | 22 | 23 | 取消置顶 24 | 全局置顶 25 | 删除 26 | 是否确认删除 27 | 收藏 28 | 取消收藏 29 | 加精选 30 | 取消精选 31 | 全球发布 32 | 取消全球 33 | 回复 34 | 35 | 轻触拍照,长按摄像 36 | 37 | 38 | 请输入要搜索的内容 39 | 热门标签 40 | 搜索历史 41 | 相关内容 42 | 相关标签 43 | %1$s条]]> 44 | -------------------------------------------------------------------------------- /test/values-de/strings_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 中文简体 4 | English 5 | 6 | 7 | Einstellungen 8 | Startseite 9 | Profil bearbeiten 10 | Anmelden 11 | Angemeldet 12 | 13 | Meine Daten 14 | Tagesbericht anzeigen 15 | Aufgaben 16 | Clubaktivitäten 17 | Favoriten 18 | Benachrichtigungen 19 | Hilfe und Feedback 20 | %1$d zu erledigende Aufgaben 21 | %1$s ungelesen 22 | 23 | Hilfreich 24 | Hat diesen Kommentar geliked 25 | Hat diesen Post geliked 26 | 我:]]> 27 | 28 | Community Management Regeln 29 | Benachrichtigungen 30 | Cache leeren 31 | Info 32 | App aktualisieren 33 | Neue Version gefunden 34 | Es ist aktuell. 35 | 36 | Mehr anzeigen 37 | Kommentare 38 | Likes 39 | System 40 | 41 | 42 | Male 43 | Female 44 | 45 | -------------------------------------------------------------------------------- /test/values-de/strings_moment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Populär 5 | Neuigkeiten 6 | Folgen 7 | 8 | Album 9 | Fotos und Videos 10 | Foto / Video 11 | Keine Apps, die Videovorschau unterstützen 12 | Bilder und Videos können nicht gleichzeitig ausgewählt werden 13 | Max. Anzahl Foto erreicht 14 | Videos überschreiten max. Anzahl 15 | Album ist leer 16 | Kamerafehler 17 | Aufzeichnungsfehler 18 | Das Video ist zu kurz. 19 | Fokussierung fehlgeschlagen 20 | Es ist keine Kamera verfügbar 21 | 22 | 23 | Von oben entfernen 24 | Oben fixieren 25 | Löschen 26 | Löschen bestätigen? 27 | Favoriten 28 | Aus Favoriten entfernen 29 | Als Beste(n) auswählen 30 | Aus der Bestenliste entfernen 31 | Global veröffentlichen 32 | Nicht global veröffentlichen 33 | Reply 34 | 35 | Tippen Sie, um ein Foto aufzunehmen und halten Sie gedrückt, um ein Video aufzunehmen 36 | 37 | 38 | Suche 39 | Beliebte Tags 40 | Verlauf durchsuchen 41 | Verwandte Inhalte 42 | Verwandte Tags 43 | %1$s条]]> 44 | -------------------------------------------------------------------------------- /test/values-zh/strings_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 中文简体 4 | English 5 | 6 | 7 | 设置 8 | 主页 9 | 修改资料 10 | 签到 11 | 已签到 12 | 13 | 我的数据 14 | 查看日报 15 | 任务中心 16 | 俱乐部活动 17 | 我的收藏 18 | 消息通知 19 | 服务与帮助 20 | %1$d个可完成任务 21 | %1$s个未读 22 | 23 | 这个答案对我有用 24 | 赞了这条评论 25 | 赞了这条动态 26 | 我:]]> 27 | 28 | 社区管理规范 29 | 推送设置 30 | 清除缓存 31 | 关于App 32 | 更新应用 33 | 有新版本 34 | 当前已是最新版本 35 | 36 | 查看更多 37 | 评论 38 | 点赞 39 | 系统 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/values-zh/strings_moment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 热门 5 | 新鲜事 6 | 关注 7 | 8 | 相册 9 | 照片与视频 10 | 拍照/摄像 11 | 没有支持视频预览的应用 12 | 视频与图片不可同时选择 13 | 照片数量已达上限 14 | 视频数量已达上限 15 | 相册内还没有图片哦 16 | 拍照错误 17 | 录制错误 18 | 视频录制时间太短 19 | 对焦失败 20 | 没有获取到可用的摄像头 21 | 22 | 23 | 取消置顶 24 | 全局置顶 25 | 删除 26 | 是否确认删除 27 | 收藏 28 | 取消收藏 29 | 加精选 30 | 取消精选 31 | 全球发布 32 | 取消全球 33 | 回复 34 | 35 | 轻触拍照,长按摄像 36 | 37 | 38 | 请输入要搜索的内容 39 | 热门标签 40 | 搜索历史 41 | 相关内容 42 | 相关标签 43 | %1$s条]]> 44 | -------------------------------------------------------------------------------- /test/values/strings_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 中文简体 4 | English 5 | 6 | 7 | Settings 8 | Homepage 9 | Edit profile 10 | Sign in 11 | Signed in 12 | 13 | My Data 14 | View daily report 15 | Tasks 16 | Club Activities 17 | Favorites 18 | Notifications 19 | Help & Feedback 20 | %1$d tasks to do 21 | %1$s unread 22 | 23 | Helpful 24 | Liked this comment 25 | Liked this post 26 | 我:]]> 27 | 28 | Community Management Rules 29 | Notifications 30 | Clear Cache 31 | About App 32 | Update App 33 | New version found 34 | It is up to date 35 | 36 | View more 37 | Comments 38 | Likes 39 | System 40 | 41 | 42 | Male 43 | Female 44 | 45 | -------------------------------------------------------------------------------- /test/values/strings_moment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trending 5 | What's new 6 | Following 7 | 8 | album 9 | photos and videos 10 | Photo/ Video 11 | No apps supporting video preview 12 | Unable to select image and video at the same time 13 | Number of photo limit reached 14 | Videos exceed limit number 15 | Album is empty 16 | Camera error 17 | Recording error 18 | Video recording time is too short 19 | Focus failure 20 | No camera available 21 | 22 | 23 | Remove from Top 24 | Sticky on Top 25 | Delete 26 | Confirm deleting? 27 | Favorites 28 | Remove from Favorites 29 | Select as Best 30 | Remove from Best 31 | Publish globally 32 | Don't publish globally 33 | Reply 34 | 35 | Tap to take photo, hold to record video 36 | 37 | 38 | Search 39 | Popular tags 40 | Search history 41 | Related contents 42 | Related tags 43 | %1$s条]]> 44 | --------------------------------------------------------------------------------