Xcode Swift Run Script|自动检查多语系与图片资源缺漏提升开发效率
iOS 开发者常遇多语系与图片资源错漏问题,透过 Swift 撰写 Run Script 自动检查语系重复、缺漏及图片未使用状态,快速定位错误并减少冗余,让专案建置更稳定且节省时间。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
Xcode 直接使用 Swift 撰写 Shell Script!
导入 Localization 多语系及 Image Assets 缺漏检查、使用 Swift 打造 Shell Script 脚本
Photo by Glenn Carstens-Peters
缘由
因为自己手残,时常在编辑多语系档案时遗漏「;」导致 app build 出来语言显示出错再加上随著开发的推移语系档案越来越庞大,重复的、已没用到的语句都夹杂再一起,非常混乱(Image Assets 同样状况)。
一直以来都想找工具协助处理这方面的问题,之前是用 iOSLocalizationEditor 这个 Mac APP,但它比较像是语系档案编辑器,读取语系档案内容&编辑,没有自动检查的功能。
期望功能
build 专案时能自动检查多语系有无错误、缺露、重复、Image Assets 有无缺漏。
解决方案
要达到我们的期望功能就要在 Build Phases 加入 Run Script 检查脚本。
但检查脚本需要使用 shell script 撰写,因自己对 shell script 的掌握度并不太高,想说站在巨人的肩膀上从网路搜寻现有脚本也找不太到完全符合期望功能的 script,再快要放弃的时候突然想到:
Shell Script 可以用 Swift 来写啊 !
相对 shell script 来说更熟悉、掌握度更高!依照这个方向果然让我找到两个现有的工具脚本!
由 freshOS 这个团队撰写的两个检查工具:
完全符合我们的期望功能需求! ! 并且他们使用 swift 撰写,要客制化魔改都很容易。
Localize 🏁 多语系档检查工具
功能:
build 时自动检查
语系档自动排版、整理
检查多语系与主要语系之缺漏、多余
检查多语系重复语句
检查多语系未经翻译语句
检查多语系未使用的语句
安装方法:
放到专案目录下 EX:
${SRCROOT}/Localize.swift
打开专案设定 → iOS Target → Build Phases →左上角「+」 → New Run Script Phases → 在 Script 内容贴上路径 EX:
${SRCROOT}/Localize.swift
- 使用 Xcode 打开编辑
Localize.swift
档案进行设定,可以在档案上半部看到可更动的设定项目:
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
//启用检查脚本
let enabled = true
//语系档案目录
let relativeLocalizableFolders = "/Resources/Languages"
//专案目录(用来搜索语句有没有在程式码中使用到)
let relativeSourceFolder = "/Sources"
//程式码中的 NSLocalized 语系档案使用正规匹配表示法
//可自行增加、无需变动
let patterns = [
"NSLocalized(Format)?String\\(\\s*@?\"([\\w\\.]+)\"", // Swift and Objc Native
"Localizations\\.((?:[A-Z]{1}[a-z]*[A-z]*)*(?:\\.[A-Z]{1}[a-z]*[A-z]*)*)", // Laurine Calls
"L10n.tr\\(key: \"(\\w+)\"", // SwiftGen generation
"ypLocalized\\(\"(.*)\"\\)",
"\"(.*)\".localized" // "key".localized pattern
]
//要忽略「语句未使用警告」的语句
let ignoredFromUnusedKeys: [String] = []
/* example
let ignoredFromUnusedKeys = [
"NotificationNoOne",
"NotificationCommentPhoto",
"NotificationCommentHisPhoto",
"NotificationCommentHerPhoto"
]
*/
//主要语系
let masterLanguage = "en"
//开启与系档案a-z排序、整理功能
let sanitizeFiles = false
//专案是单一or多语系
let singleLanguage = false
//启用检查未翻译语句功能
let checkForUntranslated = true
- Build!成功!
检查结果提示类型:
- Build Error ❌ :
- [Duplication] 项目在语系档案内存在重复
- [Unused Key] 项目在语系档案内有定义,但实际程式中未使用到
- [Missing] 项目在语系档案内未定义,但实际程式中有使用到
- [Redundant] 项目在此语系档相较于主要语系档是多余的
- [Missing Translation] 项目在主要语系档有,但在此语系档缺漏
- Build Warning ⚠️ :
- [Potentially Untranslated] 此项目未经翻译(与主语系档项目内容相同)
还没结束,现在自动检查提示有了,但我们还需要自行魔改一下。
客制化匹配正规表示:
回头看检查脚本 Localize.swift
顶部设定区块 patterns 部分的第一项:
"NSLocalized(Format)?String\\(\\s*@?\"([\\w\\.]+)\""
匹配 Swift/ObjC的 NSLocalizedString()
方法,这个正规表示式只能匹配 "Home.Title"
这种格式的语句;假设我们是完整句子或有带 Format 参数,则会被当误当成 [Unused Key]。
EX: "Hi, %@ welcome to my app"、"Hello World!"
<- 这些语句都无法匹配
我们可以新增一条 patterns 设定、或更改原本的 patterns 成:
"NSLocalized(Format)?String\\(\\s*@?\"([^(\")]+)\""
主要是调整 NSLocalizedString
方法后的匹配语句,变成取任意字串直到 "
出现就中止,你也可以 点此 依照自己的需求进行客制。
加上语系档案格式检查功能:
此脚本仅针对语系档做内容对应检查,不会检查档案格式是否正确(是否有忘记加「 ; 」),如果需要这个功能要自己加上!
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
//....
let formatResult = shell("plutil -lint \(location)")
guard formatResult.trimmingCharacters(in: .whitespacesAndNewlines).suffix(2) == "OK" else {
let str = "\(path)/\(name).lproj"
+ "/Localizable.strings:1: "
+ "error: [File Invaild] "
+ "This Localizable.strings file format is invalid."
print(str)
numberOfErrors += 1
return
}
//....
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/bash"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
增加 shell()
执行 shell script,使用 plutil -lint
检查 plist 语系档案格式正确性,有错、少「;」会回传错误,没错会回传 OK
以此作为判断!
检查的地方可加在 LocalizationFiles->process( ) -> let location = singleLanguage…
后,约 135 行的地方或参考我最后提供的完整魔改版。
其他客制化:
我们可以依照自己的需求进行客制,例如把 error 换成 warning 或是拔掉某个检查功能 (EX: Potentially Untranslated、Unused Key);脚本就是 swift 我们都很熟悉!不怕改坏改错!
要让 build 时出现 Error ❌:
1
print("Project档案.lproj" + "/档案:行: " + "error: 错误讯息")
要让 build 时出现 Warning ⚠️:
1
print("Project档案.lproj" + "/档案:行: " + "warning: 警告讯息")
最终魔改版:
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#!/usr/bin/env xcrun --sdk macosx swift
import Foundation
// WHAT
// 1. Find Missing keys in other Localisation files
// 2. Find potentially untranslated keys
// 3. Find Duplicate keys
// 4. Find Unused keys and generate script to delete them all at once
// MARK: Start Of Configurable Section
/*
You can enable or disable the script whenever you want
*/
let enabled = true
/*
Put your path here, example -> Resources/Localizations/Languages
*/
let relativeLocalizableFolders = "/streetvoice/SupportingFiles"
/*
This is the path of your source folder which will be used in searching
for the localization keys you actually use in your project
*/
let relativeSourceFolder = "/streetvoice"
/*
Those are the regex patterns to recognize localizations.
*/
let patterns = [
"NSLocalized(Format)?String\\(\\s*@?\"([^(\")]+)\"", // Swift and Objc Native
"Localizations\\.((?:[A-Z]{1}[a-z]*[A-z]*)*(?:\\.[A-Z]{1}[a-z]*[A-z]*)*)", // Laurine Calls
"L10n.tr\\(key: \"(\\w+)\"", // SwiftGen generation
"ypLocalized\\(\"(.*)\"\\)",
"\"(.*)\".localized" // "key".localized pattern
]
/*
Those are the keys you don't want to be recognized as "unused"
For instance, Keys that you concatenate will not be detected by the parsing
so you want to add them here in order not to create false positives :)
*/
let ignoredFromUnusedKeys: [String] = []
/* example
let ignoredFromUnusedKeys = [
"NotificationNoOne",
"NotificationCommentPhoto",
"NotificationCommentHisPhoto",
"NotificationCommentHerPhoto"
]
*/
let masterLanguage = "base"
/*
Sanitizing files will remove comments, empty lines and order your keys alphabetically.
*/
let sanitizeFiles = false
/*
Determines if there are multiple localizations or not.
*/
let singleLanguage = false
/*
Determines if we should show errors if there's a key within the app
that does not appear in master translations.
*/
let checkForUntranslated = false
// MARK: End Of Configurable Section
if enabled == false {
print("Localization check cancelled")
exit(000)
}
// Detect list of supported languages automatically
func listSupportedLanguages() -> [String] {
var sl: [String] = []
let path = FileManager.default.currentDirectoryPath + relativeLocalizableFolders
if !FileManager.default.fileExists(atPath: path) {
print("Invalid configuration: \(path) does not exist.")
exit(1)
}
let enumerator = FileManager.default.enumerator(atPath: path)
let extensionName = "lproj"
print("Found these languages:")
while let element = enumerator?.nextObject() as? String {
if element.hasSuffix(extensionName) {
print(element)
let name = element.replacingOccurrences(of: ".\(extensionName)", with: "")
sl.append(name)
}
}
return sl
}
let supportedLanguages = listSupportedLanguages()
var ignoredFromSameTranslation: [String: [String]] = [:]
let path = FileManager.default.currentDirectoryPath + relativeLocalizableFolders
var numberOfWarnings = 0
var numberOfErrors = 0
struct LocalizationFiles {
var name = ""
var keyValue: [String: String] = [:]
var linesNumbers: [String: Int] = [:]
init(name: String) {
self.name = name
process()
}
mutating func process() {
if sanitizeFiles {
removeCommentsFromFile()
removeEmptyLinesFromFile()
sortLinesAlphabetically()
}
let location = singleLanguage ? "\(path)/Localizable.strings" : "\(path)/\(name).lproj/Localizable.strings"
let formatResult = shell("plutil -lint \(location)")
guard formatResult.trimmingCharacters(in: .whitespacesAndNewlines).suffix(2) == "OK" else {
let str = "\(path)/\(name).lproj"
+ "/Localizable.strings:1: "
+ "error: [File Invaild] "
+ "This Localizable.strings file format is invalid."
print(str)
numberOfErrors += 1
return
}
guard let string = try? String(contentsOfFile: location, encoding: .utf8) else {
return
}
let lines = string.components(separatedBy: .newlines)
keyValue = [:]
let pattern = "\"(.*)\" = \"(.+)\";"
let regex = try? NSRegularExpression(pattern: pattern, options: [])
var ignoredTranslation: [String] = []
for (lineNumber, line) in lines.enumerated() {
let range = NSRange(location: 0, length: (line as NSString).length)
// Ignored pattern
let ignoredPattern = "\"(.*)\" = \"(.+)\"; *\\/\\/ *ignore-same-translation-warning"
let ignoredRegex = try? NSRegularExpression(pattern: ignoredPattern, options: [])
if let ignoredMatch = ignoredRegex?.firstMatch(in: line,
options: [],
range: range) {
let key = (line as NSString).substring(with: ignoredMatch.range(at: 1))
ignoredTranslation.append(key)
}
if let firstMatch = regex?.firstMatch(in: line, options: [], range: range) {
let key = (line as NSString).substring(with: firstMatch.range(at: 1))
let value = (line as NSString).substring(with: firstMatch.range(at: 2))
if keyValue[key] != nil {
let str = "\(path)/\(name).lproj"
+ "/Localizable.strings:\(linesNumbers[key]!): "
+ "error: [Duplication] \"\(key)\" "
+ "is duplicated in \(name.uppercased()) file"
print(str)
numberOfErrors += 1
} else {
keyValue[key] = value
linesNumbers[key] = lineNumber + 1
}
}
}
print(ignoredFromSameTranslation)
ignoredFromSameTranslation[name] = ignoredTranslation
}
func rebuildFileString(from lines: [String]) -> String {
return lines.reduce("") { (r: String, s: String) -> String in
(r == "") ? (r + s) : (r + "\n" + s)
}
}
func removeEmptyLinesFromFile() {
let location = "\(path)/\(name).lproj/Localizable.strings"
if let string = try? String(contentsOfFile: location, encoding: .utf8) {
var lines = string.components(separatedBy: .newlines)
lines = lines.filter { $0.trimmingCharacters(in: .whitespaces) != "" }
let s = rebuildFileString(from: lines)
try? s.write(toFile: location, atomically: false, encoding: .utf8)
}
}
func removeCommentsFromFile() {
let location = "\(path)/\(name).lproj/Localizable.strings"
if let string = try? String(contentsOfFile: location, encoding: .utf8) {
var lines = string.components(separatedBy: .newlines)
lines = lines.filter { !$0.hasPrefix("//") }
let s = rebuildFileString(from: lines)
try? s.write(toFile: location, atomically: false, encoding: .utf8)
}
}
func sortLinesAlphabetically() {
let location = "\(path)/\(name).lproj/Localizable.strings"
if let string = try? String(contentsOfFile: location, encoding: .utf8) {
let lines = string.components(separatedBy: .newlines)
var s = ""
for (i, l) in sortAlphabetically(lines).enumerated() {
s += l
if i != lines.count - 1 {
s += "\n"
}
}
try? s.write(toFile: location, atomically: false, encoding: .utf8)
}
}
func removeEmptyLinesFromLines(_ lines: [String]) -> [String] {
return lines.filter { $0.trimmingCharacters(in: .whitespaces) != "" }
}
func sortAlphabetically(_ lines: [String]) -> [String] {
return lines.sorted()
}
}
// MARK: - Load Localisation Files in memory
let masterLocalizationFile = LocalizationFiles(name: masterLanguage)
let localizationFiles = supportedLanguages
.filter { $0 != masterLanguage }
.map { LocalizationFiles(name: $0) }
// MARK: - Detect Unused Keys
let sourcesPath = FileManager.default.currentDirectoryPath + relativeSourceFolder
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(atPath: sourcesPath)
var localizedStrings: [String] = []
while let swiftFileLocation = enumerator?.nextObject() as? String {
// checks the extension
if swiftFileLocation.hasSuffix(".swift") \\|\\| swiftFileLocation.hasSuffix(".m") \\|\\| swiftFileLocation.hasSuffix(".mm") {
let location = "\(sourcesPath)/\(swiftFileLocation)"
if let string = try? String(contentsOfFile: location, encoding: .utf8) {
for p in patterns {
let regex = try? NSRegularExpression(pattern: p, options: [])
let range = NSRange(location: 0, length: (string as NSString).length) // Obj c wa
regex?.enumerateMatches(in: string,
options: [],
range: range,
using: { result, _, _ in
if let r = result {
let value = (string as NSString).substring(with: r.range(at: r.numberOfRanges - 1))
localizedStrings.append(value)
}
})
}
}
}
}
var masterKeys = Set(masterLocalizationFile.keyValue.keys)
let usedKeys = Set(localizedStrings)
let ignored = Set(ignoredFromUnusedKeys)
let unused = masterKeys.subtracting(usedKeys).subtracting(ignored)
let untranslated = usedKeys.subtracting(masterKeys)
// Here generate Xcode regex Find and replace script to remove dead keys all at once!
var replaceCommand = "\"("
var counter = 0
for v in unused {
var str = "\(path)/\(masterLocalizationFile.name).lproj/Localizable.strings:\(masterLocalizationFile.linesNumbers[v]!): "
str += "error: [Unused Key] \"\(v)\" is never used"
print(str)
numberOfErrors += 1
if counter != 0 {
replaceCommand += "\\|"
}
replaceCommand += v
if counter == unused.count - 1 {
replaceCommand += ")\" = \".*\";"
}
counter += 1
}
print(replaceCommand)
// MARK: - Compare each translation file against master (en)
for file in localizationFiles {
for k in masterLocalizationFile.keyValue.keys {
if file.keyValue[k] == nil {
var str = "\(path)/\(file.name).lproj/Localizable.strings:\(masterLocalizationFile.linesNumbers[k]!): "
str += "error: [Missing] \"\(k)\" missing from \(file.name.uppercased()) file"
print(str)
numberOfErrors += 1
}
}
let redundantKeys = file.keyValue.keys.filter { !masterLocalizationFile.keyValue.keys.contains($0) }
for k in redundantKeys {
let str = "\(path)/\(file.name).lproj/Localizable.strings:\(file.linesNumbers[k]!): "
+ "error: [Redundant key] \"\(k)\" redundant in \(file.name.uppercased()) file"
print(str)
}
}
if checkForUntranslated {
for key in untranslated {
var str = "\(path)/\(masterLocalizationFile.name).lproj/Localizable.strings:1: "
str += "error: [Missing Translation] \(key) is not translated"
print(str)
numberOfErrors += 1
}
}
print("Number of warnings : \(numberOfWarnings)")
print("Number of errors : \(numberOfErrors)")
if numberOfErrors > 0 {
exit(1)
}
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/bash"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
最后最后,还没结束!
当我们的 swift 检查工具脚本都调试完成之后,要将其 compile 成执行档减少 build 花费时间 ,否则每次 build 都要重新 compile 一次(约能减少 90% 的时间)。
打开 terminal ,前往专案中检查工具脚本所在目录下执行:
1
swiftc -o Localize Localize.swift
然后再回头到 Build Phases 更改 Script 内容路径成执行档
EX: ${SRCROOT}/Localize
完工!
工具 2. Asset Checker 👮 图片资源检查工具
功能:
build 时自动检查
检查图片缺漏:名称有呼叫,但图片资源目录内没有出现
检查图片多余:名称未使用,但图片资源目录存在的
安装方法:
放到专案目录下 EX:
${SRCROOT}/AssetChecker.swift
打开专案设定 → iOS Target → Build Phases →左上角「+」 → New Run Script Phases → 在 Script 内容贴上路径
1
2
${SRCROOT}/AssetChecker.swift ${SRCROOT}/专案目录 ${SRCROOT}/Resources/Images.xcassets
//${SRCROOT}/Resources/Images.xcassets = 你 .xcassets 的位置
可直接将设定参数带在路径上,参数1:专案目录位置、参数2:图片资源目录位置;或跟语系检查工具一样编辑 AssetChecker.swift
顶部参数设定区块:
1
2
3
4
5
6
7
8
9
10
// Configure me \o/
// 专案目录位置(用来搜索图片有没有在程式码中使用到)
var sourcePathOption:String? = nil
// .xcassets 目录位置
var assetCatalogPathOption:String? = nil
// Unused 警告忽略项目
let ignoredUnusedNames = [String]()
- Build! 成功!
检查结果提示类型:
- Build Error ❌ :
- [Asset Missing] 项目在程式内有呼叫使用,但图片资源目录内没有出现
- Build Warning ⚠️ :
- [Asset Unused] 项目在程式内未使用,但图片资源目录内有出现 p.s 假设图片是动态变数提供,检查工具将无法识别,可将其加入
ignoredUnusedNames
中设为例外。
- [Asset Unused] 项目在程式内未使用,但图片资源目录内有出现 p.s 假设图片是动态变数提供,检查工具将无法识别,可将其加入
其他操作同语系检查工具,这边就不做赘述;最重要的事是也要 记得调适完后要 compile 成执行档,并更改 run script 内容为执行档!
开发自己的工具!
我们可以参考图片资源检查工具脚本:
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env xcrun --sdk macosx swift
import Foundation
// Configure me \o/
var sourcePathOption:String? = nil
var assetCatalogPathOption:String? = nil
let ignoredUnusedNames = [String]()
for (index, arg) in CommandLine.arguments.enumerated() {
switch index {
case 1:
sourcePathOption = arg
case 2:
assetCatalogPathOption = arg
default:
break
}
}
guard let sourcePath = sourcePathOption else {
print("AssetChecker:: error: Source path was missing!")
exit(0)
}
guard let assetCatalogAbsolutePath = assetCatalogPathOption else {
print("AssetChecker:: error: Asset Catalog path was missing!")
exit(0)
}
print("Searching sources in \(sourcePath) for assets in \(assetCatalogAbsolutePath)")
/* Put here the asset generating false positives,
For instance whne you build asset names at runtime
let ignoredUnusedNames = [
"IconArticle",
"IconMedia",
"voteEN",
"voteES",
"voteFR"
]
*/
// MARK : - End Of Configurable Section
func elementsInEnumerator(_ enumerator: FileManager.DirectoryEnumerator?) -> [String] {
var elements = [String]()
while let e = enumerator?.nextObject() as? String {
elements.append(e)
}
return elements
}
// MARK: - List Assets
func listAssets() -> [String] {
let extensionName = "imageset"
let enumerator = FileManager.default.enumerator(atPath: assetCatalogAbsolutePath)
return elementsInEnumerator(enumerator)
.filter { $0.hasSuffix(extensionName) } // Is Asset
.map { $0.replacingOccurrences(of: ".\(extensionName)", with: "") } // Remove extension
.map { $0.components(separatedBy: "/").last ?? $0 } // Remove folder path
}
// MARK: - List Used Assets in the codebase
func localizedStrings(inStringFile: String) -> [String] {
var localizedStrings = [String]()
let namePattern = "([\\w-]+)"
let patterns = [
"#imageLiteral\\(resourceName: \"\(namePattern)\"\\)", // Image Literal
"UIImage\\(named:\\s*\"\(namePattern)\"\\)", // Default UIImage call (Swift)
"UIImage imageNamed:\\s*\\@\"\(namePattern)\"", // Default UIImage call
"\\<image name=\"\(namePattern)\".*", // Storyboard resources
"R.image.\(namePattern)\\(\\)" //R.swift support
]
for p in patterns {
let regex = try? NSRegularExpression(pattern: p, options: [])
let range = NSRange(location:0, length:(inStringFile as NSString).length)
regex?.enumerateMatches(in: inStringFile,options: [], range: range) { result, _, _ in
if let r = result {
let value = (inStringFile as NSString).substring(with:r.range(at: 1))
localizedStrings.append(value)
}
}
}
return localizedStrings
}
func listUsedAssetLiterals() -> [String] {
let enumerator = FileManager.default.enumerator(atPath:sourcePath)
print(sourcePath)
#if swift(>=4.1)
return elementsInEnumerator(enumerator)
.filter { $0.hasSuffix(".m") \\|\\| $0.hasSuffix(".swift") \\|\\| $0.hasSuffix(".xib") \\|\\| $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files
.map { "\(sourcePath)/\($0)" } // Build file paths
.map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents
.compactMap{$0}
.compactMap{$0} // Remove nil entries
.map(localizedStrings) // Find localizedStrings ocurrences
.flatMap{$0} // Flatten
#else
return elementsInEnumerator(enumerator)
.filter { $0.hasSuffix(".m") \\|\\| $0.hasSuffix(".swift") \\|\\| $0.hasSuffix(".xib") \\|\\| $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files
.map { "\(sourcePath)/\($0)" } // Build file paths
.map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents
.flatMap{$0}
.flatMap{$0} // Remove nil entries
.map(localizedStrings) // Find localizedStrings ocurrences
.flatMap{$0} // Flatten
#endif
}
// MARK: - Begining of script
let assets = Set(listAssets())
let used = Set(listUsedAssetLiterals() + ignoredUnusedNames)
// Generate Warnings for Unused Assets
let unused = assets.subtracting(used)
unused.forEach { print("\(assetCatalogAbsolutePath):: warning: [Asset Unused] \($0)") }
// Generate Error for broken Assets
let broken = used.subtracting(assets)
broken.forEach { print("\(assetCatalogAbsolutePath):: error: [Asset Missing] \($0)") }
if broken.count > 0 {
exit(1)
}
相较于语系检查脚本,这个脚本简洁且重要的功能都有,很有参考价值!
P.S 可以看到程式码出现 localizedStrings()
命名,怀疑作者是从语系检查工具的逻辑搬来用,忘了改方法名称XD
例如:
1
2
3
4
5
6
7
8
9
10
for (index, arg) in CommandLine.arguments.enumerated() {
switch index {
case 1:
//参数1
case 2:
//参数2
default:
break
}
}
^接收外部参数的方法
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
52
53
54
55
func elementsInEnumerator(_ enumerator: FileManager.DirectoryEnumerator?) -> [String] {
var elements = [String]()
while let e = enumerator?.nextObject() as? String {
elements.append(e)
}
return elements
}
func localizedStrings(inStringFile: String) -> [String] {
var localizedStrings = [String]()
let namePattern = "([\\w-]+)"
let patterns = [
"#imageLiteral\\(resourceName: \"\(namePattern)\"\\)", // Image Literal
"UIImage\\(named:\\s*\"\(namePattern)\"\\)", // Default UIImage call (Swift)
"UIImage imageNamed:\\s*\\@\"\(namePattern)\"", // Default UIImage call
"\\<image name=\"\(namePattern)\".*", // Storyboard resources
"R.image.\(namePattern)\\(\\)" //R.swift support
]
for p in patterns {
let regex = try? NSRegularExpression(pattern: p, options: [])
let range = NSRange(location:0, length:(inStringFile as NSString).length)
regex?.enumerateMatches(in: inStringFile,options: [], range: range) { result, _, _ in
if let r = result {
let value = (inStringFile as NSString).substring(with:r.range(at: 1))
localizedStrings.append(value)
}
}
}
return localizedStrings
}
func listUsedAssetLiterals() -> [String] {
let enumerator = FileManager.default.enumerator(atPath:sourcePath)
print(sourcePath)
#if swift(>=4.1)
return elementsInEnumerator(enumerator)
.filter { $0.hasSuffix(".m") \\|\\| $0.hasSuffix(".swift") \\|\\| $0.hasSuffix(".xib") \\|\\| $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files
.map { "\(sourcePath)/\($0)" } // Build file paths
.map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents
.compactMap{$0}
.compactMap{$0} // Remove nil entries
.map(localizedStrings) // Find localizedStrings ocurrences
.flatMap{$0} // Flatten
#else
return elementsInEnumerator(enumerator)
.filter { $0.hasSuffix(".m") \\|\\| $0.hasSuffix(".swift") \\|\\| $0.hasSuffix(".xib") \\|\\| $0.hasSuffix(".storyboard") } // Only Swift and Obj-C files
.map { "\(sourcePath)/\($0)" } // Build file paths
.map { try? String(contentsOfFile: $0, encoding: .utf8)} // Get file contents
.flatMap{$0}
.flatMap{$0} // Remove nil entries
.map(localizedStrings) // Find localizedStrings ocurrences
.flatMap{$0} // Flatten
#endif
}
^遍历所有专案档案并进行正则匹配的方法
1
2
3
4
//要让 build 时出现 Error ❌:
print("Project档案.lproj" + "/档案:行: " + "error: 错误讯息")
//要让 build 时出现 Warning ⚠️:
print("Project档案.lproj" + "/档案:行: " + "warning: 警告讯息")
^print error or warning
可以综合参考以上的程式方法,自己打造想要的工具。
总结
这两个检查工具导入之后,我们在开发上就能更安心、更有效率并且减少冗余;也因为这次经验大开眼界,日后如果有什么新的 build run script 需求都能直接使用最熟悉的语言 swift 来进行制作!
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。