├── .github └── FUNDING.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Example └── LocalizeExample │ ├── LocalizeExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── LaunchImage.launchimage │ │ │ └── Contents.json │ ├── Info.plist │ ├── Languages │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ ├── es.lproj │ │ │ └── Localizable.strings │ │ └── fr.lproj │ │ │ └── Localizable.strings │ └── Storyboards │ │ └── LaunchScreen.storyboard │ └── Sources │ ├── AppDelegate.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ └── ViewController.swift ├── LICENSE ├── Localize.swift ├── README.md ├── banner.png └── xcodeScreenshot.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: freshos 2 | github: s4cha 3 | -------------------------------------------------------------------------------- /.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 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 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/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sachadso@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Example/LocalizeExample/LocalizeExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 994630691FAC68410004A9A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994630681FAC68410004A9A2 /* AppDelegate.swift */; }; 11 | 9946306B1FAC68410004A9A2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9946306A1FAC68410004A9A2 /* ViewController.swift */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 994630651FAC68410004A9A2 /* LocalizeExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LocalizeExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 994630681FAC68410004A9A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 17 | 9946306A1FAC68410004A9A2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 18 | 99EB54961FAC6D3300AD728C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 19 | 99EB54981FAC6D3300AD728C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 20 | 99EB549B1FAC6D3300AD728C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 21 | 99EB549C1FAC6D3300AD728C /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 22 | 99EB549D1FAC6D3300AD728C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 23 | 99EB549E1FAC6D3300AD728C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | 994630621FAC68410004A9A2 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 9946305C1FAC68410004A9A2 = { 38 | isa = PBXGroup; 39 | children = ( 40 | 99EB54941FAC6D3300AD728C /* Resources */, 41 | 994630671FAC68410004A9A2 /* Sources */, 42 | 994630661FAC68410004A9A2 /* Products */, 43 | ); 44 | sourceTree = ""; 45 | }; 46 | 994630661FAC68410004A9A2 /* Products */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | 994630651FAC68410004A9A2 /* LocalizeExample.app */, 50 | ); 51 | name = Products; 52 | sourceTree = ""; 53 | }; 54 | 994630671FAC68410004A9A2 /* Sources */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 994630681FAC68410004A9A2 /* AppDelegate.swift */, 58 | 9946306A1FAC68410004A9A2 /* ViewController.swift */, 59 | ); 60 | path = Sources; 61 | sourceTree = ""; 62 | }; 63 | 99EB54941FAC6D3300AD728C /* Resources */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 99EB549E1FAC6D3300AD728C /* Info.plist */, 67 | 99EB54951FAC6D3300AD728C /* Storyboards */, 68 | 99EB54981FAC6D3300AD728C /* Assets.xcassets */, 69 | 99EB54991FAC6D3300AD728C /* Languages */, 70 | ); 71 | path = Resources; 72 | sourceTree = SOURCE_ROOT; 73 | }; 74 | 99EB54951FAC6D3300AD728C /* Storyboards */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 99EB54961FAC6D3300AD728C /* LaunchScreen.storyboard */, 78 | ); 79 | path = Storyboards; 80 | sourceTree = ""; 81 | }; 82 | 99EB54991FAC6D3300AD728C /* Languages */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 99EB549A1FAC6D3300AD728C /* Localizable.strings */, 86 | ); 87 | path = Languages; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 994630641FAC68410004A9A2 /* LocalizeExample */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 994630771FAC68410004A9A2 /* Build configuration list for PBXNativeTarget "LocalizeExample" */; 96 | buildPhases = ( 97 | 994630611FAC68410004A9A2 /* Sources */, 98 | 994630621FAC68410004A9A2 /* Frameworks */, 99 | 994630631FAC68410004A9A2 /* Resources */, 100 | 99EB549F1FAC6DF600AD728C /* Localization Run Script */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = LocalizeExample; 107 | productName = LocalizeExample; 108 | productReference = 994630651FAC68410004A9A2 /* LocalizeExample.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 9946305D1FAC68410004A9A2 /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastSwiftUpdateCheck = 0900; 118 | LastUpgradeCheck = 0900; 119 | ORGANIZATIONNAME = freshos; 120 | TargetAttributes = { 121 | 994630641FAC68410004A9A2 = { 122 | CreatedOnToolsVersion = 9.0; 123 | LastSwiftMigration = 1140; 124 | ProvisioningStyle = Automatic; 125 | }; 126 | }; 127 | }; 128 | buildConfigurationList = 994630601FAC68410004A9A2 /* Build configuration list for PBXProject "LocalizeExample" */; 129 | compatibilityVersion = "Xcode 8.0"; 130 | developmentRegion = en; 131 | hasScannedForEncodings = 0; 132 | knownRegions = ( 133 | en, 134 | Base, 135 | es, 136 | fr, 137 | ); 138 | mainGroup = 9946305C1FAC68410004A9A2; 139 | productRefGroup = 994630661FAC68410004A9A2 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | 994630641FAC68410004A9A2 /* LocalizeExample */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | 994630631FAC68410004A9A2 /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXShellScriptBuildPhase section */ 159 | 99EB549F1FAC6DF600AD728C /* Localization Run Script */ = { 160 | isa = PBXShellScriptBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | ); 164 | inputPaths = ( 165 | ); 166 | name = "Localization Run Script"; 167 | outputPaths = ( 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | shellPath = /bin/sh; 171 | shellScript = "${SRCROOT}/../../Localize.swift\n"; 172 | }; 173 | /* End PBXShellScriptBuildPhase section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | 994630611FAC68410004A9A2 /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 9946306B1FAC68410004A9A2 /* ViewController.swift in Sources */, 181 | 994630691FAC68410004A9A2 /* AppDelegate.swift in Sources */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | /* End PBXSourcesBuildPhase section */ 186 | 187 | /* Begin PBXVariantGroup section */ 188 | 99EB549A1FAC6D3300AD728C /* Localizable.strings */ = { 189 | isa = PBXVariantGroup; 190 | children = ( 191 | 99EB549B1FAC6D3300AD728C /* en */, 192 | 99EB549C1FAC6D3300AD728C /* es */, 193 | 99EB549D1FAC6D3300AD728C /* fr */, 194 | ); 195 | name = Localizable.strings; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | 994630751FAC68410004A9A2 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 209 | CLANG_CXX_LIBRARY = "libc++"; 210 | CLANG_ENABLE_MODULES = YES; 211 | CLANG_ENABLE_OBJC_ARC = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 226 | CLANG_WARN_STRICT_PROTOTYPES = YES; 227 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 229 | CLANG_WARN_UNREACHABLE_CODE = YES; 230 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 231 | CODE_SIGN_IDENTITY = "iPhone Developer"; 232 | COPY_PHASE_STRIP = NO; 233 | DEBUG_INFORMATION_FORMAT = dwarf; 234 | ENABLE_STRICT_OBJC_MSGSEND = YES; 235 | ENABLE_TESTABILITY = YES; 236 | GCC_C_LANGUAGE_STANDARD = gnu11; 237 | GCC_DYNAMIC_NO_PIC = NO; 238 | GCC_NO_COMMON_BLOCKS = YES; 239 | GCC_OPTIMIZATION_LEVEL = 0; 240 | GCC_PREPROCESSOR_DEFINITIONS = ( 241 | "DEBUG=1", 242 | "$(inherited)", 243 | ); 244 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 245 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 246 | GCC_WARN_UNDECLARED_SELECTOR = YES; 247 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 248 | GCC_WARN_UNUSED_FUNCTION = YES; 249 | GCC_WARN_UNUSED_VARIABLE = YES; 250 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 251 | MTL_ENABLE_DEBUG_INFO = YES; 252 | ONLY_ACTIVE_ARCH = YES; 253 | SDKROOT = iphoneos; 254 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 255 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 256 | }; 257 | name = Debug; 258 | }; 259 | 994630761FAC68410004A9A2 /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 264 | CLANG_ANALYZER_NONNULL = YES; 265 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 266 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 267 | CLANG_CXX_LIBRARY = "libc++"; 268 | CLANG_ENABLE_MODULES = YES; 269 | CLANG_ENABLE_OBJC_ARC = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 275 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 276 | CLANG_WARN_EMPTY_BODY = YES; 277 | CLANG_WARN_ENUM_CONVERSION = YES; 278 | CLANG_WARN_INFINITE_RECURSION = YES; 279 | CLANG_WARN_INT_CONVERSION = YES; 280 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 284 | CLANG_WARN_STRICT_PROTOTYPES = YES; 285 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 286 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | CODE_SIGN_IDENTITY = "iPhone Developer"; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu11; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 303 | MTL_ENABLE_DEBUG_INFO = NO; 304 | SDKROOT = iphoneos; 305 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 306 | VALIDATE_PRODUCT = YES; 307 | }; 308 | name = Release; 309 | }; 310 | 994630781FAC68410004A9A2 /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 315 | CODE_SIGN_STYLE = Automatic; 316 | INFOPLIST_FILE = Resources/Info.plist; 317 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 318 | PRODUCT_BUNDLE_IDENTIFIER = com.freshos.LocalizeExample; 319 | PRODUCT_NAME = "$(TARGET_NAME)"; 320 | SWIFT_VERSION = 5.0; 321 | TARGETED_DEVICE_FAMILY = "1,2"; 322 | }; 323 | name = Debug; 324 | }; 325 | 994630791FAC68410004A9A2 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 329 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 330 | CODE_SIGN_STYLE = Automatic; 331 | INFOPLIST_FILE = Resources/Info.plist; 332 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 333 | PRODUCT_BUNDLE_IDENTIFIER = com.freshos.LocalizeExample; 334 | PRODUCT_NAME = "$(TARGET_NAME)"; 335 | SWIFT_VERSION = 5.0; 336 | TARGETED_DEVICE_FAMILY = "1,2"; 337 | }; 338 | name = Release; 339 | }; 340 | /* End XCBuildConfiguration section */ 341 | 342 | /* Begin XCConfigurationList section */ 343 | 994630601FAC68410004A9A2 /* Build configuration list for PBXProject "LocalizeExample" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 994630751FAC68410004A9A2 /* Debug */, 347 | 994630761FAC68410004A9A2 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | 994630771FAC68410004A9A2 /* Build configuration list for PBXNativeTarget "LocalizeExample" */ = { 353 | isa = XCConfigurationList; 354 | buildConfigurations = ( 355 | 994630781FAC68410004A9A2 /* Debug */, 356 | 994630791FAC68410004A9A2 /* Release */, 357 | ); 358 | defaultConfigurationIsVisible = 0; 359 | defaultConfigurationName = Release; 360 | }; 361 | /* End XCConfigurationList section */ 362 | }; 363 | rootObject = 9946305D1FAC68410004A9A2 /* Project object */; 364 | } 365 | -------------------------------------------------------------------------------- /Example/LocalizeExample/LocalizeExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "11.0", 8 | "subtype" : "2436h", 9 | "scale" : "3x" 10 | }, 11 | { 12 | "orientation" : "landscape", 13 | "idiom" : "iphone", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "11.0", 16 | "subtype" : "2436h", 17 | "scale" : "3x" 18 | }, 19 | { 20 | "orientation" : "portrait", 21 | "idiom" : "iphone", 22 | "extent" : "full-screen", 23 | "minimum-system-version" : "8.0", 24 | "subtype" : "736h", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "orientation" : "landscape", 29 | "idiom" : "iphone", 30 | "extent" : "full-screen", 31 | "minimum-system-version" : "8.0", 32 | "subtype" : "736h", 33 | "scale" : "3x" 34 | }, 35 | { 36 | "orientation" : "portrait", 37 | "idiom" : "iphone", 38 | "extent" : "full-screen", 39 | "minimum-system-version" : "8.0", 40 | "subtype" : "667h", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "orientation" : "portrait", 45 | "idiom" : "iphone", 46 | "extent" : "full-screen", 47 | "minimum-system-version" : "7.0", 48 | "scale" : "2x" 49 | }, 50 | { 51 | "orientation" : "portrait", 52 | "idiom" : "iphone", 53 | "extent" : "full-screen", 54 | "minimum-system-version" : "7.0", 55 | "subtype" : "retina4", 56 | "scale" : "2x" 57 | }, 58 | { 59 | "orientation" : "portrait", 60 | "idiom" : "ipad", 61 | "extent" : "full-screen", 62 | "minimum-system-version" : "7.0", 63 | "scale" : "1x" 64 | }, 65 | { 66 | "orientation" : "landscape", 67 | "idiom" : "ipad", 68 | "extent" : "full-screen", 69 | "minimum-system-version" : "7.0", 70 | "scale" : "1x" 71 | }, 72 | { 73 | "orientation" : "portrait", 74 | "idiom" : "ipad", 75 | "extent" : "full-screen", 76 | "minimum-system-version" : "7.0", 77 | "scale" : "2x" 78 | }, 79 | { 80 | "orientation" : "landscape", 81 | "idiom" : "ipad", 82 | "extent" : "full-screen", 83 | "minimum-system-version" : "7.0", 84 | "scale" : "2x" 85 | }, 86 | { 87 | "orientation" : "portrait", 88 | "idiom" : "iphone", 89 | "extent" : "full-screen", 90 | "scale" : "1x" 91 | }, 92 | { 93 | "orientation" : "portrait", 94 | "idiom" : "iphone", 95 | "extent" : "full-screen", 96 | "scale" : "2x" 97 | }, 98 | { 99 | "orientation" : "portrait", 100 | "idiom" : "iphone", 101 | "extent" : "full-screen", 102 | "subtype" : "retina4", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "orientation" : "portrait", 107 | "idiom" : "ipad", 108 | "extent" : "to-status-bar", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "orientation" : "portrait", 113 | "idiom" : "ipad", 114 | "extent" : "full-screen", 115 | "scale" : "1x" 116 | }, 117 | { 118 | "orientation" : "landscape", 119 | "idiom" : "ipad", 120 | "extent" : "to-status-bar", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "orientation" : "landscape", 125 | "idiom" : "ipad", 126 | "extent" : "full-screen", 127 | "scale" : "1x" 128 | }, 129 | { 130 | "orientation" : "portrait", 131 | "idiom" : "ipad", 132 | "extent" : "to-status-bar", 133 | "scale" : "2x" 134 | }, 135 | { 136 | "orientation" : "portrait", 137 | "idiom" : "ipad", 138 | "extent" : "full-screen", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "orientation" : "landscape", 143 | "idiom" : "ipad", 144 | "extent" : "to-status-bar", 145 | "scale" : "2x" 146 | }, 147 | { 148 | "orientation" : "landscape", 149 | "idiom" : "ipad", 150 | "extent" : "full-screen", 151 | "scale" : "2x" 152 | } 153 | ], 154 | "info" : { 155 | "version" : 1, 156 | "author" : "xcode" 157 | } 158 | } -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIcons 10 | 11 | CFBundleIcons~ipad 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Languages/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "IgnoredUntranslatedKey" = "OK"; 2 | "MissingKey" = "Hey I'm not present in Spanish!"; 3 | "NeverUsedKey" = "Hey I'm never used so you can just get rid of me!"; 4 | "UntranslatedKey" = "Hey I'm always in english so you need to translate me!"; 5 | "DuplicatedKey" = "Hey, I'm here more than once!"; 6 | "DuplicatedKey" = "Hey, I'm here more than once!"; 7 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Languages/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "IgnoredUntranslatedKey" = "OK"; //ignore-same-translation-warning 2 | "NeverUsedKey" = "Hello I'm never used so you can just get rid of me!"; 3 | "UntranslatedKey" = "Hey I'm always in english so you need to translate me!"; 4 | "DuplicatedKey" = "Hey, I'm more than once in the master translations!"; 5 | "RedundantKey" = "Hi, I'm a redundant key only appearing in Spanish"; 6 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Languages/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "IgnoredUntranslatedKey" = "OK"; //ignore-same-translation-warning 2 | "MissingKey" = "Hey! je n'existe pas en espagnol!"; 3 | "NeverUsedKey" = "Bonjour I'm never used so you can just get rid of me!"; 4 | "DuplicatedKey" = "Hey, I'm more than once in the master translations!"; 5 | "UntranslatedKey" = "Hey I'm always in english so you need to translate me!"; 6 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Resources/Storyboards/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 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LocalizeExample 4 | // 5 | // Created by Sacha DSO on 03/11/2017. 6 | // Copyright © 2017 freshos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/LocalizeExample/Sources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/LocalizeExample/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // LocalizeExample 4 | // 5 | // Created by Sacha DSO on 03/11/2017. 6 | // Copyright © 2017 freshos. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Here simulates their usage in code 16 | _ = NSLocalizedString("UntranslatedKey", comment: "") 17 | _ = NSLocalizedString("IgnoredUntranslatedKey", comment: "") 18 | _ = NSLocalizedString("MissingKey", comment: "") 19 | _ = NSLocalizedString("DuplicatedKey", comment: "") 20 | _ = NSLocalizedString("MissingKeyFromMain", comment: "This key is not present in master translation file") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 S4cha 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 | -------------------------------------------------------------------------------- /Localize.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun --sdk macosx swift 2 | 3 | import Foundation 4 | 5 | // WHAT 6 | // 1. Find Missing keys in other Localisation files 7 | // 2. Find potentially untranslated keys 8 | // 3. Find Duplicate keys 9 | // 4. Find Unused keys and generate script to delete them all at once 10 | 11 | // MARK: Start Of Configurable Section 12 | 13 | /* 14 | You can enable or disable the script whenever you want 15 | */ 16 | let enabled = true 17 | 18 | /* 19 | Put your path here, example -> Resources/Localizations/Languages 20 | */ 21 | let relativeLocalizableFolders = "/Resources/Languages" 22 | 23 | /* 24 | This is the path of your source folder which will be used in searching 25 | for the localization keys you actually use in your project 26 | */ 27 | let relativeSourceFolder = "/Sources" 28 | 29 | /* 30 | Those are the regex patterns to recognize localizations. 31 | */ 32 | let patterns = [ 33 | "NSLocalized(Format)?String\\(\\s*@?\"([\\w\\.]+)\"", // Swift and Objc Native 34 | "Localizations\\.((?:[A-Z]{1}[a-z]*[A-z]*)*(?:\\.[A-Z]{1}[a-z]*[A-z]*)*)", // Laurine Calls 35 | "L10n.tr\\(key: \"(\\w+)\"", // SwiftGen generation 36 | "ypLocalized\\(\"(.*)\"\\)", 37 | "\"(.*)\".localized" // "key".localized pattern 38 | ] 39 | 40 | /* 41 | Those are the keys you don't want to be recognized as "unused" 42 | For instance, Keys that you concatenate will not be detected by the parsing 43 | so you want to add them here in order not to create false positives :) 44 | */ 45 | let ignoredFromUnusedKeys: [String] = [] 46 | /* example 47 | let ignoredFromUnusedKeys = [ 48 | "NotificationNoOne", 49 | "NotificationCommentPhoto", 50 | "NotificationCommentHisPhoto", 51 | "NotificationCommentHerPhoto" 52 | ] 53 | */ 54 | 55 | let masterLanguage = "en" 56 | 57 | /* 58 | Sanitizing files will remove comments, empty lines and order your keys alphabetically. 59 | */ 60 | let sanitizeFiles = false 61 | 62 | /* 63 | Determines if there are multiple localizations or not. 64 | */ 65 | let singleLanguage = false 66 | 67 | /* 68 | Determines if we should show errors if there's a key within the app 69 | that does not appear in master translations. 70 | */ 71 | let checkForUntranslated = true 72 | 73 | // MARK: End Of Configurable Section 74 | // MARK: - 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | if enabled == false { 87 | print("Localization check cancelled") 88 | exit(000) 89 | } 90 | 91 | // Detect list of supported languages automatically 92 | func listSupportedLanguages() -> [String] { 93 | var sl: [String] = [] 94 | let path = FileManager.default.currentDirectoryPath + relativeLocalizableFolders 95 | if !FileManager.default.fileExists(atPath: path) { 96 | print("Invalid configuration: \(path) does not exist.") 97 | exit(1) 98 | } 99 | let enumerator = FileManager.default.enumerator(atPath: path) 100 | let extensionName = "lproj" 101 | print("Found these languages:") 102 | while let element = enumerator?.nextObject() as? String { 103 | if element.hasSuffix(extensionName) { 104 | print(element) 105 | let name = element.replacingOccurrences(of: ".\(extensionName)", with: "") 106 | sl.append(name) 107 | } 108 | } 109 | return sl 110 | } 111 | 112 | let supportedLanguages = listSupportedLanguages() 113 | var ignoredFromSameTranslation: [String: [String]] = [:] 114 | let path = FileManager.default.currentDirectoryPath + relativeLocalizableFolders 115 | var numberOfWarnings = 0 116 | var numberOfErrors = 0 117 | 118 | struct LocalizationFiles { 119 | var name = "" 120 | var keyValue: [String: String] = [:] 121 | var linesNumbers: [String: Int] = [:] 122 | 123 | init(name: String) { 124 | self.name = name 125 | process() 126 | } 127 | 128 | mutating func process() { 129 | if sanitizeFiles { 130 | removeCommentsFromFile() 131 | removeEmptyLinesFromFile() 132 | sortLinesAlphabetically() 133 | } 134 | let location = singleLanguage ? "\(path)/Localizable.strings" : "\(path)/\(name).lproj/Localizable.strings" 135 | guard let string = try? String(contentsOfFile: location, encoding: .utf8) else { 136 | return 137 | } 138 | 139 | let lines = string.components(separatedBy: .newlines) 140 | keyValue = [:] 141 | 142 | let pattern = "\"(.*)\" = \"(.+)\";" 143 | let regex = try? NSRegularExpression(pattern: pattern, options: []) 144 | var ignoredTranslation: [String] = [] 145 | 146 | for (lineNumber, line) in lines.enumerated() { 147 | let range = NSRange(location: 0, length: (line as NSString).length) 148 | 149 | // Ignored pattern 150 | let ignoredPattern = "\"(.*)\" = \"(.+)\"; *\\/\\/ *ignore-same-translation-warning" 151 | let ignoredRegex = try? NSRegularExpression(pattern: ignoredPattern, options: []) 152 | if let ignoredMatch = ignoredRegex?.firstMatch(in: line, 153 | options: [], 154 | range: range) { 155 | let key = (line as NSString).substring(with: ignoredMatch.range(at: 1)) 156 | ignoredTranslation.append(key) 157 | } 158 | 159 | if let firstMatch = regex?.firstMatch(in: line, options: [], range: range) { 160 | let key = (line as NSString).substring(with: firstMatch.range(at: 1)) 161 | let value = (line as NSString).substring(with: firstMatch.range(at: 2)) 162 | 163 | if keyValue[key] != nil { 164 | let str = "\(path)/\(name).lproj" 165 | + "/Localizable.strings:\(linesNumbers[key]!): " 166 | + "error: [Duplication] \"\(key)\" " 167 | + "is duplicated in \(name.uppercased()) file" 168 | print(str) 169 | numberOfErrors += 1 170 | } else { 171 | keyValue[key] = value 172 | linesNumbers[key] = lineNumber + 1 173 | } 174 | } 175 | } 176 | print(ignoredFromSameTranslation) 177 | ignoredFromSameTranslation[name] = ignoredTranslation 178 | } 179 | 180 | func rebuildFileString(from lines: [String]) -> String { 181 | return lines.reduce("") { (r: String, s: String) -> String in 182 | (r == "") ? (r + s) : (r + "\n" + s) 183 | } 184 | } 185 | 186 | func removeEmptyLinesFromFile() { 187 | let location = "\(path)/\(name).lproj/Localizable.strings" 188 | if let string = try? String(contentsOfFile: location, encoding: .utf8) { 189 | var lines = string.components(separatedBy: .newlines) 190 | lines = lines.filter { $0.trimmingCharacters(in: .whitespaces) != "" } 191 | let s = rebuildFileString(from: lines) 192 | try? s.write(toFile: location, atomically: false, encoding: .utf8) 193 | } 194 | } 195 | 196 | func removeCommentsFromFile() { 197 | let location = "\(path)/\(name).lproj/Localizable.strings" 198 | if let string = try? String(contentsOfFile: location, encoding: .utf8) { 199 | var lines = string.components(separatedBy: .newlines) 200 | lines = lines.filter { !$0.hasPrefix("//") } 201 | let s = rebuildFileString(from: lines) 202 | try? s.write(toFile: location, atomically: false, encoding: .utf8) 203 | } 204 | } 205 | 206 | func sortLinesAlphabetically() { 207 | let location = "\(path)/\(name).lproj/Localizable.strings" 208 | if let string = try? String(contentsOfFile: location, encoding: .utf8) { 209 | let lines = string.components(separatedBy: .newlines) 210 | 211 | var s = "" 212 | for (i, l) in sortAlphabetically(lines).enumerated() { 213 | s += l 214 | if i != lines.count - 1 { 215 | s += "\n" 216 | } 217 | } 218 | try? s.write(toFile: location, atomically: false, encoding: .utf8) 219 | } 220 | } 221 | 222 | func removeEmptyLinesFromLines(_ lines: [String]) -> [String] { 223 | return lines.filter { $0.trimmingCharacters(in: .whitespaces) != "" } 224 | } 225 | 226 | func sortAlphabetically(_ lines: [String]) -> [String] { 227 | return lines.sorted() 228 | } 229 | } 230 | 231 | // MARK: - Load Localisation Files in memory 232 | 233 | let masterLocalizationFile = LocalizationFiles(name: masterLanguage) 234 | let localizationFiles = supportedLanguages 235 | .filter { $0 != masterLanguage } 236 | .map { LocalizationFiles(name: $0) } 237 | 238 | // MARK: - Detect Unused Keys 239 | 240 | let sourcesPath = FileManager.default.currentDirectoryPath + relativeSourceFolder 241 | let fileManager = FileManager.default 242 | let enumerator = fileManager.enumerator(atPath: sourcesPath) 243 | var localizedStrings: [String] = [] 244 | while let swiftFileLocation = enumerator?.nextObject() as? String { 245 | // checks the extension 246 | if swiftFileLocation.hasSuffix(".swift") || swiftFileLocation.hasSuffix(".m") || swiftFileLocation.hasSuffix(".mm") { 247 | let location = "\(sourcesPath)/\(swiftFileLocation)" 248 | if let string = try? String(contentsOfFile: location, encoding: .utf8) { 249 | for p in patterns { 250 | let regex = try? NSRegularExpression(pattern: p, options: []) 251 | let range = NSRange(location: 0, length: (string as NSString).length) // Obj c wa 252 | regex?.enumerateMatches(in: string, 253 | options: [], 254 | range: range, 255 | using: { result, _, _ in 256 | if let r = result { 257 | let value = (string as NSString).substring(with: r.range(at: r.numberOfRanges - 1)) 258 | localizedStrings.append(value) 259 | } 260 | }) 261 | } 262 | } 263 | } 264 | } 265 | 266 | var masterKeys = Set(masterLocalizationFile.keyValue.keys) 267 | let usedKeys = Set(localizedStrings) 268 | let ignored = Set(ignoredFromUnusedKeys) 269 | let unused = masterKeys.subtracting(usedKeys).subtracting(ignored) 270 | let untranslated = usedKeys.subtracting(masterKeys) 271 | 272 | // Here generate Xcode regex Find and replace script to remove dead keys all at once! 273 | var replaceCommand = "\"(" 274 | var counter = 0 275 | for v in unused { 276 | var str = "\(path)/\(masterLocalizationFile.name).lproj/Localizable.strings:\(masterLocalizationFile.linesNumbers[v]!): " 277 | str += "error: [Unused Key] \"\(v)\" is never used" 278 | print(str) 279 | numberOfErrors += 1 280 | if counter != 0 { 281 | replaceCommand += "|" 282 | } 283 | replaceCommand += v 284 | if counter == unused.count - 1 { 285 | replaceCommand += ")\" = \".*\";" 286 | } 287 | counter += 1 288 | } 289 | 290 | print(replaceCommand) 291 | 292 | // MARK: - Compare each translation file against master (en) 293 | 294 | for file in localizationFiles { 295 | for k in masterLocalizationFile.keyValue.keys { 296 | if let v = file.keyValue[k] { 297 | if v == masterLocalizationFile.keyValue[k] { 298 | if !ignoredFromSameTranslation[file.name]!.contains(k) { 299 | let str = "\(path)/\(file.name).lproj/Localizable.strings" 300 | + ":\(file.linesNumbers[k]!): " 301 | + "warning: [Potentially Untranslated] \"\(k)\"" 302 | + "in \(file.name.uppercased()) file doesn't seem to be localized" 303 | print(str) 304 | numberOfWarnings += 1 305 | } 306 | } 307 | } else { 308 | var str = "\(path)/\(file.name).lproj/Localizable.strings:\(masterLocalizationFile.linesNumbers[k]!): " 309 | str += "error: [Missing] \"\(k)\" missing from \(file.name.uppercased()) file" 310 | print(str) 311 | numberOfErrors += 1 312 | } 313 | } 314 | 315 | let redundantKeys = file.keyValue.keys.filter { !masterLocalizationFile.keyValue.keys.contains($0) } 316 | 317 | for k in redundantKeys { 318 | let str = "\(path)/\(file.name).lproj/Localizable.strings:\(file.linesNumbers[k]!): " 319 | + "error: [Redundant key] \"\(k)\" redundant in \(file.name.uppercased()) file" 320 | 321 | print(str) 322 | } 323 | } 324 | 325 | if checkForUntranslated { 326 | for key in untranslated { 327 | var str = "\(path)/\(masterLocalizationFile.name).lproj/Localizable.strings:1: " 328 | str += "error: [Missing Translation] \(key) is not translated" 329 | 330 | print(str) 331 | numberOfErrors += 1 332 | } 333 | } 334 | 335 | print("Number of warnings : \(numberOfWarnings)") 336 | print("Number of errors : \(numberOfErrors)") 337 | 338 | if numberOfErrors > 0 { 339 | exit(1) 340 | } 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Localize](https://raw.githubusercontent.com/s4cha/Localize/master/banner.png) 2 | 3 | # Localize 4 | [![Language: Swift](https://img.shields.io/badge/language-swift-f48041.svg?style=flat)](https://developer.apple.com/swift) 5 | ![Platform: iOS](https://img.shields.io/badge/platform-iOS-blue.svg?style=flat) 6 | [![codebeat badge](https://codebeat.co/badges/1eefc440-01e1-4408-bc92-1b8eeab9796c)](https://codebeat.co/projects/github-com-freshos-localize-master) 7 | [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/s4cha/Localize/blob/master/LICENSE) 8 | [![Release version](https://img.shields.io/badge/release-0.1-blue.svg)]() 9 | 10 | *Localize* is a tiny run script that keeps your `Localizable.strings` files clean and emits warnings when something is suspicious. 11 | 12 | 13 | ![Localize](https://raw.githubusercontent.com/s4cha/Localize/master/xcodeScreenshot.png) 14 | 15 | 16 | Because **Localization** files tend to **rot over time** and become a hassle to work with. **Stressful** when you have to test your app against many different Localizations. 17 | 18 | ## Try it! 19 | 20 | Localize is part of [freshOS](https://freshos.github.io/) iOS toolset. Try it out in the included example app! 21 | 22 | ## How 23 | By using a **script** running automatically, you have a **safety net** keeping them **sane**, checking for **translation issues** and preventing then to **rot over time.** 24 | 25 | ## What 26 | Automatically (On build) 27 | - [x] **Cleans** you localization files (removes spaces) 28 | - [x] **Sorts** keys Alphabetically 29 | - [x] Checks for **Unused Keys** 30 | - [x] Checks for **Missing Keys** 31 | - [x] Checks for **Untranslated** (which you can ignore with a flag) 32 | - [x] Checks for **Redundant Keys** 33 | - [x] Checks for **Duplicate Keys** 34 | 35 | ## Installation 36 | 37 | Add the following `Run Script` in your project's `Build Phases` in XCode, this will run the script at every build. 38 | Use the path of where you copied `Localize.swift` script. 39 | 40 | ```shell 41 | ${SRCROOT}/{REPLACE ME}} # e.g. ${SRCROOT}/Libs/Localize.swift 42 | ``` 43 | Run and Enjoy \o/ 44 | 45 | ## Configuration 46 | Configure the top section of the `Localize.swift` according to your project. 47 | 48 | ## More 49 | 50 | ### Ignore [Potentially Untranslated] warnings 51 | Just Add `//ignore-same-translation-warning` next to the translation. 52 | Example : 53 | ``` 54 | "PhotoKey" = "Photo"; //ignore-same-translation-warning 55 | ``` 56 | This will take care of ignoring `[Potentially Untranslated] "XXX" in FR file doesn't seem to be localized` 57 | 58 | ### Unused false positive 59 | 60 | #### Not found by the script reason 1 61 | The script parses your project sources and checks if your keys are called within `NSLocalizedString` calls. 62 | But chances are you have a helper for a shorter NSLocalizedString syntax. 63 | This is indeed supported but you have to give the script what to look for. 64 | 65 | You can modify the script to include other ways of localizations: 66 | 67 | ```swift 68 | let patterns = [ 69 | "NSLocalizedString\\(@?\"(\\w+)\"", // Swift and Objc Native 70 | "Localizations\\.((?:[A-Z]{1}[a-z]*[A-z]*)*(?:\\.[A-Z]{1}[a-z]*[A-z]*)*)", // Laurine Calls 71 | //Add your own matching regex here 72 | "fsLocalized\\(@?\"(\\w+)\"" 73 | ] 74 | ``` 75 | 76 | #### Not found by the script reason 2 77 | Another common pattern is to have keys being built at runtime. 78 | Of course those keys are not present at compile time so the script can't know about them and emits false positive errors. 79 | You can add those keys at the top of of the script to prevent this from happening: 80 | 81 | ```swift 82 | let ignoredFromUnusedKeys = [ 83 | "NotificationNoOne", 84 | "NotificationCommentPhoto", 85 | "NotificationCommentHisPhoto", 86 | "NotificationCommentHerPhoto" 87 | ] 88 | ``` 89 | 90 | ## Author 91 | 92 | Sacha Durand Saint Omer, sachadso@gmail.com 93 | 94 | ## Contributors 95 | [JuliusBahr](https://github.com/JuliusBahr), [ezisazis](https://github.com/ezisazis/), [michalsrutek](https://github.com/michalsrutek/) 96 | 97 | ## Contributing 98 | 99 | Contributions to Localize are very welcomed and encouraged! 100 | 101 | ## License 102 | 103 | Localize is available under the MIT license. See [LICENSE](https://github.com/s4cha/Localize/blob/master/LICENSE) for more information. 104 | 105 | 106 | # Backers 107 | Like the project? Offer coffee or support us with a monthly donation and help us continue our activities :) 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 | ### Sponsors 141 | Become a sponsor and get your logo on our README on Github with a link to your site :) 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 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshOS/Localize/d16bb0a7eed421b746fc6204639f436e7b299e07/banner.png -------------------------------------------------------------------------------- /xcodeScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshOS/Localize/d16bb0a7eed421b746fc6204639f436e7b299e07/xcodeScreenshot.png --------------------------------------------------------------------------------