├── FIND_UNLOCALIZABLE.md ├── LICENSE ├── LocalToos.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── wangsuyan.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── wangsuyan.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── LocalToos.xcscheme │ └── xcschememanagement.plist ├── LocalToos ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── CHCSVParser.h ├── CHCSVParser.m ├── Info.plist ├── TCZLocalizable │ ├── TCZInfoViewController.h │ ├── TCZInfoViewController.m │ ├── TCZLocalizableTools.h │ ├── TCZLocalizableTools.m │ ├── TCZMainViewController.h │ ├── TCZMainViewController.m │ ├── TCZPriviewViewController.h │ ├── TCZPriviewViewController.m │ └── unLocalizable.py ├── checkLocalizable.py ├── languages.csv ├── localizableError.py ├── main.m ├── source.strings └── unUseImage.py ├── LocalToosTests ├── Info.plist └── LocalToosTests.m ├── LocalToosUITests ├── Info.plist └── LocalToosUITests.m └── README.md /FIND_UNLOCALIZABLE.md: -------------------------------------------------------------------------------- 1 | ### 痛点 2 | 3 | 对于支持多语言的 APP 来说,国际化非常麻烦,而找出项目中未国际化的文字非常耗时(如果单纯的靠手动查找)。虽然可以使用 Xcode 自带的工具(Show not-localized strings)或者 Analyze 找出未国际化的文本,但是它们都不够灵活。如果能直接把项目中未国际化的文本导入到一个文件中,直接给产品,然后再使用 [TCZLocalizableTool](https://github.com/lefex/TCZLocalizableTool) ,岂不是事半功倍。下面这张图就是靠一个 Python 脚本获得的结果: 4 | ![result.png](http://upload-images.jianshu.io/upload_images/1664496-a775df9cfccb899f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 5 | 6 | ### 原理 7 | 8 | 本文主要使用 Python 脚本来查找未国际化的文本。主要思路: 9 | - 给定一个项目地址 10 | - 递归项目中的 `.m` 文件,找出汉语(这里可能有没考虑到的情况,需要读者自行修改源码) 11 | - 写入文件中(可以按照自己的需求写入文件) 12 | 13 | ``` 14 | # coding=utf-8 15 | # 这是一个查找项目中未国际化的脚本 16 | 17 | import os 18 | import re 19 | 20 | # 汉语写入文件时需要 21 | import sys 22 | reload(sys) 23 | sys.setdefaultencoding('utf-8') 24 | 25 | # 将要解析的项目名称 26 | DESPATH = "/Users/wangsuyan/Desktop/Kmart" 27 | 28 | # 解析结果存放的路径 29 | WDESPATH = "/Users/wangsuyan/Desktop/unlocalized.log" 30 | 31 | #目录黑名单,这个目录下所有的文件将被忽略 32 | BLACKDIRLIST = [ 33 | DESPATH + '/Classes/Personal/PERSetting/PERAccount', # 多级目录 34 | DESPATH + '/Utils', # Utils 下所有的文件将被忽略 35 | 'PREPhoneNumResetViewController.m', # 文件名直接写,将忽略这个文件 36 | ] 37 | 38 | # 输出分隔符 39 | SEPREATE = ' <=> ' 40 | 41 | def isInBlackList(filePath): 42 | if os.path.isfile(filePath): 43 | return fileNameAtPath(filePath) in BLACKDIRLIST 44 | if filePath: 45 | return filePath in BLACKDIRLIST 46 | return False 47 | 48 | def fileNameAtPath(filePath): 49 | return os.path.split(filePath)[1] 50 | 51 | def isSignalNote(str): 52 | if '//' in str: 53 | return True 54 | if str.startswith('#pragma'): 55 | return True 56 | return False 57 | 58 | def isLogMsg(str): 59 | if str.startswith('NSLog') or str.startswith('FLOG'): 60 | return True 61 | return False 62 | 63 | def unlocalizedStrs(filePath): 64 | f = open(filePath) 65 | fileName = fileNameAtPath(filePath) 66 | isMutliNote = False 67 | isHaveWriteFileName = False 68 | for index, line in enumerate(f): 69 | #多行注释 70 | line = line.strip() 71 | if '/*' in line: 72 | isMutliNote = True 73 | if '*/' in line: 74 | isMutliNote = False 75 | if isMutliNote: 76 | continue 77 | 78 | #单行注释 79 | if isSignalNote(line): 80 | continue 81 | 82 | #打印信息 83 | if isLogMsg(line): 84 | continue 85 | 86 | matchList = re.findall(u'@"[\u4e00-\u9fff]+', line.decode('utf-8')) 87 | if matchList: 88 | if not isHaveWriteFileName: 89 | wf.write('\n' + fileName + '\n') 90 | isHaveWriteFileName = True 91 | 92 | for item in matchList: 93 | wf.write(str(index + 1) + ':' + item[2 : len(item)] + SEPREATE + line + '\n') 94 | 95 | def findFromFile(path): 96 | paths = os.listdir(path) 97 | for aCompent in paths: 98 | aPath = os.path.join(path, aCompent) 99 | if isInBlackList(aPath): 100 | print('在黑名单中,被自动忽略' + aPath) 101 | continue 102 | if os.path.isdir(aPath): 103 | findFromFile(aPath) 104 | elif os.path.isfile(aPath) and os.path.splitext(aPath)[1]=='.m': 105 | unlocalizedStrs(aPath) 106 | 107 | if __name__ == '__main__': 108 | wf = open(WDESPATH, 'w') 109 | findFromFile(DESPATH) 110 | wf.close() 111 | ``` 112 | 113 | ### 使用 114 | - 修改 DESPATH 路径为你项目的路径 115 | - 直接在脚本所在的目录下,执行 `python unLocalizable.py`,这个的 `unLocalizable.py` 为脚本文件名。你可以在 [这里](https://github.com/lefex/TCZLocalizableTool/blob/master/LocalToos/TCZLocalizable/unLocalizable.py) 找到脚本文件。 116 | - BLACKDIRLIST 你可以过滤掉和国际化无关的文件,比如某些第三方库。 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lefe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LocalToos.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 230A68E11F4D838D009481A2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 230A68E01F4D838D009481A2 /* main.m */; }; 11 | 230A68E41F4D838D009481A2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 230A68E31F4D838D009481A2 /* AppDelegate.m */; }; 12 | 230A68EA1F4D838D009481A2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 230A68E81F4D838D009481A2 /* Main.storyboard */; }; 13 | 230A68EC1F4D838D009481A2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 230A68EB1F4D838D009481A2 /* Assets.xcassets */; }; 14 | 230A68EF1F4D838D009481A2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 230A68ED1F4D838D009481A2 /* LaunchScreen.storyboard */; }; 15 | 230A68FA1F4D838D009481A2 /* LocalToosTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 230A68F91F4D838D009481A2 /* LocalToosTests.m */; }; 16 | 230A69051F4D838D009481A2 /* LocalToosUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 230A69041F4D838D009481A2 /* LocalToosUITests.m */; }; 17 | 230A691B1F4D87EE009481A2 /* CHCSVParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 230A691A1F4D87EE009481A2 /* CHCSVParser.m */; }; 18 | 230A691E1F4D887A009481A2 /* languages.csv in Resources */ = {isa = PBXBuildFile; fileRef = 230A691C1F4D887A009481A2 /* languages.csv */; }; 19 | 230A69211F4D8A92009481A2 /* source.strings in Resources */ = {isa = PBXBuildFile; fileRef = 230A69201F4D8A92009481A2 /* source.strings */; }; 20 | 23527AEE1F8BB6CE003DFCD9 /* unLocalizable.py in Resources */ = {isa = PBXBuildFile; fileRef = 23527AED1F8BB6CE003DFCD9 /* unLocalizable.py */; }; 21 | 235521B01F4E8F4F005F38B9 /* TCZInfoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 235521A91F4E8F4F005F38B9 /* TCZInfoViewController.m */; }; 22 | 235521B11F4E8F4F005F38B9 /* TCZLocalizableTools.m in Sources */ = {isa = PBXBuildFile; fileRef = 235521AB1F4E8F4F005F38B9 /* TCZLocalizableTools.m */; }; 23 | 235521B21F4E8F4F005F38B9 /* TCZMainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 235521AD1F4E8F4F005F38B9 /* TCZMainViewController.m */; }; 24 | 235521B31F4E8F4F005F38B9 /* TCZPriviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 235521AF1F4E8F4F005F38B9 /* TCZPriviewViewController.m */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXContainerItemProxy section */ 28 | 230A68F61F4D838D009481A2 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 230A68D41F4D838D009481A2 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 230A68DB1F4D838D009481A2; 33 | remoteInfo = LocalToos; 34 | }; 35 | 230A69011F4D838D009481A2 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 230A68D41F4D838D009481A2 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 230A68DB1F4D838D009481A2; 40 | remoteInfo = LocalToos; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 230A68DC1F4D838D009481A2 /* LocalToos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LocalToos.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 230A68E01F4D838D009481A2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 47 | 230A68E21F4D838D009481A2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 48 | 230A68E31F4D838D009481A2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 49 | 230A68E91F4D838D009481A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 230A68EB1F4D838D009481A2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 230A68EE1F4D838D009481A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 230A68F01F4D838D009481A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 230A68F51F4D838D009481A2 /* LocalToosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LocalToosTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 230A68F91F4D838D009481A2 /* LocalToosTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalToosTests.m; sourceTree = ""; }; 55 | 230A68FB1F4D838D009481A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 230A69001F4D838D009481A2 /* LocalToosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LocalToosUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 230A69041F4D838D009481A2 /* LocalToosUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalToosUITests.m; sourceTree = ""; }; 58 | 230A69061F4D838D009481A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 230A69191F4D87EE009481A2 /* CHCSVParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHCSVParser.h; sourceTree = ""; }; 60 | 230A691A1F4D87EE009481A2 /* CHCSVParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CHCSVParser.m; sourceTree = ""; }; 61 | 230A691C1F4D887A009481A2 /* languages.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = languages.csv; sourceTree = ""; }; 62 | 230A69201F4D8A92009481A2 /* source.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = source.strings; sourceTree = ""; }; 63 | 23527AED1F8BB6CE003DFCD9 /* unLocalizable.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = unLocalizable.py; sourceTree = ""; }; 64 | 235521A81F4E8F4F005F38B9 /* TCZInfoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TCZInfoViewController.h; sourceTree = ""; }; 65 | 235521A91F4E8F4F005F38B9 /* TCZInfoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TCZInfoViewController.m; sourceTree = ""; }; 66 | 235521AA1F4E8F4F005F38B9 /* TCZLocalizableTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TCZLocalizableTools.h; sourceTree = ""; }; 67 | 235521AB1F4E8F4F005F38B9 /* TCZLocalizableTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TCZLocalizableTools.m; sourceTree = ""; }; 68 | 235521AC1F4E8F4F005F38B9 /* TCZMainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TCZMainViewController.h; sourceTree = ""; }; 69 | 235521AD1F4E8F4F005F38B9 /* TCZMainViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TCZMainViewController.m; sourceTree = ""; }; 70 | 235521AE1F4E8F4F005F38B9 /* TCZPriviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TCZPriviewViewController.h; sourceTree = ""; }; 71 | 235521AF1F4E8F4F005F38B9 /* TCZPriviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TCZPriviewViewController.m; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | 230A68D91F4D838D009481A2 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | 230A68F21F4D838D009481A2 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | 230A68FD1F4D838D009481A2 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 230A68D31F4D838D009481A2 = { 100 | isa = PBXGroup; 101 | children = ( 102 | 230A68DE1F4D838D009481A2 /* LocalToos */, 103 | 230A68F81F4D838D009481A2 /* LocalToosTests */, 104 | 230A69031F4D838D009481A2 /* LocalToosUITests */, 105 | 230A68DD1F4D838D009481A2 /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 230A68DD1F4D838D009481A2 /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 230A68DC1F4D838D009481A2 /* LocalToos.app */, 113 | 230A68F51F4D838D009481A2 /* LocalToosTests.xctest */, 114 | 230A69001F4D838D009481A2 /* LocalToosUITests.xctest */, 115 | ); 116 | name = Products; 117 | sourceTree = ""; 118 | }; 119 | 230A68DE1F4D838D009481A2 /* LocalToos */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 235521A71F4E8F4F005F38B9 /* TCZLocalizable */, 123 | 230A691C1F4D887A009481A2 /* languages.csv */, 124 | 230A69191F4D87EE009481A2 /* CHCSVParser.h */, 125 | 230A691A1F4D87EE009481A2 /* CHCSVParser.m */, 126 | 230A68E21F4D838D009481A2 /* AppDelegate.h */, 127 | 230A68E31F4D838D009481A2 /* AppDelegate.m */, 128 | 230A68E81F4D838D009481A2 /* Main.storyboard */, 129 | 230A68EB1F4D838D009481A2 /* Assets.xcassets */, 130 | 230A68ED1F4D838D009481A2 /* LaunchScreen.storyboard */, 131 | 230A68F01F4D838D009481A2 /* Info.plist */, 132 | 230A68DF1F4D838D009481A2 /* Supporting Files */, 133 | 230A69201F4D8A92009481A2 /* source.strings */, 134 | ); 135 | path = LocalToos; 136 | sourceTree = ""; 137 | }; 138 | 230A68DF1F4D838D009481A2 /* Supporting Files */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 230A68E01F4D838D009481A2 /* main.m */, 142 | ); 143 | name = "Supporting Files"; 144 | sourceTree = ""; 145 | }; 146 | 230A68F81F4D838D009481A2 /* LocalToosTests */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 230A68F91F4D838D009481A2 /* LocalToosTests.m */, 150 | 230A68FB1F4D838D009481A2 /* Info.plist */, 151 | ); 152 | path = LocalToosTests; 153 | sourceTree = ""; 154 | }; 155 | 230A69031F4D838D009481A2 /* LocalToosUITests */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 230A69041F4D838D009481A2 /* LocalToosUITests.m */, 159 | 230A69061F4D838D009481A2 /* Info.plist */, 160 | ); 161 | path = LocalToosUITests; 162 | sourceTree = ""; 163 | }; 164 | 235521A71F4E8F4F005F38B9 /* TCZLocalizable */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 23527AED1F8BB6CE003DFCD9 /* unLocalizable.py */, 168 | 235521A81F4E8F4F005F38B9 /* TCZInfoViewController.h */, 169 | 235521A91F4E8F4F005F38B9 /* TCZInfoViewController.m */, 170 | 235521AA1F4E8F4F005F38B9 /* TCZLocalizableTools.h */, 171 | 235521AB1F4E8F4F005F38B9 /* TCZLocalizableTools.m */, 172 | 235521AC1F4E8F4F005F38B9 /* TCZMainViewController.h */, 173 | 235521AD1F4E8F4F005F38B9 /* TCZMainViewController.m */, 174 | 235521AE1F4E8F4F005F38B9 /* TCZPriviewViewController.h */, 175 | 235521AF1F4E8F4F005F38B9 /* TCZPriviewViewController.m */, 176 | ); 177 | path = TCZLocalizable; 178 | sourceTree = ""; 179 | }; 180 | /* End PBXGroup section */ 181 | 182 | /* Begin PBXNativeTarget section */ 183 | 230A68DB1F4D838D009481A2 /* LocalToos */ = { 184 | isa = PBXNativeTarget; 185 | buildConfigurationList = 230A69091F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToos" */; 186 | buildPhases = ( 187 | 230A68D81F4D838D009481A2 /* Sources */, 188 | 230A68D91F4D838D009481A2 /* Frameworks */, 189 | 230A68DA1F4D838D009481A2 /* Resources */, 190 | ); 191 | buildRules = ( 192 | ); 193 | dependencies = ( 194 | ); 195 | name = LocalToos; 196 | productName = LocalToos; 197 | productReference = 230A68DC1F4D838D009481A2 /* LocalToos.app */; 198 | productType = "com.apple.product-type.application"; 199 | }; 200 | 230A68F41F4D838D009481A2 /* LocalToosTests */ = { 201 | isa = PBXNativeTarget; 202 | buildConfigurationList = 230A690C1F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToosTests" */; 203 | buildPhases = ( 204 | 230A68F11F4D838D009481A2 /* Sources */, 205 | 230A68F21F4D838D009481A2 /* Frameworks */, 206 | 230A68F31F4D838D009481A2 /* Resources */, 207 | ); 208 | buildRules = ( 209 | ); 210 | dependencies = ( 211 | 230A68F71F4D838D009481A2 /* PBXTargetDependency */, 212 | ); 213 | name = LocalToosTests; 214 | productName = LocalToosTests; 215 | productReference = 230A68F51F4D838D009481A2 /* LocalToosTests.xctest */; 216 | productType = "com.apple.product-type.bundle.unit-test"; 217 | }; 218 | 230A68FF1F4D838D009481A2 /* LocalToosUITests */ = { 219 | isa = PBXNativeTarget; 220 | buildConfigurationList = 230A690F1F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToosUITests" */; 221 | buildPhases = ( 222 | 230A68FC1F4D838D009481A2 /* Sources */, 223 | 230A68FD1F4D838D009481A2 /* Frameworks */, 224 | 230A68FE1F4D838D009481A2 /* Resources */, 225 | ); 226 | buildRules = ( 227 | ); 228 | dependencies = ( 229 | 230A69021F4D838D009481A2 /* PBXTargetDependency */, 230 | ); 231 | name = LocalToosUITests; 232 | productName = LocalToosUITests; 233 | productReference = 230A69001F4D838D009481A2 /* LocalToosUITests.xctest */; 234 | productType = "com.apple.product-type.bundle.ui-testing"; 235 | }; 236 | /* End PBXNativeTarget section */ 237 | 238 | /* Begin PBXProject section */ 239 | 230A68D41F4D838D009481A2 /* Project object */ = { 240 | isa = PBXProject; 241 | attributes = { 242 | LastUpgradeCheck = 0830; 243 | ORGANIZATIONNAME = WangSuyan; 244 | TargetAttributes = { 245 | 230A68DB1F4D838D009481A2 = { 246 | CreatedOnToolsVersion = 8.3.3; 247 | DevelopmentTeam = D47CZT3KBD; 248 | ProvisioningStyle = Automatic; 249 | }; 250 | 230A68F41F4D838D009481A2 = { 251 | CreatedOnToolsVersion = 8.3.3; 252 | DevelopmentTeam = D47CZT3KBD; 253 | ProvisioningStyle = Automatic; 254 | TestTargetID = 230A68DB1F4D838D009481A2; 255 | }; 256 | 230A68FF1F4D838D009481A2 = { 257 | CreatedOnToolsVersion = 8.3.3; 258 | DevelopmentTeam = D47CZT3KBD; 259 | ProvisioningStyle = Automatic; 260 | TestTargetID = 230A68DB1F4D838D009481A2; 261 | }; 262 | }; 263 | }; 264 | buildConfigurationList = 230A68D71F4D838D009481A2 /* Build configuration list for PBXProject "LocalToos" */; 265 | compatibilityVersion = "Xcode 3.2"; 266 | developmentRegion = English; 267 | hasScannedForEncodings = 0; 268 | knownRegions = ( 269 | en, 270 | Base, 271 | ); 272 | mainGroup = 230A68D31F4D838D009481A2; 273 | productRefGroup = 230A68DD1F4D838D009481A2 /* Products */; 274 | projectDirPath = ""; 275 | projectRoot = ""; 276 | targets = ( 277 | 230A68DB1F4D838D009481A2 /* LocalToos */, 278 | 230A68F41F4D838D009481A2 /* LocalToosTests */, 279 | 230A68FF1F4D838D009481A2 /* LocalToosUITests */, 280 | ); 281 | }; 282 | /* End PBXProject section */ 283 | 284 | /* Begin PBXResourcesBuildPhase section */ 285 | 230A68DA1F4D838D009481A2 /* Resources */ = { 286 | isa = PBXResourcesBuildPhase; 287 | buildActionMask = 2147483647; 288 | files = ( 289 | 230A68EF1F4D838D009481A2 /* LaunchScreen.storyboard in Resources */, 290 | 230A68EC1F4D838D009481A2 /* Assets.xcassets in Resources */, 291 | 23527AEE1F8BB6CE003DFCD9 /* unLocalizable.py in Resources */, 292 | 230A68EA1F4D838D009481A2 /* Main.storyboard in Resources */, 293 | 230A691E1F4D887A009481A2 /* languages.csv in Resources */, 294 | 230A69211F4D8A92009481A2 /* source.strings in Resources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | 230A68F31F4D838D009481A2 /* Resources */ = { 299 | isa = PBXResourcesBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | 230A68FE1F4D838D009481A2 /* Resources */ = { 306 | isa = PBXResourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | /* End PBXResourcesBuildPhase section */ 313 | 314 | /* Begin PBXSourcesBuildPhase section */ 315 | 230A68D81F4D838D009481A2 /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 230A691B1F4D87EE009481A2 /* CHCSVParser.m in Sources */, 320 | 235521B21F4E8F4F005F38B9 /* TCZMainViewController.m in Sources */, 321 | 230A68E41F4D838D009481A2 /* AppDelegate.m in Sources */, 322 | 235521B31F4E8F4F005F38B9 /* TCZPriviewViewController.m in Sources */, 323 | 235521B11F4E8F4F005F38B9 /* TCZLocalizableTools.m in Sources */, 324 | 235521B01F4E8F4F005F38B9 /* TCZInfoViewController.m in Sources */, 325 | 230A68E11F4D838D009481A2 /* main.m in Sources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | 230A68F11F4D838D009481A2 /* Sources */ = { 330 | isa = PBXSourcesBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | 230A68FA1F4D838D009481A2 /* LocalToosTests.m in Sources */, 334 | ); 335 | runOnlyForDeploymentPostprocessing = 0; 336 | }; 337 | 230A68FC1F4D838D009481A2 /* Sources */ = { 338 | isa = PBXSourcesBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | 230A69051F4D838D009481A2 /* LocalToosUITests.m in Sources */, 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | /* End PBXSourcesBuildPhase section */ 346 | 347 | /* Begin PBXTargetDependency section */ 348 | 230A68F71F4D838D009481A2 /* PBXTargetDependency */ = { 349 | isa = PBXTargetDependency; 350 | target = 230A68DB1F4D838D009481A2 /* LocalToos */; 351 | targetProxy = 230A68F61F4D838D009481A2 /* PBXContainerItemProxy */; 352 | }; 353 | 230A69021F4D838D009481A2 /* PBXTargetDependency */ = { 354 | isa = PBXTargetDependency; 355 | target = 230A68DB1F4D838D009481A2 /* LocalToos */; 356 | targetProxy = 230A69011F4D838D009481A2 /* PBXContainerItemProxy */; 357 | }; 358 | /* End PBXTargetDependency section */ 359 | 360 | /* Begin PBXVariantGroup section */ 361 | 230A68E81F4D838D009481A2 /* Main.storyboard */ = { 362 | isa = PBXVariantGroup; 363 | children = ( 364 | 230A68E91F4D838D009481A2 /* Base */, 365 | ); 366 | name = Main.storyboard; 367 | sourceTree = ""; 368 | }; 369 | 230A68ED1F4D838D009481A2 /* LaunchScreen.storyboard */ = { 370 | isa = PBXVariantGroup; 371 | children = ( 372 | 230A68EE1F4D838D009481A2 /* Base */, 373 | ); 374 | name = LaunchScreen.storyboard; 375 | sourceTree = ""; 376 | }; 377 | /* End PBXVariantGroup section */ 378 | 379 | /* Begin XCBuildConfiguration section */ 380 | 230A69071F4D838D009481A2 /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_NONNULL = YES; 385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_WARN_BOOL_CONVERSION = YES; 391 | CLANG_WARN_CONSTANT_CONVERSION = YES; 392 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 393 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INFINITE_RECURSION = YES; 397 | CLANG_WARN_INT_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 400 | CLANG_WARN_UNREACHABLE_CODE = YES; 401 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 402 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 403 | COPY_PHASE_STRIP = NO; 404 | DEBUG_INFORMATION_FORMAT = dwarf; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | ENABLE_TESTABILITY = YES; 407 | GCC_C_LANGUAGE_STANDARD = gnu99; 408 | GCC_DYNAMIC_NO_PIC = NO; 409 | GCC_NO_COMMON_BLOCKS = YES; 410 | GCC_OPTIMIZATION_LEVEL = 0; 411 | GCC_PREPROCESSOR_DEFINITIONS = ( 412 | "DEBUG=1", 413 | "$(inherited)", 414 | ); 415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 417 | GCC_WARN_UNDECLARED_SELECTOR = YES; 418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 419 | GCC_WARN_UNUSED_FUNCTION = YES; 420 | GCC_WARN_UNUSED_VARIABLE = YES; 421 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 422 | MTL_ENABLE_DEBUG_INFO = YES; 423 | ONLY_ACTIVE_ARCH = YES; 424 | SDKROOT = iphoneos; 425 | }; 426 | name = Debug; 427 | }; 428 | 230A69081F4D838D009481A2 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ALWAYS_SEARCH_USER_PATHS = NO; 432 | CLANG_ANALYZER_NONNULL = YES; 433 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 434 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 435 | CLANG_CXX_LIBRARY = "libc++"; 436 | CLANG_ENABLE_MODULES = YES; 437 | CLANG_ENABLE_OBJC_ARC = YES; 438 | CLANG_WARN_BOOL_CONVERSION = YES; 439 | CLANG_WARN_CONSTANT_CONVERSION = YES; 440 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 441 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 442 | CLANG_WARN_EMPTY_BODY = YES; 443 | CLANG_WARN_ENUM_CONVERSION = YES; 444 | CLANG_WARN_INFINITE_RECURSION = YES; 445 | CLANG_WARN_INT_CONVERSION = YES; 446 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 447 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 448 | CLANG_WARN_UNREACHABLE_CODE = YES; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 451 | COPY_PHASE_STRIP = NO; 452 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 453 | ENABLE_NS_ASSERTIONS = NO; 454 | ENABLE_STRICT_OBJC_MSGSEND = YES; 455 | GCC_C_LANGUAGE_STANDARD = gnu99; 456 | GCC_NO_COMMON_BLOCKS = YES; 457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 459 | GCC_WARN_UNDECLARED_SELECTOR = YES; 460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 461 | GCC_WARN_UNUSED_FUNCTION = YES; 462 | GCC_WARN_UNUSED_VARIABLE = YES; 463 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 464 | MTL_ENABLE_DEBUG_INFO = NO; 465 | SDKROOT = iphoneos; 466 | VALIDATE_PRODUCT = YES; 467 | }; 468 | name = Release; 469 | }; 470 | 230A690A1F4D838D009481A2 /* Debug */ = { 471 | isa = XCBuildConfiguration; 472 | buildSettings = { 473 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 474 | DEVELOPMENT_TEAM = D47CZT3KBD; 475 | INFOPLIST_FILE = LocalToos/Info.plist; 476 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 477 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 478 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToos; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | }; 481 | name = Debug; 482 | }; 483 | 230A690B1F4D838D009481A2 /* Release */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 487 | DEVELOPMENT_TEAM = D47CZT3KBD; 488 | INFOPLIST_FILE = LocalToos/Info.plist; 489 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 490 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 491 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToos; 492 | PRODUCT_NAME = "$(TARGET_NAME)"; 493 | }; 494 | name = Release; 495 | }; 496 | 230A690D1F4D838D009481A2 /* Debug */ = { 497 | isa = XCBuildConfiguration; 498 | buildSettings = { 499 | BUNDLE_LOADER = "$(TEST_HOST)"; 500 | DEVELOPMENT_TEAM = D47CZT3KBD; 501 | INFOPLIST_FILE = LocalToosTests/Info.plist; 502 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 503 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToosTests; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LocalToos.app/LocalToos"; 506 | }; 507 | name = Debug; 508 | }; 509 | 230A690E1F4D838D009481A2 /* Release */ = { 510 | isa = XCBuildConfiguration; 511 | buildSettings = { 512 | BUNDLE_LOADER = "$(TEST_HOST)"; 513 | DEVELOPMENT_TEAM = D47CZT3KBD; 514 | INFOPLIST_FILE = LocalToosTests/Info.plist; 515 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 516 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToosTests; 517 | PRODUCT_NAME = "$(TARGET_NAME)"; 518 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LocalToos.app/LocalToos"; 519 | }; 520 | name = Release; 521 | }; 522 | 230A69101F4D838D009481A2 /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | DEVELOPMENT_TEAM = D47CZT3KBD; 526 | INFOPLIST_FILE = LocalToosUITests/Info.plist; 527 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 528 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToosUITests; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | TEST_TARGET_NAME = LocalToos; 531 | }; 532 | name = Debug; 533 | }; 534 | 230A69111F4D838D009481A2 /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | DEVELOPMENT_TEAM = D47CZT3KBD; 538 | INFOPLIST_FILE = LocalToosUITests/Info.plist; 539 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 540 | PRODUCT_BUNDLE_IDENTIFIER = tczy.LocalToosUITests; 541 | PRODUCT_NAME = "$(TARGET_NAME)"; 542 | TEST_TARGET_NAME = LocalToos; 543 | }; 544 | name = Release; 545 | }; 546 | /* End XCBuildConfiguration section */ 547 | 548 | /* Begin XCConfigurationList section */ 549 | 230A68D71F4D838D009481A2 /* Build configuration list for PBXProject "LocalToos" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | 230A69071F4D838D009481A2 /* Debug */, 553 | 230A69081F4D838D009481A2 /* Release */, 554 | ); 555 | defaultConfigurationIsVisible = 0; 556 | defaultConfigurationName = Release; 557 | }; 558 | 230A69091F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToos" */ = { 559 | isa = XCConfigurationList; 560 | buildConfigurations = ( 561 | 230A690A1F4D838D009481A2 /* Debug */, 562 | 230A690B1F4D838D009481A2 /* Release */, 563 | ); 564 | defaultConfigurationIsVisible = 0; 565 | defaultConfigurationName = Release; 566 | }; 567 | 230A690C1F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToosTests" */ = { 568 | isa = XCConfigurationList; 569 | buildConfigurations = ( 570 | 230A690D1F4D838D009481A2 /* Debug */, 571 | 230A690E1F4D838D009481A2 /* Release */, 572 | ); 573 | defaultConfigurationIsVisible = 0; 574 | defaultConfigurationName = Release; 575 | }; 576 | 230A690F1F4D838D009481A2 /* Build configuration list for PBXNativeTarget "LocalToosUITests" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | 230A69101F4D838D009481A2 /* Debug */, 580 | 230A69111F4D838D009481A2 /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | /* End XCConfigurationList section */ 586 | }; 587 | rootObject = 230A68D41F4D838D009481A2 /* Project object */; 588 | } 589 | -------------------------------------------------------------------------------- /LocalToos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LocalToos.xcodeproj/project.xcworkspace/xcuserdata/wangsuyan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lefex/TCZLocalizableTool/e1460cfe1ca4ab66bd07d13089f74f970d299283/LocalToos.xcodeproj/project.xcworkspace/xcuserdata/wangsuyan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /LocalToos.xcodeproj/xcuserdata/wangsuyan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /LocalToos.xcodeproj/xcuserdata/wangsuyan.xcuserdatad/xcschemes/LocalToos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 76 | 78 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /LocalToos.xcodeproj/xcuserdata/wangsuyan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LocalToos.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 230A68DB1F4D838D009481A2 16 | 17 | primary 18 | 19 | 20 | 230A68F41F4D838D009481A2 21 | 22 | primary 23 | 24 | 25 | 230A68FF1F4D838D009481A2 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /LocalToos/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/23. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /LocalToos/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/23. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /LocalToos/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LocalToos/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LocalToos/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /LocalToos/CHCSVParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // CHCSVParser.h 3 | // CHCSVParser 4 | /** 5 | Copyright (c) 2014 Dave DeLong 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | **/ 25 | 26 | #import 27 | 28 | #ifndef NS_DESIGNATED_INITIALIZER 29 | #define NS_DESIGNATED_INITIALIZER 30 | #endif 31 | 32 | #ifndef CHCSV_DEPRECATED 33 | #define CHCSV_DEPRECATED(...) __attribute__((deprecated("" #__VA_ARGS__))) 34 | #endif 35 | 36 | extern NSString * const CHCSVErrorDomain; 37 | 38 | typedef NS_ENUM(NSInteger, CHCSVErrorCode) { 39 | /** 40 | * Indicates that a delimited file is incorrectly formatted. 41 | * For example, perhaps a double quote is in the wrong position. 42 | */ 43 | CHCSVErrorCodeInvalidFormat = 1, 44 | 45 | /** 46 | * When using @c CHCSVParserOptionsUsesFirstLineAsKeys, all of the lines in the file 47 | * must have the same number of fields. If they do not, parsing is aborted and this error is returned. 48 | */ 49 | CHCSVErrorCodeIncorrectNumberOfFields, 50 | }; 51 | 52 | @class CHCSVParser; 53 | @protocol CHCSVParserDelegate 54 | 55 | @optional 56 | 57 | /** 58 | * Indicates that the parser has started parsing the stream 59 | * 60 | * @param parser The @c CHCSVParser instance 61 | */ 62 | - (void)parserDidBeginDocument:(CHCSVParser *)parser; 63 | 64 | /** 65 | * Indicates that the parser has successfully finished parsing the stream 66 | * 67 | * This method is not invoked if any error is encountered 68 | * 69 | * @param parser The @c CHCSVParser instance 70 | */ 71 | - (void)parserDidEndDocument:(CHCSVParser *)parser; 72 | 73 | /** 74 | * Indicates the parser has started parsing a line 75 | * 76 | * @param parser The @c CHCSVParser instance 77 | * @param recordNumber The 1-based number of the record 78 | */ 79 | - (void)parser:(CHCSVParser *)parser didBeginLine:(NSUInteger)recordNumber; 80 | 81 | /** 82 | * Indicates the parser has finished parsing a line 83 | * 84 | * @param parser The @c CHCSVParser instance 85 | * @param recordNumber The 1-based number of the record 86 | */ 87 | - (void)parser:(CHCSVParser *)parser didEndLine:(NSUInteger)recordNumber; 88 | 89 | /** 90 | * Indicates the parser has parsed a field on the current line 91 | * 92 | * @param parser The @c CHCSVParser instance 93 | * @param field The parsed string. If configured to do so, this string may be sanitized and trimmed 94 | * @param fieldIndex The 0-based index of the field within the current record 95 | */ 96 | - (void)parser:(CHCSVParser *)parser didReadField:(NSString *)field atIndex:(NSInteger)fieldIndex; 97 | 98 | /** 99 | * Indicates the parser has encountered a comment 100 | * 101 | * This method is only invoked if @c CHCSVParser.recognizesComments is @c YES 102 | * 103 | * @param parser The @c CHCSVParser instance 104 | * @param comment The parsed comment 105 | */ 106 | - (void)parser:(CHCSVParser *)parser didReadComment:(NSString *)comment; 107 | 108 | /** 109 | * Indicates the parser encounter an error while parsing 110 | * 111 | * @param parser The @c CHCSVParser instance 112 | * @param error The @c NSError instance 113 | */ 114 | - (void)parser:(CHCSVParser *)parser didFailWithError:(NSError *)error; 115 | 116 | @end 117 | 118 | @interface CHCSVParser : NSObject 119 | 120 | /** 121 | * The delegate for the @c CHCSVParser 122 | */ 123 | @property (assign) id delegate; 124 | 125 | /** 126 | * If @c YES, then the parser will removing surrounding double quotes and will unescape characters. 127 | * The default value is @c NO. 128 | * @warning Do not mutate this property after parsing has begun 129 | */ 130 | @property (nonatomic, assign) BOOL sanitizesFields; 131 | 132 | /** 133 | * If @c YES, then the parser will trim whitespace around fields. If @c sanitizesFields is also @c YES, 134 | * then the sanitized field is also trimmed. The default value is @c NO. 135 | * @warning Do not mutate this property after parsing has begun 136 | */ 137 | @property (nonatomic, assign) BOOL trimsWhitespace; 138 | 139 | /** 140 | * If @c YES, then the parser will allow special characters (delimiter, newline, quote, etc) 141 | * to be escaped within a field using a backslash character. The default value is @c NO. 142 | * @warning Do not mutate this property after parsing has begun 143 | */ 144 | @property (nonatomic, assign) BOOL recognizesBackslashesAsEscapes; 145 | 146 | /** 147 | * If @c YES, then the parser will interpret any field that begins with an octothorpe as a comment. 148 | * Comments are terminated using an unescaped newline character. The default value is @c NO. 149 | * @warning Do not mutate this property after parsing has begun 150 | */ 151 | @property (nonatomic, assign) BOOL recognizesComments; 152 | 153 | /** 154 | * If @c YES, then quoted fields may begin with an equal sign. 155 | * Some programs produce fields with a leading equal sign to indicate that the contents must be represented exactly. 156 | * The default value is @c NO. 157 | * @warning Do not mutate this property after parsing has begun 158 | */ 159 | @property (nonatomic, assign) BOOL recognizesLeadingEqualSign; 160 | 161 | /** 162 | * The number of bytes that have been read from the input stream so far 163 | * 164 | * This property is key-value observable. 165 | */ 166 | @property (readonly) NSUInteger totalBytesRead; 167 | 168 | 169 | /** 170 | * This method is unavailable, because there is nothing supplied to parse. 171 | */ 172 | - (instancetype)init NS_UNAVAILABLE; 173 | 174 | /** 175 | * The designated initializer 176 | * 177 | * @param stream The @c NSInputStream from which bytes will be read and parsed. Must not be @c nil 178 | * @param encoding A pointer to an @c NSStringEncoding. If non-nil, this will be filled in with the encoding used to parse the stream 179 | * @param delimiter The delimiter character to be used when parsing the stream. Must not be @c nil, and may not be the double quote character 180 | * 181 | * @return a @c CHCSVParser instance, or @c nil if initialization failed 182 | */ 183 | - (instancetype)initWithInputStream:(NSInputStream *)stream usedEncoding:(inout NSStringEncoding *)encoding delimiter:(unichar)delimiter NS_DESIGNATED_INITIALIZER; 184 | 185 | /** 186 | * An initializer to parse a CSV string 187 | * 188 | * Internally it calls the designated initializer and provides a stream of the UTF8 representation of the string as well as the comma delimiter. 189 | * 190 | * @param csv The @c NSString to parse. Must not be @c nil 191 | * 192 | * @return a @c CHCSVParser instance, or @c nil if initialization failed 193 | */ 194 | - (instancetype)initWithCSVString:(NSString *)csv; 195 | 196 | /** 197 | * An initializer to parse a delimited string 198 | * 199 | * Internally it calls the designated initializer and provides a stream of the UTF8 representation of the string as well as the provided delimiter. 200 | * 201 | * @param string The @c NSString to parse. Must not be @c nil 202 | * @param delimiter The delimiter character to be used when parsing the string. Must not be @c nil, and may not be the double quote character 203 | * 204 | * @return a @c CHCSVParser instance, or @c nil if initialization failed 205 | */ 206 | - (instancetype)initWithDelimitedString:(NSString *)string delimiter:(unichar)delimiter; 207 | 208 | /** 209 | * An initializer to parse the contents of URL 210 | * 211 | * Internally it calls the designated initializer and provides a stream to the URL as well as the comma delimiter. 212 | * The parser attempts to infer the encoding from the stream. 213 | * 214 | * @param csvURL The @c NSURL to the CSV file 215 | * 216 | * @return a @c CHCSVParser instance, or @c nil if initialization failed 217 | */ 218 | - (instancetype)initWithContentsOfCSVURL:(NSURL *)csvURL; 219 | 220 | /** 221 | * An initializer to parse the contents of URL 222 | * 223 | * Internally it calls the designated initializer and provides a stream to the URL as well as the provided delimiter. 224 | * The parser attempts to infer the encoding from the stream. 225 | * 226 | * @param URL The @c NSURL to the delimited file 227 | * @param delimiter The delimiter character to be used when parsing the string. Must not be @c nil, and may not be the double quote character 228 | * 229 | * @return a @c CHCSVParser instance, or @c nil if initialization failed 230 | */ 231 | - (instancetype)initWithContentsOfDelimitedURL:(NSURL *)URL delimiter:(unichar)delimiter; 232 | 233 | /** 234 | * Instruct the parser to begin parsing 235 | * 236 | * You should not invoke this method more than once. 237 | */ 238 | - (void)parse; 239 | 240 | /** 241 | * Instruct the parser to abort parsing 242 | * 243 | * Invoking this method after parsing has completed has no effect. 244 | */ 245 | - (void)cancelParsing; 246 | 247 | @end 248 | 249 | @interface CHCSVWriter : NSObject 250 | 251 | /** 252 | * This method is unavailable, because there is no way to extract the written CSV. 253 | */ 254 | - (instancetype)init NS_UNAVAILABLE; 255 | 256 | /** 257 | * Initializes a @c CHCSVWriter to write to the provided file path. Assumes @c NSUTF8Encoding and the comma delimiter 258 | * 259 | * @param path The path to the CSV file 260 | * 261 | * @return a @c CHCSVWriter instance, or @c nil if initialization failed 262 | */ 263 | - (instancetype)initForWritingToCSVFile:(NSString *)path; 264 | 265 | /** 266 | * The designated initializer 267 | * 268 | * @param stream The @c NSOutputStream to which bytes will be written. 269 | * If you wish to append to an existing file, you can provide an @c NSOutputStream that is set to append to the target file 270 | * @param encoding The byte encoding to use when writing strings to the stream 271 | * @param delimiter The field delimiter to use during writing 272 | * 273 | * @return a @c CHCSVWriter instance, or @c nil if initialization failed 274 | */ 275 | - (instancetype)initWithOutputStream:(NSOutputStream *)stream encoding:(NSStringEncoding)encoding delimiter:(unichar)delimiter NS_DESIGNATED_INITIALIZER; 276 | 277 | /** 278 | * Write a field to the output stream 279 | * 280 | * If necessary, this will also write a delimiter to the stream as well. This method takes care of all escaping. 281 | * 282 | * @param field The object to be written to the stream 283 | * If you provide an object that is not an @c NSString, its @c description will be written to the stream. 284 | */ 285 | - (void)writeField:(id)field; 286 | 287 | /** 288 | * Write a newline character to the output stream 289 | */ 290 | - (void)finishLine; 291 | 292 | /** 293 | * Write a series of fields to the stream as a new line 294 | * 295 | * If another line is already in progress, it is terminated and a new line is begun. 296 | * This method iteratively invokes @c writeField:, followed by @c finishLine. 297 | * 298 | * @param fields A sequence of fields to be written 299 | */ 300 | - (void)writeLineOfFields:(id)fields; 301 | 302 | /** 303 | * Write the contents of an @c NSDictionary as a new line 304 | * 305 | * @param dictionary The @c NSDictionary whose values will be written to the output stream. 306 | * Values will be written in the order specified by the first line of fields written to the stream. 307 | * If no lines have been written yet, this method will throw an exception. 308 | */ 309 | - (void)writeLineWithDictionary:(NSDictionary *)dictionary; 310 | 311 | /** 312 | * Write a comment to the stream 313 | * 314 | * If another line is already in progress, it is terminated and a new line is begun. 315 | * The new line will be started with the octothorpe (#) character, followed by the comment. 316 | * The comment is terminated using @c finishLine 317 | * 318 | * @param comment The comment to be written to the stream 319 | */ 320 | - (void)writeComment:(NSString *)comment; 321 | 322 | /** 323 | * Closes the output stream. 324 | * You do not have to invoke this method yourself, as it is invoked during deallocation. 325 | */ 326 | - (void)closeStream; 327 | 328 | @end 329 | 330 | #pragma mark - Convenience Categories 331 | 332 | typedef NS_OPTIONS(NSUInteger, CHCSVParserOptions) { 333 | /** 334 | * Allow backslash to escape special characters. 335 | * If you specify this option, you may not use a backslash as the delimiter. 336 | * @see CHCSVParser.recognizesBackslashesAsEscapes 337 | */ 338 | CHCSVParserOptionsRecognizesBackslashesAsEscapes = 1 << 0, 339 | /** 340 | * Cleans the field before reporting it. 341 | * @see CHCSVParser.sanitizesFields 342 | */ 343 | CHCSVParserOptionsSanitizesFields = 1 << 1, 344 | /** 345 | * Fields that begin with a "#" will be reported as comments. 346 | * If you specify this option, you may not use an octothorpe as the delimiter. 347 | * @see CHCSVParser.recognizesComments 348 | */ 349 | CHCSVParserOptionsRecognizesComments = 1 << 2, 350 | /** 351 | * Trims whitespace around a field. 352 | * @see CHCSVParser.trimsWhitespace 353 | */ 354 | CHCSVParserOptionsTrimsWhitespace = 1 << 3, 355 | /** 356 | * When you specify this option, instead of getting an Array of Arrays of Strings, 357 | * you get an Array of @c CHCSVOrderedDictionary instances. 358 | * If the file only contains a single line, then an empty array is returned. 359 | */ 360 | CHCSVParserOptionsUsesFirstLineAsKeys = 1 << 4, 361 | /** 362 | * Some delimited files contain fields that begin with a leading equal sign, 363 | * to indicate that the contents should not be summarized or re-interpreted. 364 | * (For example, to remove insignificant digits) 365 | * If you specify this option, you may not use an equal sign as the delimiter. 366 | * @see CHCSVParser.recognizesLeadingEqualSign 367 | * @link http://edoceo.com/utilitas/csv-file-format 368 | */ 369 | CHCSVParserOptionsRecognizesLeadingEqualSign = 1 << 5 370 | }; 371 | 372 | /** 373 | * An @c NSDictionary subclass that maintains a strong ordering of its key-value pairs 374 | */ 375 | @interface CHCSVOrderedDictionary : NSDictionary 376 | 377 | - (instancetype)initWithObjects:(NSArray *)objects forKeys:(NSArray *)keys NS_DESIGNATED_INITIALIZER; 378 | - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; 379 | 380 | - (id)objectAtIndexedSubscript:(NSUInteger)idx; 381 | - (id)objectAtIndex:(NSUInteger)idx; 382 | 383 | @end 384 | 385 | @interface NSArray (CHCSVAdditions) 386 | 387 | /** 388 | * A convenience constructor to parse a CSV file 389 | * 390 | * @param fileURL The @c NSURL to the CSV file 391 | * 392 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise 393 | */ 394 | + (instancetype)arrayWithContentsOfCSVURL:(NSURL *)fileURL; 395 | 396 | /** 397 | * A convenience constructor to parse a CSV file 398 | * 399 | * @param fileURL The @c NSURL to the CSV file 400 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 401 | * 402 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise 403 | */ 404 | + (instancetype)arrayWithContentsOfCSVURL:(NSURL *)fileURL options:(CHCSVParserOptions)options; 405 | 406 | /** 407 | * A convenience constructor to parse a delimited file 408 | * 409 | * @param fileURL The @c NSURL to the delimited file 410 | * @param delimiter The delimiter used in the file 411 | * 412 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise 413 | */ 414 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL delimiter:(unichar)delimiter; 415 | 416 | /** 417 | * A convenience constructor to parse a delimited file 418 | * 419 | * @param fileURL The @c NSURL to the delimited file 420 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 421 | * @param delimiter The delimiter used in the file 422 | * 423 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise 424 | */ 425 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL options:(CHCSVParserOptions)options delimiter:(unichar)delimiter; 426 | 427 | /** 428 | * A convenience constructor to parse a delimited file 429 | * 430 | * @param fileURL The @c NSURL to the delimited file 431 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 432 | * @param delimiter The delimiter used in the file 433 | * @param error A pointer to an @c NSError*, which will be filled in if parsing fails 434 | * 435 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 436 | */ 437 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL options:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error; 438 | 439 | /** 440 | * If the receiver is an @c NSArray of @c NSArrays of objects, this will turn it into a comma-delimited string 441 | * Returns the string of CSV, if writing succeeds; @c nil otherwise. 442 | */ 443 | - (NSString *)CSVString; 444 | 445 | @end 446 | 447 | @interface NSString (CHCSVAdditions) 448 | 449 | /** 450 | * Parses the receiver as a comma-delimited string 451 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 452 | */ 453 | @property (nonatomic, readonly) NSArray *CSVComponents; 454 | 455 | /** 456 | * Parses the receiver as a comma-delimited string 457 | * 458 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 459 | * 460 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 461 | */ 462 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options; 463 | 464 | /** 465 | * Parses the receiver as a delimited string 466 | * 467 | * @param delimiter The delimiter used in the string 468 | * 469 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 470 | */ 471 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter; 472 | 473 | /** 474 | * Parses the receiver as a delimited string 475 | * 476 | * @param delimiter The delimiter used in the string 477 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 478 | * 479 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 480 | */ 481 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter options:(CHCSVParserOptions)options; 482 | 483 | /** 484 | * Parses the receiver as a delimited string 485 | * 486 | * @param delimiter The delimiter used in the string 487 | * @param options A bitwise-OR of @c CHCSVParserOptions to control how parsing should occur 488 | * @param error A pointer to an @c NSError*, which will be filled in if parsing fails 489 | * 490 | * @return An @c NSArray of @c NSArrays of @c NSStrings, if parsing succeeds; @c nil otherwise. 491 | */ 492 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter options:(CHCSVParserOptions)options error:(NSError *__autoreleasing *)error; 493 | 494 | @end 495 | 496 | #pragma mark - Deprecated stuff 497 | 498 | /** 499 | * These methods have been deprecated, but are preserved here for source compatibility. 500 | * They will be removed in the future. 501 | */ 502 | 503 | @interface CHCSVParser (Deprecated) 504 | 505 | @property (nonatomic, assign) BOOL stripsLeadingAndTrailingWhitespace CHCSV_DEPRECATED(@"use .trimsWhitespace instead"); // default is NO 506 | 507 | - (instancetype)initWithCSVString:(NSString *)csv delimiter:(unichar)delimiter CHCSV_DEPRECATED("use -initWithDelimitedString:delimiter: instead"); 508 | - (instancetype)initWithContentsOfCSVFile:(NSString *)csvFilePath CHCSV_DEPRECATED("use -initWithContentsOfCSVURL: instead"); 509 | - (instancetype)initWithContentsOfCSVFile:(NSString *)csvFilePath delimiter:(unichar)delimiter CHCSV_DEPRECATED("use -initWithContentsOfDelimitedURL:delimiter: instead"); 510 | 511 | @end 512 | 513 | @interface NSArray (CHCSVAdditions_Deprecated) 514 | 515 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath CHCSV_DEPRECATED("Use +arrayWithContentsOfCSVURL: instead"); 516 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath delimiter:(unichar)delimiter CHCSV_DEPRECATED("Use +arrayWithContentsOfDelimitedURL:delimiter: instead"); 517 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options CHCSV_DEPRECATED("Use +arrayWithContentsOfCSVURL:options: instead"); 518 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options delimiter:(unichar)delimiter CHCSV_DEPRECATED("Use +arrayWithContentsOfDelimitedURL:options:delimiter: instead"); 519 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error CHCSV_DEPRECATED("Use +arrayWithContentsOfDelimitedURL:options:delimiter:error: instead"); 520 | 521 | @end 522 | 523 | @interface NSString (CHCSVAdditions_Deprecated) 524 | 525 | - (NSArray *)CSVComponentsWithDelimiter:(unichar)delimiter CHCSV_DEPRECATED("Use -componentsSeparatedByDelimiter: instead"); 526 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options delimiter:(unichar)delimiter CHCSV_DEPRECATED("Use -componentsSeparatedByDelimiter:options: instead"); 527 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error CHCSV_DEPRECATED("Use -componentsSeparatedByDelimiter:options:error: instead"); 528 | 529 | @end 530 | -------------------------------------------------------------------------------- /LocalToos/CHCSVParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // CHCSVParser.m 3 | // CHCSVParser 4 | /** 5 | Copyright (c) 2014 Dave DeLong 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | **/ 25 | 26 | #import "CHCSVParser.h" 27 | 28 | #if !__has_feature(objc_arc) 29 | #error CHCSVParser requires ARC. If the rest of your project is non-ARC, add the "-fobjc-arc" compiler flag for this file. 30 | #endif 31 | 32 | NSString *const CHCSVErrorDomain = @"com.davedelong.csv"; 33 | 34 | #define CHUNK_SIZE 512 35 | #define DOUBLE_QUOTE '"' 36 | #define COMMA ',' 37 | #define OCTOTHORPE '#' 38 | #define EQUAL '=' 39 | #define BACKSLASH '\\' 40 | #define NULLCHAR '\0' 41 | 42 | @interface CHCSVParser () 43 | @property (assign) NSUInteger totalBytesRead; 44 | @end 45 | 46 | @implementation CHCSVParser { 47 | NSInputStream *_stream; 48 | NSStringEncoding _streamEncoding; 49 | NSMutableData *_stringBuffer; 50 | NSMutableString *_string; 51 | NSCharacterSet *_validFieldCharacters; 52 | 53 | NSUInteger _nextIndex; 54 | 55 | NSInteger _fieldIndex; 56 | NSRange _fieldRange; 57 | NSMutableString *_sanitizedField; 58 | 59 | unichar _delimiter; 60 | 61 | NSError *_error; 62 | 63 | NSUInteger _currentRecord; 64 | BOOL _cancelled; 65 | } 66 | 67 | - (id)initWithCSVString:(NSString *)csv { 68 | return [self initWithDelimitedString:csv delimiter:COMMA]; 69 | } 70 | 71 | - (instancetype)initWithDelimitedString:(NSString *)string delimiter:(unichar)delimiter { 72 | NSInputStream *stream = [NSInputStream inputStreamWithData:[string dataUsingEncoding:NSUTF8StringEncoding]]; 73 | return [self initWithInputStream:stream usedEncoding:NULL delimiter:delimiter]; 74 | } 75 | 76 | - (instancetype)initWithContentsOfCSVURL:(NSURL *)csvURL { 77 | return [self initWithContentsOfDelimitedURL:csvURL delimiter:COMMA]; 78 | } 79 | 80 | - (instancetype)initWithContentsOfDelimitedURL:(NSURL *)URL delimiter:(unichar)delimiter { 81 | NSInputStream *stream = [NSInputStream inputStreamWithURL:URL]; 82 | return [self initWithInputStream:stream usedEncoding:NULL delimiter:delimiter]; 83 | } 84 | 85 | - (id)initWithInputStream:(NSInputStream *)stream usedEncoding:(NSStringEncoding *)encoding delimiter:(unichar)delimiter { 86 | NSParameterAssert(stream); 87 | NSParameterAssert(delimiter); 88 | NSAssert([[NSCharacterSet newlineCharacterSet] characterIsMember:delimiter] == NO, @"The field delimiter may not be a newline"); 89 | NSAssert(delimiter != DOUBLE_QUOTE, @"The field delimiter may not be a double quote"); 90 | 91 | self = [super init]; 92 | if (self) { 93 | _stream = stream; 94 | [_stream open]; 95 | 96 | _stringBuffer = [[NSMutableData alloc] init]; 97 | _string = [[NSMutableString alloc] init]; 98 | 99 | _delimiter = delimiter; 100 | 101 | _nextIndex = 0; 102 | _recognizesComments = NO; 103 | _recognizesBackslashesAsEscapes = NO; 104 | _sanitizesFields = NO; 105 | _sanitizedField = [[NSMutableString alloc] init]; 106 | _trimsWhitespace = NO; 107 | _recognizesLeadingEqualSign = NO; 108 | 109 | NSMutableCharacterSet *m = [[NSCharacterSet newlineCharacterSet] mutableCopy]; 110 | NSString *invalid = [NSString stringWithFormat:@"%c%C", DOUBLE_QUOTE, _delimiter]; 111 | [m addCharactersInString:invalid]; 112 | _validFieldCharacters = [m invertedSet]; 113 | 114 | if (encoding == NULL || *encoding == 0) { 115 | // we need to determine the encoding 116 | [self _sniffEncoding]; 117 | if (encoding) { 118 | *encoding = _streamEncoding; 119 | } 120 | } else { 121 | _streamEncoding = *encoding; 122 | } 123 | } 124 | return self; 125 | } 126 | 127 | - (void)dealloc { 128 | [_stream close]; 129 | } 130 | 131 | #pragma mark - 132 | 133 | - (void)setRecognizesBackslashesAsEscapes:(BOOL)recognizesBackslashesAsEscapes { 134 | _recognizesBackslashesAsEscapes = recognizesBackslashesAsEscapes; 135 | if (_delimiter == BACKSLASH && _recognizesBackslashesAsEscapes) { 136 | [NSException raise:NSInternalInconsistencyException format:@"Cannot recognize backslashes as escapes when using '\\' as the delimiter"]; 137 | } 138 | } 139 | 140 | - (void)setRecognizesComments:(BOOL)recognizesComments { 141 | _recognizesComments = recognizesComments; 142 | if (_delimiter == OCTOTHORPE && _recognizesComments) { 143 | [NSException raise:NSInternalInconsistencyException format:@"Cannot recognize comments when using '#' as the delimiter"]; 144 | } 145 | } 146 | 147 | - (void)setRecognizesLeadingEqualSign:(BOOL)recognizesLeadingEqualSign { 148 | _recognizesLeadingEqualSign = recognizesLeadingEqualSign; 149 | if (_delimiter == EQUAL && _recognizesLeadingEqualSign) { 150 | [NSException raise:NSInternalInconsistencyException format:@"Cannot recognize leading equal sign when using '=' as the delimiter"]; 151 | } 152 | } 153 | 154 | #pragma mark - 155 | 156 | - (void)_sniffEncoding { 157 | NSStringEncoding encoding = NSUTF8StringEncoding; 158 | 159 | uint8_t bytes[CHUNK_SIZE]; 160 | NSInteger readLength = [_stream read:bytes maxLength:CHUNK_SIZE]; 161 | if (readLength > 0 && readLength <= CHUNK_SIZE) { 162 | [_stringBuffer appendBytes:bytes length:readLength]; 163 | [self setTotalBytesRead:[self totalBytesRead] + readLength]; 164 | 165 | NSInteger bomLength = 0; 166 | 167 | if (readLength > 3 && bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF) { 168 | encoding = NSUTF32BigEndianStringEncoding; 169 | bomLength = 4; 170 | } else if (readLength > 3 && bytes[0] == 0xFF && bytes[1] == 0xFE && bytes[2] == 0x00 && bytes[3] == 0x00) { 171 | encoding = NSUTF32LittleEndianStringEncoding; 172 | bomLength = 4; 173 | } else if (readLength > 3 && bytes[0] == 0x1B && bytes[1] == 0x24 && bytes[2] == 0x29 && bytes[3] == 0x43) { 174 | encoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISO_2022_KR); 175 | bomLength = 4; 176 | } else if (readLength > 1 && bytes[0] == 0xFE && bytes[1] == 0xFF) { 177 | encoding = NSUTF16BigEndianStringEncoding; 178 | bomLength = 2; 179 | } else if (readLength > 1 && bytes[0] == 0xFF && bytes[1] == 0xFE) { 180 | encoding = NSUTF16LittleEndianStringEncoding; 181 | bomLength = 2; 182 | } else if (readLength > 2 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) { 183 | encoding = NSUTF8StringEncoding; 184 | bomLength = 3; 185 | } else { 186 | NSString *bufferAsUTF8 = nil; 187 | 188 | for (NSInteger triedLength = 0; triedLength < 4; ++triedLength) { 189 | bufferAsUTF8 = [[NSString alloc] initWithBytes:bytes length:readLength-triedLength encoding:NSUTF8StringEncoding]; 190 | if (bufferAsUTF8 != nil) { 191 | break; 192 | } 193 | } 194 | 195 | if (bufferAsUTF8 != nil) { 196 | encoding = NSUTF8StringEncoding; 197 | } else { 198 | NSLog(@"unable to determine stream encoding; assuming MacOSRoman"); 199 | encoding = NSMacOSRomanStringEncoding; 200 | } 201 | } 202 | 203 | if (bomLength > 0) { 204 | [_stringBuffer replaceBytesInRange:NSMakeRange(0, bomLength) withBytes:NULL length:0]; 205 | } 206 | } 207 | _streamEncoding = encoding; 208 | } 209 | 210 | - (void)_loadMoreIfNecessary { 211 | NSUInteger stringLength = [_string length]; 212 | NSUInteger reloadPortion = stringLength / 3; 213 | if (reloadPortion < 10) { reloadPortion = 10; } 214 | 215 | if (_nextIndex+reloadPortion >= stringLength && [_stream hasBytesAvailable]) { 216 | // read more from the stream 217 | uint8_t buffer[CHUNK_SIZE]; 218 | NSInteger readBytes = [_stream read:buffer maxLength:CHUNK_SIZE]; 219 | if (readBytes > 0) { 220 | // append it to the buffer 221 | [_stringBuffer appendBytes:buffer length:readBytes]; 222 | [self setTotalBytesRead:[self totalBytesRead] + readBytes]; 223 | } 224 | } 225 | 226 | if ([_stringBuffer length] > 0) { 227 | // try to turn the next portion of the buffer into a string 228 | NSUInteger readLength = [_stringBuffer length]; 229 | while (readLength > 0) { 230 | NSString *readString = [[NSString alloc] initWithBytes:[_stringBuffer bytes] length:readLength encoding:_streamEncoding]; 231 | if (readString == nil) { 232 | readLength--; 233 | } else { 234 | [_string appendString:readString]; 235 | break; 236 | } 237 | }; 238 | 239 | [_stringBuffer replaceBytesInRange:NSMakeRange(0, readLength) withBytes:NULL length:0]; 240 | } 241 | } 242 | 243 | - (void)_advance { 244 | [self _loadMoreIfNecessary]; 245 | _nextIndex++; 246 | } 247 | 248 | - (unichar)_peekCharacter { 249 | [self _loadMoreIfNecessary]; 250 | if (_nextIndex >= [_string length]) { return NULLCHAR; } 251 | 252 | return [_string characterAtIndex:_nextIndex]; 253 | } 254 | 255 | - (unichar)_peekPeekCharacter { 256 | [self _loadMoreIfNecessary]; 257 | NSUInteger nextNextIndex = _nextIndex+1; 258 | if (nextNextIndex >= [_string length]) { return NULLCHAR; } 259 | 260 | return [_string characterAtIndex:nextNextIndex]; 261 | } 262 | 263 | #pragma mark - 264 | 265 | - (void)parse { 266 | @autoreleasepool { 267 | [self _beginDocument]; 268 | 269 | _currentRecord = 0; 270 | while ([self _parseRecord]) { 271 | ; // yep; 272 | } 273 | 274 | if (_error != nil) { 275 | [self _error]; 276 | } else { 277 | [self _endDocument]; 278 | } 279 | } 280 | } 281 | 282 | - (void)cancelParsing { 283 | _cancelled = YES; 284 | } 285 | 286 | - (BOOL)_parseRecord { 287 | while ([self _peekCharacter] == OCTOTHORPE && _recognizesComments) { 288 | [self _parseComment]; 289 | } 290 | 291 | if ([self _peekCharacter] != NULLCHAR) { 292 | @autoreleasepool { 293 | [self _beginRecord]; 294 | while (1) { 295 | if (![self _parseField]) { 296 | break; 297 | } 298 | if (![self _parseDelimiter]) { 299 | break; 300 | } 301 | } 302 | [self _endRecord]; 303 | } 304 | } 305 | 306 | BOOL followedByNewline = [self _parseNewline]; 307 | return (followedByNewline && _error == nil && [self _peekCharacter] != NULLCHAR); 308 | } 309 | 310 | - (BOOL)_parseNewline { 311 | if (_cancelled) { return NO; } 312 | 313 | NSUInteger charCount = 0; 314 | while ([[NSCharacterSet newlineCharacterSet] characterIsMember:[self _peekCharacter]]) { 315 | charCount++; 316 | [self _advance]; 317 | } 318 | return (charCount > 0); 319 | } 320 | 321 | - (BOOL)_parseComment { 322 | [self _advance]; // consume the octothorpe 323 | 324 | NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; 325 | 326 | [self _beginComment]; 327 | BOOL isBackslashEscaped = NO; 328 | while (1) { 329 | if (isBackslashEscaped == NO) { 330 | unichar next = [self _peekCharacter]; 331 | if (next == BACKSLASH && _recognizesBackslashesAsEscapes) { 332 | isBackslashEscaped = YES; 333 | [self _advance]; 334 | } else if ([newlines characterIsMember:next] == NO && next != NULLCHAR) { 335 | [self _advance]; 336 | } else { 337 | // it's a newline 338 | break; 339 | } 340 | } else { 341 | isBackslashEscaped = YES; 342 | [self _advance]; 343 | } 344 | } 345 | [self _endComment]; 346 | 347 | return [self _parseNewline]; 348 | } 349 | 350 | - (void)_parseFieldWhitespace { 351 | NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; 352 | while ([self _peekCharacter] != NULLCHAR && 353 | [whitespace characterIsMember:[self _peekCharacter]] && 354 | [self _peekCharacter] != _delimiter) { 355 | 356 | if (_trimsWhitespace == NO) { 357 | [_sanitizedField appendFormat:@"%C", [self _peekCharacter]]; 358 | // if we're sanitizing fields, then these characters would be stripped (because they're not appended to _sanitizedField) 359 | } 360 | [self _advance]; 361 | } 362 | } 363 | 364 | - (BOOL)_parseField { 365 | if (_cancelled) { return NO; } 366 | 367 | BOOL parsedField = NO; 368 | [self _beginField]; 369 | 370 | // consume leading whitespace 371 | [self _parseFieldWhitespace]; 372 | 373 | if ([self _peekCharacter] == DOUBLE_QUOTE) { 374 | parsedField = [self _parseEscapedField]; 375 | } else if (_recognizesLeadingEqualSign && [self _peekCharacter] == EQUAL && [self _peekPeekCharacter] == DOUBLE_QUOTE) { 376 | [self _advance]; // consume the equal sign 377 | parsedField = [self _parseEscapedField]; 378 | } else { 379 | parsedField = [self _parseUnescapedField]; 380 | if (_trimsWhitespace) { 381 | NSString *trimmedString = [_sanitizedField stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 382 | [_sanitizedField setString:trimmedString]; 383 | } 384 | } 385 | 386 | if (parsedField) { 387 | // consume trailing whitespace 388 | [self _parseFieldWhitespace]; 389 | [self _endField]; 390 | } 391 | return parsedField; 392 | } 393 | 394 | - (BOOL)_parseEscapedField { 395 | [self _advance]; // consume the opening double quote 396 | 397 | NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; 398 | BOOL isBackslashEscaped = NO; 399 | while (1) { 400 | unichar next = [self _peekCharacter]; 401 | if (next == NULLCHAR) { break; } 402 | 403 | if (isBackslashEscaped == NO) { 404 | if (next == BACKSLASH && _recognizesBackslashesAsEscapes) { 405 | isBackslashEscaped = YES; 406 | [self _advance]; // consume the backslash 407 | } else if ([_validFieldCharacters characterIsMember:next] || 408 | [newlines characterIsMember:next] || 409 | next == _delimiter) { 410 | [_sanitizedField appendFormat:@"%C", next]; 411 | [self _advance]; 412 | } else if (next == DOUBLE_QUOTE && [self _peekPeekCharacter] == DOUBLE_QUOTE) { 413 | [_sanitizedField appendFormat:@"%C", next]; 414 | [self _advance]; 415 | [self _advance]; 416 | } else { 417 | // not valid, or it's not a doubled double quote 418 | break; 419 | } 420 | } else { 421 | [_sanitizedField appendFormat:@"%C", next]; 422 | isBackslashEscaped = NO; 423 | [self _advance]; 424 | } 425 | } 426 | 427 | if ([self _peekCharacter] == DOUBLE_QUOTE) { 428 | [self _advance]; 429 | return YES; 430 | } 431 | 432 | return NO; 433 | } 434 | 435 | - (BOOL)_parseUnescapedField { 436 | 437 | NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet]; 438 | BOOL isBackslashEscaped = NO; 439 | while (1) { 440 | unichar next = [self _peekCharacter]; 441 | if (next == NULLCHAR) { break; } 442 | 443 | if (isBackslashEscaped == NO) { 444 | if (next == BACKSLASH && _recognizesBackslashesAsEscapes) { 445 | isBackslashEscaped = YES; 446 | [self _advance]; 447 | } else if ([newlines characterIsMember:next] == YES || next == _delimiter) { 448 | break; 449 | } else { 450 | [_sanitizedField appendFormat:@"%C", next]; 451 | [self _advance]; 452 | } 453 | } else { 454 | isBackslashEscaped = NO; 455 | [_sanitizedField appendFormat:@"%C", next]; 456 | [self _advance]; 457 | } 458 | } 459 | 460 | return YES; 461 | } 462 | 463 | - (BOOL)_parseDelimiter { 464 | unichar next = [self _peekCharacter]; 465 | if (next == _delimiter) { 466 | [self _advance]; 467 | return YES; 468 | } 469 | if (next != NULLCHAR && [[NSCharacterSet newlineCharacterSet] characterIsMember:next] == NO) { 470 | NSString *description = [NSString stringWithFormat:@"Unexpected delimiter. Expected '%C' (0x%X), but got '%C' (0x%X)", _delimiter, _delimiter, [self _peekCharacter], [self _peekCharacter]]; 471 | _error = [[NSError alloc] initWithDomain:CHCSVErrorDomain code:CHCSVErrorCodeInvalidFormat userInfo:@{NSLocalizedDescriptionKey : description}]; 472 | } 473 | return NO; 474 | } 475 | 476 | - (void)_beginDocument { 477 | if ([_delegate respondsToSelector:@selector(parserDidBeginDocument:)]) { 478 | [_delegate parserDidBeginDocument:self]; 479 | } 480 | } 481 | 482 | - (void)_endDocument { 483 | if ([_delegate respondsToSelector:@selector(parserDidEndDocument:)]) { 484 | [_delegate parserDidEndDocument:self]; 485 | } 486 | } 487 | 488 | - (void)_beginRecord { 489 | if (_cancelled) { return; } 490 | 491 | _fieldIndex = 0; 492 | _currentRecord++; 493 | if ([_delegate respondsToSelector:@selector(parser:didBeginLine:)]) { 494 | [_delegate parser:self didBeginLine:_currentRecord]; 495 | } 496 | } 497 | 498 | - (void)_endRecord { 499 | if (_cancelled) { return; } 500 | 501 | if ([_delegate respondsToSelector:@selector(parser:didEndLine:)]) { 502 | [_delegate parser:self didEndLine:_currentRecord]; 503 | } 504 | } 505 | 506 | - (void)_beginField { 507 | if (_cancelled) { return; } 508 | 509 | [_sanitizedField setString:@""]; 510 | _fieldRange.location = _nextIndex; 511 | } 512 | 513 | - (void)_endField { 514 | if (_cancelled) { return; } 515 | 516 | _fieldRange.length = (_nextIndex - _fieldRange.location); 517 | NSString *field = nil; 518 | 519 | if (_sanitizesFields) { 520 | field = [_sanitizedField copy]; 521 | } else { 522 | field = [_string substringWithRange:_fieldRange]; 523 | if (_trimsWhitespace) { 524 | field = [field stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 525 | } 526 | } 527 | 528 | if ([_delegate respondsToSelector:@selector(parser:didReadField:atIndex:)]) { 529 | [_delegate parser:self didReadField:field atIndex:_fieldIndex]; 530 | } 531 | 532 | [_string replaceCharactersInRange:NSMakeRange(0, NSMaxRange(_fieldRange)) withString:@""]; 533 | _nextIndex = 0; 534 | _fieldIndex++; 535 | } 536 | 537 | - (void)_beginComment { 538 | if (_cancelled) { return; } 539 | 540 | _fieldRange.location = _nextIndex; 541 | } 542 | 543 | - (void)_endComment { 544 | if (_cancelled) { return; } 545 | 546 | _fieldRange.length = (_nextIndex - _fieldRange.location); 547 | if ([_delegate respondsToSelector:@selector(parser:didReadComment:)]) { 548 | NSString *comment = [_string substringWithRange:_fieldRange]; 549 | [_delegate parser:self didReadComment:comment]; 550 | } 551 | 552 | [_string replaceCharactersInRange:NSMakeRange(0, NSMaxRange(_fieldRange)) withString:@""]; 553 | _nextIndex = 0; 554 | } 555 | 556 | - (void)_error { 557 | if (_cancelled) { return; } 558 | 559 | if ([_delegate respondsToSelector:@selector(parser:didFailWithError:)]) { 560 | [_delegate parser:self didFailWithError:_error]; 561 | } 562 | } 563 | 564 | @end 565 | 566 | @implementation CHCSVWriter { 567 | NSOutputStream *_stream; 568 | NSStringEncoding _streamEncoding; 569 | 570 | NSData *_delimiter; 571 | NSData *_bom; 572 | NSCharacterSet *_illegalCharacters; 573 | 574 | NSUInteger _currentLine; 575 | NSUInteger _currentField; 576 | NSMutableArray *_firstLineKeys; 577 | } 578 | 579 | - (instancetype)initForWritingToCSVFile:(NSString *)path { 580 | NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:path append:NO]; 581 | return [self initWithOutputStream:stream encoding:NSUTF8StringEncoding delimiter:COMMA]; 582 | } 583 | 584 | - (instancetype)initWithOutputStream:(NSOutputStream *)stream encoding:(NSStringEncoding)encoding delimiter:(unichar)delimiter { 585 | self = [super init]; 586 | if (self) { 587 | _stream = stream; 588 | _streamEncoding = encoding; 589 | 590 | if ([_stream streamStatus] == NSStreamStatusNotOpen) { 591 | [_stream open]; 592 | } 593 | 594 | NSData *a = [@"a" dataUsingEncoding:_streamEncoding]; 595 | NSData *aa = [@"aa" dataUsingEncoding:_streamEncoding]; 596 | if ([a length] * 2 != [aa length]) { 597 | NSUInteger characterLength = [aa length] - [a length]; 598 | _bom = [a subdataWithRange:NSMakeRange(0, [a length] - characterLength)]; 599 | [self _writeData:_bom]; 600 | } 601 | 602 | NSString *delimiterString = [NSString stringWithFormat:@"%C", delimiter]; 603 | NSData *delimiterData = [delimiterString dataUsingEncoding:_streamEncoding]; 604 | if ([_bom length] > 0) { 605 | _delimiter = [delimiterData subdataWithRange:NSMakeRange([_bom length], [delimiterData length] - [_bom length])]; 606 | } else { 607 | _delimiter = delimiterData; 608 | } 609 | 610 | NSMutableCharacterSet *illegalCharacters = [[NSCharacterSet newlineCharacterSet] mutableCopy]; 611 | [illegalCharacters addCharactersInString:delimiterString]; 612 | [illegalCharacters addCharactersInString:@"\""]; 613 | _illegalCharacters = [illegalCharacters copy]; 614 | 615 | _firstLineKeys = [NSMutableArray array]; 616 | } 617 | return self; 618 | } 619 | 620 | - (void)dealloc { 621 | [self closeStream]; 622 | } 623 | 624 | - (void)_writeData:(NSData *)data { 625 | if ([data length] > 0) { 626 | const void *bytes = [data bytes]; 627 | [_stream write:bytes maxLength:[data length]]; 628 | } 629 | } 630 | 631 | - (void)_writeString:(NSString *)string { 632 | NSData *stringData = [string dataUsingEncoding:_streamEncoding]; 633 | if ([_bom length] > 0) { 634 | stringData = [stringData subdataWithRange:NSMakeRange([_bom length], [stringData length] - [_bom length])]; 635 | } 636 | [self _writeData:stringData]; 637 | } 638 | 639 | - (void)_writeDelimiter { 640 | [self _writeData:_delimiter]; 641 | } 642 | 643 | - (void)writeField:(id)field { 644 | if (_currentField > 0) { 645 | [self _writeDelimiter]; 646 | } 647 | 648 | if (_currentLine == 0) { 649 | [_firstLineKeys addObject:field]; 650 | } 651 | 652 | NSString *string = field ? [field description] : @""; 653 | 654 | if ([string rangeOfCharacterFromSet:_illegalCharacters].location != NSNotFound) { 655 | // replace double quotes with double double quotes 656 | string = [string stringByReplacingOccurrencesOfString:@"\"" withString:@"\"\""]; 657 | // surround in double quotes 658 | string = [NSString stringWithFormat:@"\"%@\"", string]; 659 | } 660 | [self _writeString:string]; 661 | _currentField++; 662 | } 663 | 664 | - (void)finishLine { 665 | [self _writeString:@"\n"]; 666 | _currentField = 0; 667 | _currentLine++; 668 | } 669 | 670 | - (void)_finishLineIfNecessary { 671 | if (_currentField != 0) { 672 | [self finishLine]; 673 | } 674 | } 675 | 676 | - (void)writeLineOfFields:(id)fields { 677 | [self _finishLineIfNecessary]; 678 | 679 | for (id field in fields) { 680 | [self writeField:field]; 681 | } 682 | [self finishLine]; 683 | } 684 | 685 | - (void)writeLineWithDictionary:(NSDictionary *)dictionary { 686 | if (_currentLine == 0) { 687 | [NSException raise:NSInternalInconsistencyException format:@"Cannot write a dictionary unless a line of keys has already been given"]; 688 | } 689 | 690 | [self _finishLineIfNecessary]; 691 | 692 | for (id key in _firstLineKeys) { 693 | id value = [dictionary objectForKey:key]; 694 | [self writeField:value]; 695 | } 696 | [self finishLine]; 697 | } 698 | 699 | - (void)writeComment:(NSString *)comment { 700 | [self _finishLineIfNecessary]; 701 | 702 | NSArray *lines = [comment componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; 703 | for (NSString *line in lines) { 704 | NSString *commented = [NSString stringWithFormat:@"#%@\n", line]; 705 | [self _writeString:commented]; 706 | } 707 | } 708 | 709 | - (void)closeStream { 710 | [_stream close]; 711 | _stream = nil; 712 | } 713 | 714 | @end 715 | 716 | #pragma mark - Convenience Categories 717 | 718 | @interface _CHCSVAggregator : NSObject 719 | 720 | @property (strong) NSMutableArray *lines; 721 | @property (strong) NSError *error; 722 | 723 | @property (strong) NSMutableArray *currentLine; 724 | 725 | @end 726 | 727 | @implementation _CHCSVAggregator 728 | 729 | - (void)parserDidBeginDocument:(CHCSVParser *)parser { 730 | self.lines = [[NSMutableArray alloc] init]; 731 | } 732 | 733 | - (void)parser:(CHCSVParser *)parser didBeginLine:(NSUInteger)recordNumber { 734 | self.currentLine = [[NSMutableArray alloc] init]; 735 | } 736 | 737 | - (void)parser:(CHCSVParser *)parser didEndLine:(NSUInteger)recordNumber { 738 | [self.lines addObject:self.currentLine]; 739 | self.currentLine = nil; 740 | } 741 | 742 | - (void)parser:(CHCSVParser *)parser didReadField:(NSString *)field atIndex:(NSInteger)fieldIndex { 743 | [self.currentLine addObject:field]; 744 | } 745 | 746 | - (void)parser:(CHCSVParser *)parser didFailWithError:(NSError *)error { 747 | self.error = error; 748 | self.lines = nil; 749 | } 750 | 751 | @end 752 | 753 | @interface _CHCSVKeyedAggregator : _CHCSVAggregator 754 | 755 | @property (strong) NSArray *firstLine; 756 | 757 | @end 758 | 759 | @implementation _CHCSVKeyedAggregator 760 | 761 | - (void)parser:(CHCSVParser *)parser didEndLine:(NSUInteger)recordNumber { 762 | if (self.firstLine == nil) { 763 | self.firstLine = self.currentLine; 764 | } else if (self.currentLine.count == self.firstLine.count) { 765 | CHCSVOrderedDictionary *line = [[CHCSVOrderedDictionary alloc] initWithObjects:self.currentLine 766 | forKeys:self.firstLine]; 767 | [self.lines addObject:line]; 768 | } else { 769 | [parser cancelParsing]; 770 | self.error = [NSError errorWithDomain:CHCSVErrorDomain code:CHCSVErrorCodeIncorrectNumberOfFields userInfo:nil]; 771 | } 772 | self.currentLine = nil; 773 | } 774 | 775 | @end 776 | 777 | NSArray *_CHCSVParserParse(NSInputStream *inputStream, CHCSVParserOptions options, unichar delimiter, NSError *__autoreleasing *error); 778 | NSArray *_CHCSVParserParse(NSInputStream *inputStream, CHCSVParserOptions options, unichar delimiter, NSError *__autoreleasing *error) { 779 | CHCSVParser *parser = [[CHCSVParser alloc] initWithInputStream:inputStream usedEncoding:nil delimiter:delimiter]; 780 | 781 | BOOL usesFirstLineAsKeys = !!(options & CHCSVParserOptionsUsesFirstLineAsKeys); 782 | _CHCSVAggregator *aggregator = usesFirstLineAsKeys ? [[_CHCSVKeyedAggregator alloc] init] : [[_CHCSVAggregator alloc] init]; 783 | parser.delegate = aggregator; 784 | 785 | parser.recognizesBackslashesAsEscapes = !!(options & CHCSVParserOptionsRecognizesBackslashesAsEscapes); 786 | parser.sanitizesFields = !!(options & CHCSVParserOptionsSanitizesFields); 787 | parser.recognizesComments = !!(options & CHCSVParserOptionsRecognizesComments); 788 | parser.trimsWhitespace = !!(options & CHCSVParserOptionsTrimsWhitespace); 789 | parser.recognizesLeadingEqualSign = !!(options & CHCSVParserOptionsRecognizesLeadingEqualSign); 790 | 791 | [parser parse]; 792 | 793 | if (aggregator.error != nil) { 794 | if (error) { 795 | *error = aggregator.error; 796 | } 797 | return nil; 798 | } else { 799 | return aggregator.lines; 800 | } 801 | } 802 | 803 | @implementation NSArray (CHCSVAdditions) 804 | 805 | + (instancetype)arrayWithContentsOfCSVURL:(NSURL *)fileURL { 806 | return [self arrayWithContentsOfDelimitedURL:fileURL options:0 delimiter:COMMA error:nil]; 807 | } 808 | 809 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL delimiter:(unichar)delimiter { 810 | return [self arrayWithContentsOfDelimitedURL:fileURL options:0 delimiter:delimiter error:nil]; 811 | } 812 | 813 | + (instancetype)arrayWithContentsOfCSVURL:(NSURL *)fileURL options:(CHCSVParserOptions)options { 814 | return [self arrayWithContentsOfDelimitedURL:fileURL options:options delimiter:COMMA error:nil]; 815 | } 816 | 817 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL options:(CHCSVParserOptions)options delimiter:(unichar)delimiter { 818 | return [self arrayWithContentsOfDelimitedURL:fileURL options:options delimiter:delimiter error:nil]; 819 | } 820 | 821 | + (instancetype)arrayWithContentsOfDelimitedURL:(NSURL *)fileURL options:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error { 822 | NSParameterAssert(fileURL); 823 | NSInputStream *stream = [NSInputStream inputStreamWithURL:fileURL]; 824 | 825 | return _CHCSVParserParse(stream, options, delimiter, error); 826 | } 827 | 828 | - (NSString *)CSVString { 829 | NSOutputStream *output = [NSOutputStream outputStreamToMemory]; 830 | CHCSVWriter *writer = [[CHCSVWriter alloc] initWithOutputStream:output encoding:NSUTF8StringEncoding delimiter:COMMA]; 831 | for (id object in self) { 832 | if ([object conformsToProtocol:@protocol(NSFastEnumeration)]) { 833 | [writer writeLineOfFields:object]; 834 | } 835 | } 836 | [writer closeStream]; 837 | 838 | NSData *buffer = [output propertyForKey:NSStreamDataWrittenToMemoryStreamKey]; 839 | return [[NSString alloc] initWithData:buffer encoding:NSUTF8StringEncoding]; 840 | } 841 | 842 | @end 843 | 844 | @implementation NSString (CHCSVAdditions) 845 | 846 | - (NSArray *)CSVComponents { 847 | return [self componentsSeparatedByDelimiter:COMMA options:0 error:nil]; 848 | } 849 | 850 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options { 851 | return [self componentsSeparatedByDelimiter:COMMA options:options error:nil]; 852 | } 853 | 854 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter { 855 | return [self componentsSeparatedByDelimiter:delimiter options:0 error:nil]; 856 | } 857 | 858 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter options:(CHCSVParserOptions)options { 859 | return [self componentsSeparatedByDelimiter:delimiter options:options error:nil]; 860 | } 861 | 862 | - (NSArray *)componentsSeparatedByDelimiter:(unichar)delimiter options:(CHCSVParserOptions)options error:(NSError *__autoreleasing *)error { 863 | NSData *csvData = [self dataUsingEncoding:NSUTF8StringEncoding]; 864 | NSInputStream *stream = [NSInputStream inputStreamWithData:csvData]; 865 | 866 | return _CHCSVParserParse(stream, options, delimiter, error); 867 | } 868 | 869 | @end 870 | 871 | @implementation CHCSVOrderedDictionary { 872 | NSArray *_keys; 873 | NSArray *_values; 874 | NSDictionary *_dictionary; 875 | } 876 | 877 | - (instancetype)initWithObjects:(NSArray *)objects forKeys:(NSArray *)keys { 878 | self = [super init]; 879 | if (self) { 880 | _keys = keys.copy; 881 | _values = objects.copy; 882 | _dictionary = [NSDictionary dictionaryWithObjects:_values forKeys:_keys]; 883 | } 884 | return self; 885 | } 886 | 887 | - (instancetype)initWithObjects:(const id [])objects forKeys:(const id [])keys count:(NSUInteger)cnt { 888 | return [self initWithObjects:[NSArray arrayWithObjects:objects count:cnt] 889 | forKeys:[NSArray arrayWithObjects:keys count:cnt]]; 890 | } 891 | 892 | - (instancetype)init { 893 | return [self initWithObjects:@[] forKeys:@[]]; 894 | } 895 | 896 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 897 | return [super initWithCoder:aDecoder]; 898 | } 899 | 900 | - (NSArray *)allKeys { 901 | return _keys; 902 | } 903 | 904 | - (NSArray *)allValues { 905 | return _values; 906 | } 907 | 908 | - (NSUInteger)count { 909 | return _dictionary.count; 910 | } 911 | 912 | - (id)objectForKey:(id)aKey { 913 | return [_dictionary objectForKey:aKey]; 914 | } 915 | 916 | - (NSEnumerator *)keyEnumerator { 917 | return _keys.objectEnumerator; 918 | } 919 | 920 | - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id [])buffer count:(NSUInteger)len { 921 | return [_keys countByEnumeratingWithState:state objects:buffer count:len]; 922 | } 923 | 924 | - (id)objectAtIndex:(NSUInteger)idx { 925 | id key = [_keys objectAtIndex:idx]; 926 | return [self objectForKey:key]; 927 | } 928 | 929 | - (id)objectAtIndexedSubscript:(NSUInteger)idx { 930 | return [self objectAtIndex:idx]; 931 | } 932 | 933 | - (NSUInteger)hash { 934 | return _dictionary.hash; 935 | } 936 | 937 | - (BOOL)isEqual:(CHCSVOrderedDictionary *)object { 938 | if ([super isEqual:object] && [object isKindOfClass:[CHCSVOrderedDictionary class]]) { 939 | // we've determined that from a dictionary POV, they're equal 940 | // now we need to test for key ordering 941 | return [object->_keys isEqual:_keys]; 942 | } 943 | 944 | return NO; 945 | } 946 | 947 | @end 948 | 949 | #pragma mark - Deprecated methods 950 | 951 | @implementation CHCSVParser (Deprecated) 952 | 953 | - (id)initWithCSVString:(NSString *)csv delimiter:(unichar)delimiter { 954 | return [self initWithDelimitedString:csv delimiter:delimiter]; 955 | } 956 | 957 | - (id)initWithContentsOfCSVFile:(NSString *)csvFilePath { 958 | return [self initWithContentsOfCSVURL:[NSURL fileURLWithPath:csvFilePath]]; 959 | } 960 | 961 | - (id)initWithContentsOfCSVFile:(NSString *)csvFilePath delimiter:(unichar)delimiter { 962 | return [self initWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] delimiter:delimiter]; 963 | } 964 | 965 | - (void)setStripsLeadingAndTrailingWhitespace:(BOOL)stripsLeadingAndTrailingWhitespace { 966 | self.trimsWhitespace = stripsLeadingAndTrailingWhitespace; 967 | } 968 | 969 | - (BOOL)stripsLeadingAndTrailingWhitespace { 970 | return self.trimsWhitespace; 971 | } 972 | 973 | @end 974 | 975 | @implementation NSArray (CHCSVAdditions_Deprecated) 976 | 977 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath { 978 | return [self arrayWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] options:0 delimiter:COMMA error:nil]; 979 | } 980 | 981 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath delimiter:(unichar)delimiter { 982 | return [self arrayWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] options:0 delimiter:delimiter error:nil]; 983 | } 984 | 985 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options { 986 | return [self arrayWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] options:options delimiter:COMMA error:nil]; 987 | } 988 | 989 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options delimiter:(unichar)delimiter { 990 | return [self arrayWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] options:options delimiter:delimiter error:nil]; 991 | } 992 | 993 | + (instancetype)arrayWithContentsOfCSVFile:(NSString *)csvFilePath options:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error { 994 | return [self arrayWithContentsOfDelimitedURL:[NSURL fileURLWithPath:csvFilePath] options:options delimiter:delimiter error:error]; 995 | } 996 | 997 | @end 998 | 999 | @implementation NSString (CHCSVAdditions_Deprecated) 1000 | 1001 | - (NSArray *)CSVComponentsWithDelimiter:(unichar)delimiter { 1002 | return [self componentsSeparatedByDelimiter:delimiter options:0 error:nil]; 1003 | } 1004 | 1005 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options delimiter:(unichar)delimiter { 1006 | return [self componentsSeparatedByDelimiter:delimiter options:options error:nil]; 1007 | } 1008 | 1009 | - (NSArray *)CSVComponentsWithOptions:(CHCSVParserOptions)options delimiter:(unichar)delimiter error:(NSError *__autoreleasing *)error { 1010 | return [self componentsSeparatedByDelimiter:delimiter options:options error:error]; 1011 | } 1012 | 1013 | @end 1014 | -------------------------------------------------------------------------------- /LocalToos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZInfoViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TCZInfoViewController.h 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TCZInfoViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZInfoViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TCZInfoViewController.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import "TCZInfoViewController.h" 10 | 11 | @interface TCZInfoViewController () 12 | 13 | @end 14 | 15 | @implementation TCZInfoViewController 16 | 17 | - (void)viewDidLoad { 18 | [super viewDidLoad]; 19 | self.view.backgroundColor = [UIColor whiteColor]; 20 | self.title = @"使用说明"; 21 | 22 | UILabel *infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 20, CGRectGetWidth(self.view.frame) - 20, CGRectGetHeight(self.view.frame) - 40)]; 23 | infoLabel.textColor = [UIColor blackColor]; 24 | infoLabel.text = @"1. source.strings 是已国际化好的文件,比如当前您只有汉语国际化文件,那么就使用汉语国际化文件作为 source.strings,名字必须为 source.strings;\n\n2. 将要解析的文件(所有国际化后的文件),必须为 csv 文件,一般 World,Numbers 都支持把 excel 文件导出为 csv 文件;命名为:languages.csv \n\n3. 完成解析后,可以导出 ipa 包或者直接从沙盒中读取文件,找到对应的文件,直接复制到自己项目中即可。\n4. 解析失败的会标记为:❌❌"; 25 | infoLabel.font = [UIFont systemFontOfSize:18]; 26 | infoLabel.numberOfLines = 0; 27 | [self.view addSubview:infoLabel]; 28 | } 29 | 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZLocalizableTools.h: -------------------------------------------------------------------------------- 1 | // 2 | // TCZLocalizableTools.h 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class TCZLocalizableTools; 12 | 13 | @protocol TCZLocalizableToolsDelegate 14 | 15 | - (void)localizableToolsEndParse:(TCZLocalizableTools *)tool; 16 | - (void)localizableToolsEndWrite:(TCZLocalizableTools *)tool; 17 | - (void)localizableToolsError:(TCZLocalizableTools *)tool error:(NSError *)error; 18 | 19 | @end 20 | 21 | @interface TCZLocalizableTools : NSObject 22 | 23 | @property (nonatomic, assign) id delegate; 24 | 25 | 26 | // 解析完成后,文件被存放的路径 27 | @property (nonatomic, strong, readonly) NSArray *languagePaths; 28 | 29 | 30 | /** 31 | 初始化 32 | 33 | @param filePath 各国语言文件路径(必须为 csv 文件) 34 | @param lanCount 语言数 35 | @return TCZLocalizableTools 36 | */ 37 | - (instancetype)initWithSourceFilePath:(NSString *)filePath languageCount:(NSUInteger)lanCount; 38 | 39 | /** 40 | 初始化 41 | 42 | @param fileName 各国语言文件名(必须为 csv 文件) 43 | @param lanCount 语言数 44 | @return TCZLocalizableTools 45 | */ 46 | - (instancetype)initWithSourceFileName:(NSString *)fileName languageCount:(NSUInteger)lanCount; 47 | 48 | 49 | /** 50 | 解析调用这个方法 51 | */ 52 | - (void)beginParse; 53 | 54 | 55 | /** 56 | 解析完成后国际化文件被保存的目录 57 | 58 | @return 根目录 59 | */ 60 | + (NSString *)saveLocalizableRootFilePath; 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZLocalizableTools.m: -------------------------------------------------------------------------------- 1 | // 2 | // TCZLocalizableTools.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import "TCZLocalizableTools.h" 10 | #import "CHCSVParser.h" 11 | 12 | @interface TCZLocalizableTools () 13 | 14 | @property (nonatomic, strong) NSArray *paeseResults; 15 | @property (nonatomic, strong) CHCSVParser *csvParser; 16 | @property (nonatomic, assign) NSUInteger lanCount; 17 | @property (nonatomic, strong) NSMutableArray *keys; 18 | @property (nonatomic, strong) NSMutableDictionary *mapDict; 19 | 20 | @property (nonatomic, strong, readwrite) NSArray *languagePaths; 21 | 22 | @end 23 | 24 | @implementation TCZLocalizableTools 25 | 26 | #pragma mark - Init 27 | - (instancetype)initWithSourceFilePath:(NSString *)filePath languageCount:(NSUInteger)lanCount 28 | { 29 | self = [super init]; 30 | if (self) { 31 | _lanCount = lanCount; 32 | 33 | // 解析 key 34 | _mapDict = [NSMutableDictionary dictionary]; 35 | NSDictionary *tempMapDict = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"source" ofType:@"strings"]]; 36 | [tempMapDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 37 | [_mapDict setObject:key forKey:obj]; 38 | }]; 39 | 40 | CHCSVParser *parse = [[CHCSVParser alloc] initWithContentsOfCSVURL:[NSURL fileURLWithPath:filePath]]; 41 | parse.delegate = self; 42 | _csvParser = parse; 43 | 44 | } 45 | return self; 46 | } 47 | 48 | - (instancetype)initWithSourceFileName:(NSString *)fileName languageCount:(NSUInteger)lanCount 49 | { 50 | NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:(fileName.pathExtension.length > 0) ? nil : @"csv"]; 51 | return [self initWithSourceFilePath:filePath languageCount:lanCount]; 52 | } 53 | 54 | - (void)setUpdata 55 | { 56 | _keys = [NSMutableArray array]; 57 | 58 | NSMutableArray *temp = [NSMutableArray array]; 59 | for (NSUInteger i = 0; i < _lanCount; i++) { 60 | [temp addObject:[NSMutableArray array]]; 61 | } 62 | _paeseResults = [temp copy]; 63 | } 64 | 65 | #pragma Puablic 66 | - (void)beginParse 67 | { 68 | [self setUpdata]; 69 | [_csvParser parse]; 70 | } 71 | 72 | #pragma mark - CHCSVParserDelegate 73 | - (void)parserDidBeginDocument:(CHCSVParser *)parser 74 | { 75 | NSLog(@"ParserDidBeginDocument"); 76 | } 77 | 78 | - (void)parser:(CHCSVParser *)parser didFailWithError:(NSError *)error 79 | { 80 | if (self.delegate && [self.delegate respondsToSelector:@selector(localizableToolsError:error:)]) { 81 | [self.delegate localizableToolsError:self error:error]; 82 | } 83 | NSLog(@"Parser error: %@", error); 84 | } 85 | 86 | - (void)parserDidEndDocument:(CHCSVParser *)parser 87 | { 88 | NSLog(@"ParserDidEndDocument"); 89 | 90 | if (self.delegate && [self.delegate respondsToSelector:@selector(localizableToolsEndParse:)]) { 91 | [self.delegate localizableToolsEndParse:self]; 92 | } 93 | 94 | @autoreleasepool { 95 | NSMutableArray *languagePaths = [NSMutableArray array]; 96 | 97 | NSString *rootPath = [TCZLocalizableTools saveLocalizableRootFilePath]; 98 | if ([[NSFileManager defaultManager] fileExistsAtPath:rootPath isDirectory:nil]) { 99 | [[NSFileManager defaultManager] removeItemAtPath:rootPath error:nil]; 100 | } 101 | [[NSFileManager defaultManager] createDirectoryAtPath:rootPath withIntermediateDirectories:YES attributes:nil error:nil]; 102 | 103 | NSLog(@"CSV 文件被保存到:%@", rootPath); 104 | 105 | for (NSUInteger i = 0; i < _lanCount; i++) { 106 | 107 | NSArray *aLanguages = _paeseResults[i]; 108 | NSMutableArray *temps = [NSMutableArray array]; 109 | NSUInteger aLanCount = aLanguages.count; 110 | for (NSUInteger i = 0, max = _keys.count; i < max; i++) { 111 | if (i < aLanCount) { 112 | 113 | // 避免文本中还有逗号 114 | NSString *aLanguage = [self removeInvalidStr:aLanguages[i]]; 115 | [temps addObject:[NSString stringWithFormat:@"\"%@\"=\"%@\";",_keys[i], aLanguage]]; 116 | } else { 117 | [temps addObject:[NSString stringWithFormat:@"\"%@\"=\"%@\";",_keys[i], @""]]; 118 | } 119 | } 120 | 121 | NSString *csvFile = [rootPath stringByAppendingPathComponent:[NSString stringWithFormat:@"language_%@.csv", @(i)]]; 122 | [[NSFileManager defaultManager] createFileAtPath:csvFile contents:nil attributes:nil]; 123 | [languagePaths addObject:csvFile]; 124 | 125 | CHCSVWriter *writer = [[CHCSVWriter alloc] initForWritingToCSVFile:csvFile]; 126 | for (NSUInteger i = 0, max = temps.count; i < max; i++) { 127 | [writer writeField:temps[i]]; 128 | [writer finishLine]; 129 | } 130 | } 131 | 132 | _languagePaths = [languagePaths copy]; 133 | 134 | if (self.delegate && [self.delegate respondsToSelector:@selector(localizableToolsEndWrite:)]) { 135 | [self.delegate localizableToolsEndWrite:self]; 136 | } 137 | } 138 | } 139 | 140 | - (void)parser:(CHCSVParser *)parser didReadField:(NSString *)field atIndex:(NSInteger)fieldIndex 141 | { 142 | field = field.length == 0 ? @"❌❌" : field; 143 | 144 | if (fieldIndex == 0) { 145 | NSString *key = [_mapDict objectForKey:[self removeInvalidStr:field]]; 146 | [_keys addObject:key ?: @"❌❌"]; 147 | } 148 | 149 | if (fieldIndex < _paeseResults.count) { 150 | [_paeseResults[fieldIndex] addObject:field]; 151 | } 152 | } 153 | 154 | #pragma mark - Helper 155 | - (NSString *)removeInvalidStr:(NSString *)sourceStr 156 | { 157 | NSMutableString *aLanguage = [[NSMutableString alloc] initWithString:sourceStr]; 158 | if ([aLanguage containsString:@","] && [aLanguage hasPrefix:@"\""] && [aLanguage hasSuffix:@"\""]) { 159 | [aLanguage replaceCharactersInRange:NSMakeRange(0, 1) withString:@""]; 160 | [aLanguage deleteCharactersInRange:NSMakeRange(aLanguage.length-1, 1)]; 161 | } 162 | return [aLanguage copy]; 163 | } 164 | 165 | + (NSString *)saveLocalizableRootFilePath 166 | { 167 | NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"language"]; 168 | return rootPath; 169 | } 170 | 171 | 172 | @end 173 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZMainViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TCZMainViewController.h 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface TCZMainViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZMainViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TCZMainViewController.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import "TCZMainViewController.h" 10 | #import "CHCSVParser.h" 11 | #import "TCZLocalizableTools.h" 12 | #import "TCZPriviewViewController.h" 13 | #import "TCZInfoViewController.h" 14 | 15 | static const NSUInteger kLanCount = 8; 16 | static NSString* const kCellId = @"ID"; 17 | static NSString* const kAllLanguageName = @"languages.csv"; 18 | 19 | 20 | @interface TCZMainViewController () 21 | 22 | @property (nonatomic, strong) TCZLocalizableTools *localizableTool; 23 | @property (nonatomic, strong) UITableView *tableView; 24 | @property (nonatomic, strong) UIActivityIndicatorView *indicatorView; 25 | @property (nonatomic, strong) UILabel *headerLabel; 26 | 27 | @end 28 | 29 | 30 | @implementation TCZMainViewController 31 | 32 | - (void)viewDidLoad { 33 | [super viewDidLoad]; 34 | 35 | self.title = @"TCZLocalizableTools"; 36 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"开始解析" style:UIBarButtonItemStylePlain target:self action:@selector(beginParseAction:)]; 37 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"❓" style:UIBarButtonItemStylePlain target:self action:@selector(infoAction)]; 38 | 39 | [self createUI]; 40 | } 41 | 42 | 43 | - (void)beginParseAction:(id)sender { 44 | self.title = @"正在解析..."; 45 | _localizableTool = [[TCZLocalizableTools alloc] initWithSourceFileName:kAllLanguageName languageCount:kLanCount]; 46 | _localizableTool.delegate = self; 47 | [_localizableTool beginParse]; 48 | } 49 | 50 | - (void)infoAction 51 | { 52 | TCZInfoViewController *infoVC = [TCZInfoViewController new]; 53 | [self.navigationController pushViewController:infoVC animated:YES]; 54 | } 55 | 56 | #pragma mark - TCZLocalizableToolsDelegate 57 | - (void)localizableToolsEndWrite:(TCZLocalizableTools *)tool 58 | { 59 | self.title = @"解析成功"; 60 | [_indicatorView stopAnimating]; 61 | self.tableView.tableHeaderView = self.headerLabel; 62 | self.headerLabel.text = [NSString stringWithFormat:@"国际化后的文件被保存到:%@", [TCZLocalizableTools saveLocalizableRootFilePath]]; 63 | [self.tableView reloadData]; 64 | } 65 | 66 | - (void)localizableToolsEndParse:(TCZLocalizableTools *)tool 67 | { 68 | [_indicatorView startAnimating]; 69 | self.title = @"解析完成,正在写入文件..."; 70 | } 71 | 72 | - (void)localizableToolsError:(TCZLocalizableTools *)tool error:(NSError *)error 73 | { 74 | self.title = @"解析错误"; 75 | [_indicatorView stopAnimating]; 76 | self.tableView.tableHeaderView = nil; 77 | 78 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:error.localizedDescription ?: @"" preferredStyle:UIAlertControllerStyleAlert]; 79 | [alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleDestructive handler:nil]]; 80 | [self presentViewController:alert animated:YES completion:nil]; 81 | } 82 | 83 | #pragma mark - UITableViewDataSource, UITableViewDelegate 84 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 85 | { 86 | return _localizableTool.languagePaths.count; 87 | } 88 | 89 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 90 | { 91 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellId forIndexPath:indexPath]; 92 | NSString *name = [[_localizableTool.languagePaths objectAtIndex:indexPath.row] lastPathComponent]; 93 | cell.textLabel.text = name ?: @""; 94 | return cell; 95 | } 96 | 97 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 98 | { 99 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 100 | 101 | TCZPriviewViewController *priviewVC = [[TCZPriviewViewController alloc] initWithFilePath:_localizableTool.languagePaths[indexPath.row]]; 102 | [self.navigationController pushViewController:priviewVC animated:YES]; 103 | } 104 | 105 | - (void)createUI 106 | { 107 | _tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; 108 | _tableView.dataSource = self; 109 | _tableView.delegate = self; 110 | _tableView.tableFooterView = [UIView new]; 111 | [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellId]; 112 | [self.view addSubview:_tableView]; 113 | 114 | _indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; 115 | _indicatorView.tintColor = [UIColor blackColor]; 116 | _indicatorView.hidesWhenStopped = YES; 117 | [self.view addSubview:_indicatorView]; 118 | } 119 | 120 | - (UILabel *)headerLabel 121 | { 122 | if (_headerLabel) { 123 | return _headerLabel; 124 | } 125 | _headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 120)]; 126 | _headerLabel.numberOfLines = 0; 127 | _headerLabel.adjustsFontSizeToFitWidth = YES; 128 | _headerLabel.textColor = [UIColor redColor]; 129 | _headerLabel.font = [UIFont systemFontOfSize:15]; 130 | return _headerLabel; 131 | } 132 | 133 | @end 134 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZPriviewViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TCZPriviewViewController.h 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import 12 | 13 | @interface TCZPriviewViewController : UIViewController 14 | 15 | @property (nonatomic, copy) NSString *filePath; 16 | 17 | @property (nonatomic, strong) QLPreviewController *qlPreviewViewController; 18 | 19 | - (instancetype)initWithFilePath:(NSString *)filePath; 20 | 21 | - (void)reloadData; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/TCZPriviewViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // TCZPriviewViewController.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/24. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import "TCZPriviewViewController.h" 10 | 11 | @interface TCZPriviewViewController () 12 | 13 | 14 | @end 15 | 16 | @implementation TCZPriviewViewController 17 | 18 | - (instancetype)initWithFilePath:(NSString *)filePath 19 | { 20 | self = [super init]; 21 | if (self) { 22 | self.filePath = filePath; 23 | [self _createPreviewViewController]; 24 | } 25 | return self; 26 | } 27 | 28 | - (void)viewDidLoad 29 | { 30 | [super viewDidLoad]; 31 | self.title = [self.filePath lastPathComponent]; 32 | } 33 | 34 | - (void)_createPreviewViewController 35 | { 36 | if (!_qlPreviewViewController) { 37 | _qlPreviewViewController = [[QLPreviewController alloc] init]; 38 | _qlPreviewViewController.dataSource = self; 39 | _qlPreviewViewController.delegate = self; 40 | if (self.navigationController.navigationBar.translucent) { 41 | _qlPreviewViewController.view.frame = self.view.bounds; 42 | } else { 43 | _qlPreviewViewController.view.frame = CGRectMake(0, 64, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)-64); 44 | } 45 | self.view.backgroundColor = [UIColor whiteColor]; 46 | [self.view addSubview:_qlPreviewViewController.view]; 47 | [self addChildViewController:_qlPreviewViewController]; 48 | } 49 | } 50 | 51 | - (void)reloadData 52 | { 53 | [_qlPreviewViewController reloadData]; 54 | } 55 | 56 | - (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller 57 | { 58 | return 1; 59 | } 60 | 61 | - (id)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index 62 | { 63 | NSURL *fileUrl; 64 | if (_filePath) { 65 | fileUrl = [NSURL fileURLWithPath:_filePath]; 66 | } 67 | return fileUrl; 68 | } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /LocalToos/TCZLocalizable/unLocalizable.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 这是一个查找项目中未国际化的脚本 3 | 4 | import os 5 | import re 6 | 7 | # 汉语写入文件时需要 8 | import sys 9 | reload(sys) 10 | sys.setdefaultencoding('utf-8') 11 | 12 | # 将要解析的项目名称 13 | DESPATH = "/Users/wangsuyan/Desktop/Kmart" 14 | 15 | # 解析结果存放的路径 16 | WDESPATH = "/Users/wangsuyan/Desktop/unlocalized.log" 17 | 18 | #目录黑名单,这个目录下所有的文件将被忽略 19 | BLACKDIRLIST = [ 20 | DESPATH + '/Classes/Personal/PERSetting/PERAccount', # 多级目录 21 | DESPATH + '/Utils', # Utils 下所有的文件将被忽略 22 | 'PREPhoneNumResetViewController.m', # 文件名直接写,将忽略这个文件 23 | ] 24 | 25 | # 输出分隔符 26 | SEPREATE = ' <=> ' 27 | 28 | def isInBlackList(filePath): 29 | if os.path.isfile(filePath): 30 | return fileNameAtPath(filePath) in BLACKDIRLIST 31 | if filePath: 32 | return filePath in BLACKDIRLIST 33 | return False 34 | 35 | def fileNameAtPath(filePath): 36 | return os.path.split(filePath)[1] 37 | 38 | def isSignalNote(str): 39 | if '//' in str: 40 | return True 41 | if str.startswith('#pragma'): 42 | return True 43 | return False 44 | 45 | def isLogMsg(str): 46 | if str.startswith('NSLog') or str.startswith('FLOG'): 47 | return True 48 | return False 49 | 50 | def unlocalizedStrs(filePath): 51 | f = open(filePath) 52 | fileName = fileNameAtPath(filePath) 53 | isMutliNote = False 54 | isHaveWriteFileName = False 55 | for index, line in enumerate(f): 56 | #多行注释 57 | line = line.strip() 58 | if '/*' in line: 59 | isMutliNote = True 60 | if '*/' in line: 61 | isMutliNote = False 62 | if isMutliNote: 63 | continue 64 | 65 | #单行注释 66 | if isSignalNote(line): 67 | continue 68 | 69 | #打印信息 70 | if isLogMsg(line): 71 | continue 72 | 73 | matchList = re.findall(u'@"[\u4e00-\u9fff]+', line.decode('utf-8')) 74 | if matchList: 75 | if not isHaveWriteFileName: 76 | wf.write('\n' + fileName + '\n') 77 | isHaveWriteFileName = True 78 | 79 | for item in matchList: 80 | wf.write(str(index + 1) + ':' + item[2 : len(item)] + SEPREATE + line + '\n') 81 | 82 | def findFromFile(path): 83 | paths = os.listdir(path) 84 | for aCompent in paths: 85 | aPath = os.path.join(path, aCompent) 86 | if isInBlackList(aPath): 87 | print('在黑名单中,被自动忽略' + aPath) 88 | continue 89 | if os.path.isdir(aPath): 90 | findFromFile(aPath) 91 | elif os.path.isfile(aPath) and os.path.splitext(aPath)[1]=='.m': 92 | unlocalizedStrs(aPath) 93 | 94 | if __name__ == '__main__': 95 | wf = open(WDESPATH, 'w') 96 | findFromFile(DESPATH) 97 | wf.close() 98 | -------------------------------------------------------------------------------- /LocalToos/checkLocalizable.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import re 5 | 6 | # 将要解析的项目名称 7 | DESPATH = "/Users/wangsuyan/desktop/project/LGSKmart/LGSKmart/Resource/Localizations" 8 | 9 | ERROR_DESPATH = "/Users/wangsuyan/Desktop/checkLocalizable.log" 10 | 11 | MAIN_LOCALIZABLE_FILE = "/zh-Hans.lproj/Localizable.strings" 12 | 13 | _result = {} 14 | 15 | def filename(filePath): 16 | return os.path.split(filePath)[1] 17 | 18 | def pragram_error(filePath): 19 | if '/Base.lproj/Localizable.strings' in filePath: 20 | return 21 | print(filePath) 22 | f = open(filePath) 23 | isMutliNote = False 24 | fname = filePath.replace(DESPATH, '') 25 | _list = set() 26 | for index, line in enumerate(f): 27 | line = line.strip() 28 | 29 | if '/*' in line: 30 | isMutliNote = True 31 | if '*/' in line: 32 | isMutliNote = False 33 | if isMutliNote: 34 | continue 35 | 36 | if len(line) == 0 or line == '*/': 37 | continue 38 | 39 | if re.findall(r'^/+', line): 40 | continue 41 | 42 | regx = r'^"(.*s?)"\s*=\s*".*?";$' 43 | matchs = re.findall(regx, line) 44 | if matchs: 45 | for item in matchs: 46 | _list.add(item) 47 | _result[fname] = _list 48 | 49 | 50 | def find_from_file(path): 51 | paths = os.listdir(path) 52 | for aCompent in paths: 53 | aPath = os.path.join(path, aCompent) 54 | if os.path.isdir(aPath): 55 | find_from_file(aPath) 56 | elif os.path.isfile(aPath) and os.path.splitext(aPath)[1]=='.strings': 57 | pragram_error(aPath) 58 | 59 | def parse_result(): 60 | fValues = _result[MAIN_LOCALIZABLE_FILE] 61 | with open(ERROR_DESPATH, 'w') as ef: 62 | for k, v in _result.items(): 63 | if k == MAIN_LOCALIZABLE_FILE: 64 | continue 65 | result = fValues - v 66 | 67 | ef.write(k + '\n') 68 | for item in result: 69 | ef.write(item + '\n') 70 | ef.write('\n') 71 | 72 | 73 | if __name__ == '__main__': 74 | find_from_file(DESPATH) 75 | parse_result() 76 | print('已解析完成,结果保存在桌面中的 checkLocalizable.log 文件中') -------------------------------------------------------------------------------- /LocalToos/languages.csv: -------------------------------------------------------------------------------- 1 | "您已忽略了该事件,确定吗?",You have missed this event,Anda telah melewatkan acara ini,このイベントをを見落としました,귀하께서 해당사항 홀시 ,Anda telah terlepas acara ini,ท่านเลินเล่อเรื่องนี้แล้ว,Bạn đã bỏ lỡ sự kiện này 2 | 账号已被封停,account stop,please contact Customer Services,"Akun telah dihetikan, tolong hubungi layanan pelanggan",アカウントが停止し、カスタマーサービスまでご連絡ください,"계좌번호가 사용정지되어, 애프터 서비스와 연락할것. ","Akaun telah ditutup, sila hubungi khidmat pelanggan",บัญชีถูกแบน โปรดติดต่อเจ้าหน้าที่บริการลูกค้า,"Tài khoản đã bị đóng, vui lòng liên hệ chăm sóc khách hàng" 3 | 请更新版本后再申请提现,Please update new version before submitting withdrawal application,Harap perbarui versi baru sebelum mengirimkan aplikasi penarikan uang,新しいバージョンに更新してから現金引き出しを申請してください。,새로운 버전으로 갱신후에 다시 인출 신청할 것. ,Sila kemas kini versi anda memohon sekarang,กรุณาอัพเดตเวอร์ชั่นแล้วค่อยยื่นขอเบิกถอนเงินสด,Vui lòng cập nhật phiên bản mới sau đó xin đổi tiền mặt 4 | 您的当前版本过低,请立即升级,"Please update to new version, your current version is outdated","Harap perbarui ke versi baru, karena versi Anda sekarang sudah usang",現在のバージョンが旧いです。新しいバージョンに更新してください,"귀하의 현재 버번이 너무 낮아, 즉시 승급할것. ","Versi semasa adalah ketinggalan zaman, sila kemas kini kepada versi baru",เวอร์ชั่นที่ท่านใช้อยู่ไม่ใช่เวอร์ชั่นล่าสุด กรุณาไปอัพเดตเวอร์ชั่นล่าสุด,"Phiên bản hiện tại của bạn quá cũ, vui lòng nâng cấp ngay" 5 | -------------------------------------------------------------------------------- /LocalToos/localizableError.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import re 5 | 6 | # 将要解析的项目名称 7 | DESPATH = "/Users/wangsuyan/desktop/project/LGSKmart/LGSKmart/Resource/Localizations" 8 | 9 | def filename(filePath): 10 | return os.path.split(filePath)[1] 11 | 12 | def pragram_error(filePath): 13 | with open(filePath) as f: 14 | isMutliNote = False 15 | fname = filePath.replace(DESPATH, '') 16 | for index, line in enumerate(f): 17 | line = line.strip() 18 | 19 | if '/*' in line: 20 | isMutliNote = True 21 | if '*/' in line: 22 | isMutliNote = False 23 | if isMutliNote: 24 | continue 25 | 26 | if len(line) == 0 or line == '*/': 27 | continue 28 | 29 | if re.findall(r'^/+', line): 30 | continue 31 | 32 | regx = r'^".*s?"\s*=\s*".*?";$' 33 | matchs = re.findall(regx, line) 34 | if not matchs: 35 | result = fname + ':line[' + str(index) + '] : ' + line 36 | print(filePath) 37 | print(result) 38 | 39 | 40 | def find_from_file(path): 41 | paths = os.listdir(path) 42 | for aCompent in paths: 43 | aPath = os.path.join(path, aCompent) 44 | if os.path.isdir(aPath): 45 | find_from_file(aPath) 46 | elif os.path.isfile(aPath) and os.path.splitext(aPath)[1]=='.strings': 47 | pragram_error(aPath) 48 | 49 | if __name__ == '__main__': 50 | find_from_file(DESPATH) 51 | print('已完成') -------------------------------------------------------------------------------- /LocalToos/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // LocalToos 4 | // 5 | // Created by WangSuyan on 2017/8/23. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LocalToos/source.strings: -------------------------------------------------------------------------------- 1 | "5-6_event" = "您已忽略了该事件,确定吗?"; 2 | "5-6_customer" = "账号已被封停"; 3 | "5-6_version" = "请更新版本后再申请提现"; 4 | "5-6_update" = "您的当前版本过低,请立即升级"; 5 | -------------------------------------------------------------------------------- /LocalToos/unUseImage.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | import re 5 | import shutil 6 | 7 | # 是否开启自动删除,开启后当检查到未用到的图, 8 | # 将自动被删除。建议确认所有的图没用后开启 9 | IS_OPEN_AUTO_DEL = False 10 | 11 | # 将要解析的项目名称 12 | DESPATH = "/Users/wangsuyan/Desktop/project/Kmart" 13 | 14 | # 可能检查出错的图片,需要特别留意下 15 | ERROR_DESPATH = "/Users/wangsuyan/Desktop/unUseImage/error.log" 16 | 17 | # 解析结果存放的路径 18 | WDESPATH = "/Users/wangsuyan/Desktop/unUseImage/image.log" 19 | 20 | # 项目中没有用到的图片 21 | IMAGE_WDESPATH = "/Users/wangsuyan/Desktop/unUseImage/images/" 22 | 23 | # 目录黑名单,这个目录下所有的图片将被忽略 24 | BLACK_DIR_LIST = [ 25 | DESPATH + '/ThirdPart', # Utils 下所有的文件将被忽略 26 | ] 27 | 28 | # 已知某些图片确实存在,比如像下面的图,脚本不会自动检查出,需要手动加入这个数组中 29 | # NSString *name = [NSString stringWithFormat:@"loading_%d",i]; 30 | # UIImage *image = [UIImage imageNamed:name]; 31 | EXCEPT_IMAGES = [ 32 | ] 33 | 34 | # 项目中所有的图 35 | source_images = dict() 36 | # 项目中所有使用到的图 37 | use_images = set() 38 | # 异常图片 39 | err_images = set() 40 | 41 | # 目录是否在黑名单中 BLACK_DIR_LIST 42 | def isInBlackList(filePath): 43 | if os.path.isfile(filePath): 44 | return filename(filePath) in BLACK_DIR_LIST 45 | if filePath: 46 | return filePath in BLACK_DIR_LIST 47 | return False 48 | 49 | # 是否为图片 50 | def isimage(filePath): 51 | ext = os.path.splitext(filePath)[1] 52 | return ext == '.png' or ext == '.jpg' or ext == '.jpeg' or ext == '.gif' 53 | 54 | # 是否为 APPIcon 55 | def isappicon(filePath): 56 | return 'appiconset' in filePath 57 | 58 | def filename(filePath): 59 | return os.path.split(filePath)[1] 60 | 61 | def is_except_image(filePath): 62 | name = filename(filePath) 63 | for item in EXCEPT_IMAGES: 64 | if item in name: 65 | return True 66 | return False 67 | 68 | def auto_remove_images(): 69 | f = open(WDESPATH, 'r') 70 | for line in f.readlines(): 71 | path = DESPATH + line.strip('\n') 72 | if not os.path.isdir(path): 73 | if 'Assets.xcassets' in line: 74 | path = os.path.split(path)[0] 75 | if os.path.exists(path): 76 | shutil.rmtree(path) 77 | else: 78 | os.remove(path) 79 | 80 | 81 | def un_use_image(filePath): 82 | if re.search(r'\w@3x.(png|jpg|jpeg|gif)', filePath): 83 | return 84 | 85 | if re.search(r'\w(@2x){0,1}.(png|jpg|jpeg|gif)', filePath): 86 | exts = os.path.splitext(filePath) 87 | result = (filename(filePath).replace('@2x', '')).replace(exts[1],'') 88 | source_images[result] = filePath 89 | 90 | def find_image_name(filePath): 91 | f = open(filePath) 92 | for index, line in enumerate(f): 93 | line = line.strip() 94 | 95 | if '.xib' in filePath or 'storyboard' in filePath: 96 | regx = r'image="(.+?).(?:png|jpg|jpeg|gif)"' 97 | else: 98 | regx = r'\[\s*UIImage\s+imageNamed\s*:\s*@"(.+?)"' 99 | matchs = re.findall(regx, line) 100 | if matchs: 101 | for item in matchs: 102 | print(item) 103 | use_images.add(item) 104 | else: 105 | err_matchs = re.findall(r'\[UIImage imageNamed:', line) 106 | if err_matchs: 107 | name = filename(filePath) 108 | # print(line) 109 | for item in err_matchs: 110 | err_images.add(str(index + 1) + ':' + name + '\n' + line + '\n') 111 | 112 | def find_from_file(path): 113 | paths = os.listdir(path) 114 | for aCompent in paths: 115 | aPath = os.path.join(path, aCompent) 116 | ext = os.path.splitext(aPath)[1] 117 | if isInBlackList(aPath): 118 | print('在黑名单中,被自动忽略' + aPath) 119 | continue 120 | if os.path.isdir(aPath): 121 | find_from_file(aPath) 122 | elif os.path.isfile(aPath) and isimage(aPath) and not isappicon(aPath) and not is_except_image(aPath): 123 | un_use_image(aPath) 124 | elif os.path.isfile(aPath) and (ext=='.m' or ext=='.xib' or ext=='.storyboard'): 125 | find_image_name(aPath) 126 | 127 | if __name__ == '__main__': 128 | if os.path.exists(IMAGE_WDESPATH): 129 | shutil.rmtree(IMAGE_WDESPATH) 130 | 131 | os.makedirs(IMAGE_WDESPATH) 132 | 133 | wf = open(WDESPATH, 'w') 134 | find_from_file(DESPATH) 135 | for item in set(source_images.keys()) - use_images: 136 | value = source_images[item] 137 | wf.write(value.replace(DESPATH, '') + '\n') 138 | ext = os.path.splitext(value)[1] 139 | shutil.copyfile(value, IMAGE_WDESPATH + item + ext) 140 | 141 | wf.close() 142 | 143 | ef = open(ERROR_DESPATH, 'w') 144 | for item in err_images: 145 | ef.write(item) 146 | ef.close() 147 | 148 | if IS_OPEN_AUTO_DEL: 149 | auto_remove_images() -------------------------------------------------------------------------------- /LocalToosTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LocalToosTests/LocalToosTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocalToosTests.m 3 | // LocalToosTests 4 | // 5 | // Created by WangSuyan on 2017/8/23. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LocalToosTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation LocalToosTests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | - (void)tearDown { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | [super tearDown]; 25 | } 26 | 27 | - (void)testExample { 28 | // This is an example of a functional test case. 29 | // Use XCTAssert and related functions to verify your tests produce the correct results. 30 | } 31 | 32 | - (void)testPerformanceExample { 33 | // This is an example of a performance test case. 34 | [self measureBlock:^{ 35 | // Put the code you want to measure the time of here. 36 | }]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /LocalToosUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LocalToosUITests/LocalToosUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // LocalToosUITests.m 3 | // LocalToosUITests 4 | // 5 | // Created by WangSuyan on 2017/8/23. 6 | // Copyright © 2017年 WangSuyan. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LocalToosUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation LocalToosUITests 16 | 17 | - (void)setUp { 18 | [super setUp]; 19 | 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | 22 | // In UI tests it is usually best to stop immediately when a failure occurs. 23 | self.continueAfterFailure = NO; 24 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 25 | [[[XCUIApplication alloc] init] launch]; 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | - (void)tearDown { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | [super tearDown]; 33 | } 34 | 35 | - (void)testExample { 36 | // Use recording to get started writing UI tests. 37 | // Use XCTAssert and related functions to verify your tests produce the correct results. 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | * [如何把国际化时需要3天的工作量缩短到10分钟](#如何把国际化时需要3天的工作量缩短到10分钟) 3 | * [如何1秒找出国际化文件语法错误](#如何1秒找出国际化文件语法错误) 4 | * [找出项目中未国际化的文本](#找出项目中未国际化的文本) 5 | * [更人性化的找出中未使用的图](#更人性化的找出中未使用的图) 6 | * [如何找出国际化文件中未国际化的文本](#如何找出国际化文件中未国际化的文本) 7 | 8 | ## 如何把国际化时需要3天的工作量缩短到10分钟 9 | 10 | #### 1.使用前必读 11 | - 1. source.strings 是已国际化好的文件,比如当前您只有汉语国际化文件,那么就使用汉语国际化文件作为 source.strings,名字必须为 source.strings; 12 | - 2. 将要解析的文件(所有国际化后的文件),必须为 csv 文件,一般 World,Numbers (建议使用 Numbers)都支持把 excel 文件导出为 csv 文件;命名为:languages.csv 13 | - 3. 完成解析后,可以导出 ipa 包或者直接从沙盒中读取文件,找到对应的文件,直接复制到自己项目中即可; 14 | - 4. 解析失败的会标记为:❌❌。 15 | 16 | #### 2.痛点 17 | 如果 APP 要求国际化,其实添加国际化文字是很头痛的一件事。对于一个大型 18 | APP 来说,更是麻烦,而且工作量很大。通常替换国际化文字时,产品会给我们一个 Excel 表: 19 | 20 | ![excel.png](http://upload-images.jianshu.io/upload_images/1664496-bcd911a268c2c574.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 21 | 22 | 我们需要做的事,就是把 Excel 表中的文字,添加到下面各个文件中: 23 | 24 | ![locailzable.png](http://upload-images.jianshu.io/upload_images/1664496-c612ff30b7cb26f6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 25 | 26 | 如果有 2000 条翻译,有 8 国语言需要添加,可想工作量有多大。 27 | 28 | #### 3.解决办法 29 | 我们目前手里只有汉语国际化文件,这里称为 `source.strings` 文件,还有一份全部翻译好的文件,这里称为 `language.xlxs`。 30 | 31 | `source.strings` 文件中的内容如下: 32 | 33 | ``` 34 | "5-6_event" = "您已忽略了该事件"; 35 | "5-6_customer" = "账号已被封停"; 36 | "5-6_version" = "请更新版本后再申请提现"; 37 | "5-6_update" = "您的当前版本过低,请立即升级"; 38 | ``` 39 | 40 | `language.xlxs` 文件中的内容如下: 41 | 42 | ![屏幕快照 2017-08-24 下午2.14.14.png](http://upload-images.jianshu.io/upload_images/1664496-dae9f3040de68c75.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 43 | 44 | 我们想做的就是把 `language.xlxs` 文件的内容转换成类似: 45 | ``` 46 | "5-6_event" = "You have missed this event"; 47 | "5-6_customer" = "account stop,please contact Customer Services"; 48 | "5-6_version" = "Please update new version before submitting withdrawal application"; 49 | "5-6_update" = "Please update to new version, your current version is outdated"; 50 | ``` 51 | 52 | ``` 53 | "5-6_event" = "귀하께서 해당사항 홀시 "; 54 | "5-6_customer" = "계좌번호가 사용정지되어, 애프터 서비스와 연락할것. 55 | "5-6_version" = "새로운 버전으로 갱신후에 다시 인출 신청할 것. "; 56 | "5-6_update" = "귀하의 현재 버번이 너무 낮아, 즉시 승급할것. "; 57 | ``` 58 | 这样的文件,然后把它导入到对应的国际化文件中 59 | 60 | ![locailzable.png](http://upload-images.jianshu.io/upload_images/1664496-93f1d3caf030a360.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 61 | 62 | 这样就算完成了。 63 | 64 | 观察发现,这些都有规律可循,我们完全可以使用一个工具来做这些事,而不是手动。 65 | 66 | #### 4.TCZLocalizableTool 67 | [TCZLocalizableTool](https://github.com/lefex/TCZLocalizableTool) 可以帮助我们完成这些事,当然某些地方可能需要手动改一下。 **如果觉得能帮到你,给个星星支持一下**。它主要原理为: 68 | 69 | 70 | ![process.png](http://upload-images.jianshu.io/upload_images/1664496-b5293bcfe41a2a62.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 71 | 72 | 以汉语为例说明一下 [TCZLocalizableTool](https://github.com/lefex/TCZLocalizableTool) 工作流程: 73 | 74 | - `source.strings` 其实是一个 `plist` 文件,可以把它转换成 key-value 的形式,这样我们可以拿到自己定义的 key 和 value; 75 | - 解析 Excel 文件 `language.xlxs` 一般需要把它转换成 `language.csv` 文件; 76 | - 读取 `language.csv` 文件中的每个值的时候,根据 `source.strings` 转换后的字典,可以找到对应的 key 和 value; 77 | - 把结果导出为 csv 文件。 78 | 79 | 最终结果如图: 80 | 81 | 82 | 83 | 84 | ## 如何1秒找出国际化文件语法错误 85 | 86 | 国际化的时候难免会由于不小心,会出现语法错误,如果国际化文件有几千行的时候,无非是一场灾难。有时为了解决一个语法错误可能会耗费几个小时。使用这个脚本可以 《1秒》 定位到报错的代码行。比如 `"HOM_Lefe" = "wsy“;` 由于错误使用了汉语双引号,导致编译失败。 87 | 88 | **【如何使用】** 89 | * 1.修改 DESPATH 为你项目的路径; 90 | * 2.直接在脚本所在的目录下,打开终端执行 python localizableError.py,这里的 localizableError.py 为脚本文件名。你可以在这里 http://t.cn/RORnD3s 找到脚本文件; 91 | * 3.执行完成后,控制台会打印报错的代码行。 92 | `/en.lproj/Localizable.strings:line[11] : "HOM_Lefe" = "wsy“;` 93 | 94 | ## 找出项目中未国际化的文本 95 | 96 | 对于支持多语言的 APP 来说,国际化非常麻烦,而找出项目中未国际化的文字非常耗时(如果单纯的靠手动查找)。虽然可以使用 Xcode 自带的工具(Show not-localized strings)或者 Analyze 找出未国际化的文本,但是它们都不够灵活,而且比较耗时。如果能直接把项目中未国际化的文本导入到一个文件中,直接给产品,然后再使用 [TCZLocalizableTool] http://t.cn/ROcrQuB ,岂不是事半功倍。图中就是通过一个 Python 脚本获得的部分未国际化的文本。 97 | 98 | **【如何使用】** 99 | * 1.修改 DESPATH 路径为你项目的路径 100 | * 2.直接在脚本所在的目录下,执行 python unLocalizable.py,这里的 unLocalizable.py 为脚本文件名。你可以在这里 (http://t.cn/ROcrQu1 找到脚本文件。 101 | * 3.BLACKDIRLIST 你可以过滤掉和国际化无关的文件,比如某些第三方库。 102 | 103 | 104 | ## 更人性化的找出中未使用的图 105 | 106 | **【痛点】** 107 | 删除 iOS 项目中没有用到的图片市面上已经有很多种方式,但是我试过几个都不能很好地满足需求,因此使用 Python 写了这个脚本,它可能也不能很好的满足你的需求,因为这种静态查找始终会存在问题,每个人写的代码风格不一,导致匹配字符不一。所以只有掌握了脚本的写法,才能很好的满足自己的需求。如果你的项目中使用 OC,而且使用纯代码布局,使用这个脚本完全没有问题。当然你可以修改脚本来达到自己的需求。本文主要希望能够帮助更多的读者节省更多时间做一些有意义的工作,避免那些乏味重复的工作。 108 | 109 | **【如何使用】** 110 | * 1.修改 DESPATH 为你项目的路径; 111 | * 2.直接在脚本所在的目录下,打开终端执行 python unUseImage.py,这里的 unUseImage.py 为脚本文件名。你可以在这里 http://t.cn/ROXKobQ 找到脚本文件; 112 | * 3.执行完成后,桌面会出现一个 unUseImage 文件夹。文件夹中的 error.log 文件记录了可能存在未匹配到图片的文件目录,image.log 记录了项目中没使用的图片路径,images 存放了未使用到的图片。 113 | 114 | **【重要提示】** 115 | 当确认 `images` 文件夹中含有正在使用的图时,复制图片名字到 EXCEPT_IMAGES 中,再次执行脚本,确认 images 文件夹中不再包含使用的图后,修改 IS_OPEN_AUTO_DEL 为 True,执行脚本,脚本将自动清除所有未使用的图。 116 | 117 | ## 如何找出国际化文件中未国际化的文本 118 | 119 | 国际化的时候难免会由于不小心,会出现某个 .strings 文件中存在没有添加的国际化字符串。比如某个项目中支持中文和英文。在中文国际化文件(zh-Hans.lproj/Localizable.strings)中含有 : 120 | ``` 121 | "HOM_home" = "首页"; 122 | "GRB_groupBuy" = "团购"; 123 | "SHC_shopnCart" = "购物车"; 124 | "PER_personal" = "我的"; 125 | ``` 126 | 127 | 而在英文国际化文件(en.lproj/Localizable.strings)中含有 : 128 | ``` 129 | "HOM_home" = "home"; 130 | "PER_personal" = "my"; 131 | ``` 132 | 133 | 这样导致,英文环境下,`SHC_shopnCart` 和 `GRB_groupBuy` 未国际化,使用这个脚本会检测出这些错误。 134 | 135 | **【如何使用】** 136 | 137 | * 1.修改 DESPATH 为你项目的路径; 138 | * 2.直接在脚本所在的目录下,打开终端执行 python checkLocalizable.py,这里的 checkLocalizable.py 为脚本文件名。你可以在这里 http://t.cn/ROge6j4 找到脚本文件; 139 | * 3.执行完成后,桌面会出现一个文件 checkLocalizable.log,记录了未国际化的行: 140 | 141 | ``` 142 | /en.lproj/Localizable.strings 143 | SHC_shopnCart 144 | GRB_groupBuy 145 | ``` 146 | 147 | 148 | --------------------------------------------------------------------------------