├── .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 |
4 |
5 |
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 |
10 |
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 | 
160 |
161 | **excel——>xml**
162 |
163 | 
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 |
--------------------------------------------------------------------------------