├── .gitignore ├── AbandonedStrings.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── AbandonedStrings └── main.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /AbandonedStrings.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0361F1401C605FE0009D519A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361F13F1C605FE0009D519A /* main.swift */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 0361F13A1C605FE0009D519A /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 0361F13C1C605FE0009D519A /* AbandonedStrings */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AbandonedStrings; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 0361F13F1C605FE0009D519A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 0361F1391C605FE0009D519A /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 0361F1331C605FE0009D519A = { 42 | isa = PBXGroup; 43 | children = ( 44 | 0361F13E1C605FE0009D519A /* AbandonedStrings */, 45 | 0361F13D1C605FE0009D519A /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 0361F13D1C605FE0009D519A /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 0361F13C1C605FE0009D519A /* AbandonedStrings */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 0361F13E1C605FE0009D519A /* AbandonedStrings */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 0361F13F1C605FE0009D519A /* main.swift */, 61 | ); 62 | path = AbandonedStrings; 63 | sourceTree = ""; 64 | }; 65 | /* End PBXGroup section */ 66 | 67 | /* Begin PBXNativeTarget section */ 68 | 0361F13B1C605FE0009D519A /* AbandonedStrings */ = { 69 | isa = PBXNativeTarget; 70 | buildConfigurationList = 0361F1431C605FE0009D519A /* Build configuration list for PBXNativeTarget "AbandonedStrings" */; 71 | buildPhases = ( 72 | 0361F1381C605FE0009D519A /* Sources */, 73 | 0361F1391C605FE0009D519A /* Frameworks */, 74 | 0361F13A1C605FE0009D519A /* CopyFiles */, 75 | ); 76 | buildRules = ( 77 | ); 78 | dependencies = ( 79 | ); 80 | name = AbandonedStrings; 81 | productName = AbandonedStrings; 82 | productReference = 0361F13C1C605FE0009D519A /* AbandonedStrings */; 83 | productType = "com.apple.product-type.tool"; 84 | }; 85 | /* End PBXNativeTarget section */ 86 | 87 | /* Begin PBXProject section */ 88 | 0361F1341C605FE0009D519A /* Project object */ = { 89 | isa = PBXProject; 90 | attributes = { 91 | LastSwiftUpdateCheck = 0720; 92 | LastUpgradeCheck = 0720; 93 | ORGANIZATIONNAME = iJoshSmith; 94 | TargetAttributes = { 95 | 0361F13B1C605FE0009D519A = { 96 | CreatedOnToolsVersion = 7.2; 97 | LastSwiftMigration = 0800; 98 | }; 99 | }; 100 | }; 101 | buildConfigurationList = 0361F1371C605FE0009D519A /* Build configuration list for PBXProject "AbandonedStrings" */; 102 | compatibilityVersion = "Xcode 3.2"; 103 | developmentRegion = English; 104 | hasScannedForEncodings = 0; 105 | knownRegions = ( 106 | en, 107 | ); 108 | mainGroup = 0361F1331C605FE0009D519A; 109 | productRefGroup = 0361F13D1C605FE0009D519A /* Products */; 110 | projectDirPath = ""; 111 | projectRoot = ""; 112 | targets = ( 113 | 0361F13B1C605FE0009D519A /* AbandonedStrings */, 114 | ); 115 | }; 116 | /* End PBXProject section */ 117 | 118 | /* Begin PBXSourcesBuildPhase section */ 119 | 0361F1381C605FE0009D519A /* Sources */ = { 120 | isa = PBXSourcesBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | 0361F1401C605FE0009D519A /* main.swift in Sources */, 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | /* End PBXSourcesBuildPhase section */ 128 | 129 | /* Begin XCBuildConfiguration section */ 130 | 0361F1411C605FE0009D519A /* Debug */ = { 131 | isa = XCBuildConfiguration; 132 | buildSettings = { 133 | ALWAYS_SEARCH_USER_PATHS = NO; 134 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 135 | CLANG_CXX_LIBRARY = "libc++"; 136 | CLANG_ENABLE_MODULES = YES; 137 | CLANG_ENABLE_OBJC_ARC = YES; 138 | CLANG_WARN_BOOL_CONVERSION = YES; 139 | CLANG_WARN_CONSTANT_CONVERSION = YES; 140 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 141 | CLANG_WARN_EMPTY_BODY = YES; 142 | CLANG_WARN_ENUM_CONVERSION = YES; 143 | CLANG_WARN_INT_CONVERSION = YES; 144 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 145 | CLANG_WARN_UNREACHABLE_CODE = YES; 146 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 147 | CODE_SIGN_IDENTITY = "-"; 148 | COPY_PHASE_STRIP = NO; 149 | DEBUG_INFORMATION_FORMAT = dwarf; 150 | ENABLE_STRICT_OBJC_MSGSEND = YES; 151 | ENABLE_TESTABILITY = YES; 152 | GCC_C_LANGUAGE_STANDARD = gnu99; 153 | GCC_DYNAMIC_NO_PIC = NO; 154 | GCC_NO_COMMON_BLOCKS = YES; 155 | GCC_OPTIMIZATION_LEVEL = 0; 156 | GCC_PREPROCESSOR_DEFINITIONS = ( 157 | "DEBUG=1", 158 | "$(inherited)", 159 | ); 160 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 161 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 162 | GCC_WARN_UNDECLARED_SELECTOR = YES; 163 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 164 | GCC_WARN_UNUSED_FUNCTION = YES; 165 | GCC_WARN_UNUSED_VARIABLE = YES; 166 | MACOSX_DEPLOYMENT_TARGET = 10.11; 167 | MTL_ENABLE_DEBUG_INFO = YES; 168 | ONLY_ACTIVE_ARCH = YES; 169 | SDKROOT = macosx; 170 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 171 | }; 172 | name = Debug; 173 | }; 174 | 0361F1421C605FE0009D519A /* Release */ = { 175 | isa = XCBuildConfiguration; 176 | buildSettings = { 177 | ALWAYS_SEARCH_USER_PATHS = NO; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 179 | CLANG_CXX_LIBRARY = "libc++"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_WARN_BOOL_CONVERSION = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 185 | CLANG_WARN_EMPTY_BODY = YES; 186 | CLANG_WARN_ENUM_CONVERSION = YES; 187 | CLANG_WARN_INT_CONVERSION = YES; 188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 189 | CLANG_WARN_UNREACHABLE_CODE = YES; 190 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 191 | CODE_SIGN_IDENTITY = "-"; 192 | COPY_PHASE_STRIP = NO; 193 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 194 | ENABLE_NS_ASSERTIONS = NO; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | GCC_C_LANGUAGE_STANDARD = gnu99; 197 | GCC_NO_COMMON_BLOCKS = YES; 198 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 199 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 200 | GCC_WARN_UNDECLARED_SELECTOR = YES; 201 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 202 | GCC_WARN_UNUSED_FUNCTION = YES; 203 | GCC_WARN_UNUSED_VARIABLE = YES; 204 | MACOSX_DEPLOYMENT_TARGET = 10.11; 205 | MTL_ENABLE_DEBUG_INFO = NO; 206 | SDKROOT = macosx; 207 | }; 208 | name = Release; 209 | }; 210 | 0361F1441C605FE0009D519A /* Debug */ = { 211 | isa = XCBuildConfiguration; 212 | buildSettings = { 213 | PRODUCT_NAME = "$(TARGET_NAME)"; 214 | SWIFT_VERSION = 3.0; 215 | }; 216 | name = Debug; 217 | }; 218 | 0361F1451C605FE0009D519A /* Release */ = { 219 | isa = XCBuildConfiguration; 220 | buildSettings = { 221 | PRODUCT_NAME = "$(TARGET_NAME)"; 222 | SWIFT_VERSION = 3.0; 223 | }; 224 | name = Release; 225 | }; 226 | /* End XCBuildConfiguration section */ 227 | 228 | /* Begin XCConfigurationList section */ 229 | 0361F1371C605FE0009D519A /* Build configuration list for PBXProject "AbandonedStrings" */ = { 230 | isa = XCConfigurationList; 231 | buildConfigurations = ( 232 | 0361F1411C605FE0009D519A /* Debug */, 233 | 0361F1421C605FE0009D519A /* Release */, 234 | ); 235 | defaultConfigurationIsVisible = 0; 236 | defaultConfigurationName = Release; 237 | }; 238 | 0361F1431C605FE0009D519A /* Build configuration list for PBXNativeTarget "AbandonedStrings" */ = { 239 | isa = XCConfigurationList; 240 | buildConfigurations = ( 241 | 0361F1441C605FE0009D519A /* Debug */, 242 | 0361F1451C605FE0009D519A /* Release */, 243 | ); 244 | defaultConfigurationIsVisible = 0; 245 | defaultConfigurationName = Release; 246 | }; 247 | /* End XCConfigurationList section */ 248 | }; 249 | rootObject = 0361F1341C605FE0009D519A /* Project object */; 250 | } 251 | -------------------------------------------------------------------------------- /AbandonedStrings.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AbandonedStrings/main.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | // 3 | // main.swift 4 | // AbandonedStrings 5 | // 6 | // Created by Joshua Smith on 2/1/16. 7 | // Copyright © 2016 iJoshSmith. All rights reserved. 8 | // 9 | 10 | /* 11 | For overview and usage information refer to https://github.com/ijoshsmith/abandoned-strings 12 | */ 13 | 14 | import Foundation 15 | 16 | // MARK: - File processing 17 | 18 | let dispatchGroup = DispatchGroup.init() 19 | let serialWriterQueue = DispatchQueue.init(label: "writer") 20 | 21 | func findFilesIn(_ directories: [String], withExtensions extensions: [String]) -> [String] { 22 | let fileManager = FileManager.default 23 | var files = [String]() 24 | for directory in directories { 25 | guard let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: directory) else { 26 | print("Failed to create enumerator for directory: \(directory)") 27 | return [] 28 | } 29 | while let path = enumerator.nextObject() as? String { 30 | let fileExtension = (path as NSString).pathExtension.lowercased() 31 | if extensions.contains(fileExtension) { 32 | let fullPath = (directory as NSString).appendingPathComponent(path) 33 | files.append(fullPath) 34 | } 35 | } 36 | } 37 | return files 38 | } 39 | 40 | func contentsOfFile(_ filePath: String) -> String { 41 | do { 42 | return try String(contentsOfFile: filePath) 43 | } 44 | catch { 45 | print("cannot read file!!!") 46 | exit(1) 47 | } 48 | } 49 | 50 | func concatenateAllSourceCodeIn(_ directories: [String], withStoryboard: Bool) -> String { 51 | var extensions = ["h", "m", "swift", "jsbundle"] 52 | if withStoryboard { 53 | extensions.append("storyboard") 54 | } 55 | let sourceFiles = findFilesIn(directories, withExtensions: extensions) 56 | return sourceFiles.reduce("") { (accumulator, sourceFile) -> String in 57 | return accumulator + contentsOfFile(sourceFile) 58 | } 59 | } 60 | 61 | // MARK: - Identifier extraction 62 | 63 | let doubleQuote = "\"" 64 | 65 | func extractStringIdentifiersFrom(_ stringsFile: String) -> [String] { 66 | return contentsOfFile(stringsFile) 67 | .components(separatedBy: "\n") 68 | .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } 69 | .filter { $0.hasPrefix(doubleQuote) } 70 | .map { extractStringIdentifierFromTrimmedLine($0) } 71 | } 72 | 73 | func extractStringIdentifierFromTrimmedLine(_ line: String) -> String { 74 | let indexAfterFirstQuote = line.index(after: line.startIndex) 75 | let lineWithoutFirstQuote = line[indexAfterFirstQuote...] 76 | let endIndex = lineWithoutFirstQuote.index(of:"\"")! 77 | let identifier = lineWithoutFirstQuote[.. [String] { 84 | return extractStringIdentifiersFrom(stringsFile).filter { identifier in 85 | let quotedIdentifier = "\"\(identifier)\"" 86 | let quotedIdentifierForStoryboard = "\"@\(identifier)\"" 87 | let signalQuotedIdentifierForJs = "'\(identifier)'" 88 | let isAbandoned = (sourceCode.contains(quotedIdentifier) == false && sourceCode.contains(quotedIdentifierForStoryboard) == false && 89 | sourceCode.contains(signalQuotedIdentifierForJs) == false) 90 | return isAbandoned 91 | } 92 | } 93 | 94 | func stringsFile(_ stringsFile: String, without identifiers: [String]) -> String { 95 | return contentsOfFile(stringsFile) 96 | .components(separatedBy: "\n") 97 | .filter({ (line) in 98 | guard line.hasPrefix(doubleQuote) else { return true } // leave non-strings lines like comments in 99 | let lineIdentifier = extractStringIdentifierFromTrimmedLine(line.trimmingCharacters(in: CharacterSet.whitespaces)) 100 | return identifiers.contains(lineIdentifier) == false 101 | }) 102 | .joined(separator: "\n") 103 | } 104 | 105 | typealias StringsFileToAbandonedIdentifiersMap = [String: [String]] 106 | 107 | func findAbandonedIdentifiersIn(_ rootDirectories: [String], withStoryboard: Bool) -> StringsFileToAbandonedIdentifiersMap { 108 | var map = StringsFileToAbandonedIdentifiersMap() 109 | let sourceCode = concatenateAllSourceCodeIn(rootDirectories, withStoryboard: withStoryboard) 110 | let stringsFiles = findFilesIn(rootDirectories, withExtensions: ["strings"]) 111 | for stringsFile in stringsFiles { 112 | dispatchGroup.enter() 113 | DispatchQueue.global().async { 114 | let abandonedIdentifiers = findStringIdentifiersIn(stringsFile, abandonedBySourceCode: sourceCode) 115 | if abandonedIdentifiers.isEmpty == false { 116 | serialWriterQueue.async { 117 | map[stringsFile] = abandonedIdentifiers 118 | dispatchGroup.leave() 119 | } 120 | } else { 121 | NSLog("\(stringsFile) has no abandonedIdentifiers") 122 | dispatchGroup.leave() 123 | } 124 | } 125 | } 126 | dispatchGroup.wait() 127 | return map 128 | } 129 | 130 | // MARK: - Engine 131 | 132 | func getRootDirectories() -> [String]? { 133 | var c = [String]() 134 | for arg in CommandLine.arguments { 135 | c.append(arg) 136 | } 137 | c.remove(at: 0) 138 | if isOptionalParameterForStoryboardAvailable() { 139 | c.removeLast() 140 | } 141 | if isOptionaParameterForWritingAvailable() { 142 | c.remove(at: c.index(of: "write")!) 143 | } 144 | return c 145 | } 146 | 147 | func isOptionalParameterForStoryboardAvailable() -> Bool { 148 | return CommandLine.arguments.last == "storyboard" 149 | } 150 | 151 | func isOptionaParameterForWritingAvailable() -> Bool { 152 | return CommandLine.arguments.contains("write") 153 | } 154 | 155 | func displayAbandonedIdentifiersInMap(_ map: StringsFileToAbandonedIdentifiersMap) { 156 | for file in map.keys.sorted() { 157 | print("\(file)") 158 | for identifier in map[file]!.sorted() { 159 | print(" \(identifier)") 160 | } 161 | print("") 162 | } 163 | } 164 | 165 | if let rootDirectories = getRootDirectories() { 166 | print("Searching for abandoned resource strings…") 167 | let withStoryboard = isOptionalParameterForStoryboardAvailable() 168 | let map = findAbandonedIdentifiersIn(rootDirectories, withStoryboard: withStoryboard) 169 | if map.isEmpty { 170 | print("No abandoned resource strings were detected.") 171 | } 172 | else { 173 | print("Abandoned resource strings were detected:") 174 | displayAbandonedIdentifiersInMap(map) 175 | 176 | if isOptionaParameterForWritingAvailable() { 177 | map.keys.forEach { (stringsFilePath) in 178 | print("\n\nNow modifying \(stringsFilePath) ...") 179 | let updatedStringsFileContent = stringsFile(stringsFilePath, without: map[stringsFilePath]!) 180 | do { 181 | try updatedStringsFileContent.write(toFile: stringsFilePath, atomically: true, encoding: .utf8) 182 | } catch { 183 | print("ERROR writing file: \(stringsFilePath)") 184 | } 185 | } 186 | } 187 | } 188 | } else { 189 | print("Please provide the root directory for source code files as a command line argument.") 190 | } 191 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Smith 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abandoned Resource String Detection 2 | This command line program detects unused resource strings in an iOS or OS X application. 3 | 4 | Updated to Swift 3, thanks to @astaeck on Oct-17-2016 5 | 6 | ## Usage 7 | Open a Terminal to the directory which contains the *AbandonedStrings* executable, and run the following command: 8 | 9 | `$ ./AbandonedStrings /Users/your-username/path/to/source/code` 10 | 11 | ## What to expect 12 | If a `.strings` file contains… 13 | 14 | `"some_string_identifier" = "Some Display Text";` 15 | 16 | …this program will consider that resource string to be abandoned if… 17 | 18 | `"some_string_identifier"` 19 | 20 | …is not found in any of the source code files (namely, files with a `.h`, `.m`, `.swift` or `.jsbundle` extension). 21 | 22 | ## More details 23 | This program searches through the source code files in an iOS app project, looking for resource strings (in a `.strings` file) whose identifiers are not referenced by the application's source code. 24 | 25 | The search logic does not take into account if code is commented out, so it won't be as reliable if your application has a lot of commented-out code. 26 | 27 | It also does not try to determine the context in which string identifiers are used, such as whether or not the string is being used to look up a localized string value or if it just happens to match a resource string identifier by coincidence. 28 | 29 | Also, this program is ineffective if resource string identifiers are referenced via constants or dynamically constructed. 30 | 31 | ## Disclaimer 32 | As noted above, this program uses a simple heuristic and is not guaranteed to produce perfect results for every codebase. 33 | --------------------------------------------------------------------------------