From c384cc4b0d322ff6b5f11ecf63c48397b078ce39 Mon Sep 17 00:00:00 2001 From: Almer Lucke Date: Fri, 11 Sep 2015 13:47:01 +0200 Subject: [PATCH] Initial commit --- .gitignore | 45 ++ FCGenstrings.xcodeproj/project.pbxproj | 257 +++++++++++ .../contents.xcworkspacedata | 7 + FCGenstrings/main.m | 19 + LICENSE.txt | 21 + README.md | 49 ++ Source/FCGenstrings.h | 17 + Source/FCGenstrings.m | 434 ++++++++++++++++++ 8 files changed, 849 insertions(+) create mode 100644 .gitignore create mode 100644 FCGenstrings.xcodeproj/project.pbxproj create mode 100644 FCGenstrings.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 FCGenstrings/main.m create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Source/FCGenstrings.h create mode 100644 Source/FCGenstrings.m diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5493428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +.DS_Store + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +#Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build \ No newline at end of file diff --git a/FCGenstrings.xcodeproj/project.pbxproj b/FCGenstrings.xcodeproj/project.pbxproj new file mode 100644 index 0000000..20266d5 --- /dev/null +++ b/FCGenstrings.xcodeproj/project.pbxproj @@ -0,0 +1,257 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + CBED22281BA2D9FF0089A0DF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CBED22271BA2D9FF0089A0DF /* main.m */; }; + CBED22311BA2DA630089A0DF /* FCGenstrings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBED22301BA2DA630089A0DF /* FCGenstrings.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CBED22221BA2D9FF0089A0DF /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + CBED22241BA2D9FF0089A0DF /* FCGenstrings */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = FCGenstrings; sourceTree = BUILT_PRODUCTS_DIR; }; + CBED22271BA2D9FF0089A0DF /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + CBED222F1BA2DA630089A0DF /* FCGenstrings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FCGenstrings.h; sourceTree = ""; }; + CBED22301BA2DA630089A0DF /* FCGenstrings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FCGenstrings.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CBED22211BA2D9FF0089A0DF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CBED221B1BA2D9FF0089A0DF = { + isa = PBXGroup; + children = ( + CBED22261BA2D9FF0089A0DF /* FCGenstrings */, + CBED22251BA2D9FF0089A0DF /* Products */, + ); + sourceTree = ""; + }; + CBED22251BA2D9FF0089A0DF /* Products */ = { + isa = PBXGroup; + children = ( + CBED22241BA2D9FF0089A0DF /* FCGenstrings */, + ); + name = Products; + sourceTree = ""; + }; + CBED22261BA2D9FF0089A0DF /* FCGenstrings */ = { + isa = PBXGroup; + children = ( + CBED222E1BA2DA4D0089A0DF /* Source */, + CBED22271BA2D9FF0089A0DF /* main.m */, + ); + path = FCGenstrings; + sourceTree = ""; + }; + CBED222E1BA2DA4D0089A0DF /* Source */ = { + isa = PBXGroup; + children = ( + CBED222F1BA2DA630089A0DF /* FCGenstrings.h */, + CBED22301BA2DA630089A0DF /* FCGenstrings.m */, + ); + name = Source; + path = ../Source; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CBED22231BA2D9FF0089A0DF /* FCGenstrings */ = { + isa = PBXNativeTarget; + buildConfigurationList = CBED222B1BA2D9FF0089A0DF /* Build configuration list for PBXNativeTarget "FCGenstrings" */; + buildPhases = ( + CBED22201BA2D9FF0089A0DF /* Sources */, + CBED22211BA2D9FF0089A0DF /* Frameworks */, + CBED22221BA2D9FF0089A0DF /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FCGenstrings; + productName = FCGenstrings; + productReference = CBED22241BA2D9FF0089A0DF /* FCGenstrings */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CBED221C1BA2D9FF0089A0DF /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0640; + ORGANIZATIONNAME = Farcoding; + TargetAttributes = { + CBED22231BA2D9FF0089A0DF = { + CreatedOnToolsVersion = 6.4; + }; + }; + }; + buildConfigurationList = CBED221F1BA2D9FF0089A0DF /* Build configuration list for PBXProject "FCGenstrings" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CBED221B1BA2D9FF0089A0DF; + productRefGroup = CBED22251BA2D9FF0089A0DF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CBED22231BA2D9FF0089A0DF /* FCGenstrings */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + CBED22201BA2D9FF0089A0DF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CBED22311BA2DA630089A0DF /* FCGenstrings.m in Sources */, + CBED22281BA2D9FF0089A0DF /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CBED22291BA2D9FF0089A0DF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + CBED222A1BA2D9FF0089A0DF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + }; + name = Release; + }; + CBED222C1BA2D9FF0089A0DF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + CBED222D1BA2D9FF0089A0DF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CBED221F1BA2D9FF0089A0DF /* Build configuration list for PBXProject "FCGenstrings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CBED22291BA2D9FF0089A0DF /* Debug */, + CBED222A1BA2D9FF0089A0DF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CBED222B1BA2D9FF0089A0DF /* Build configuration list for PBXNativeTarget "FCGenstrings" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CBED222C1BA2D9FF0089A0DF /* Debug */, + CBED222D1BA2D9FF0089A0DF /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = CBED221C1BA2D9FF0089A0DF /* Project object */; +} diff --git a/FCGenstrings.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/FCGenstrings.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..f77cb69 --- /dev/null +++ b/FCGenstrings.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/FCGenstrings/main.m b/FCGenstrings/main.m new file mode 100644 index 0000000..c78f140 --- /dev/null +++ b/FCGenstrings/main.m @@ -0,0 +1,19 @@ +// +// main.m +// FCGenstrings +// +// Created by Almer Lucke on 9/11/15. +// Copyright (c) 2015 Farcoding. All rights reserved. +// + + +#import +#import "FCGenstrings.h" + + +int main(int argc, const char * argv[]) { + @autoreleasepool { + [FCGenstrings genstringsForDirectory:@"/Users/almerlucke/Documents/Projects/iOS/CentreOfAppliedGaming_Parkinson/CentreOfAppliedGaming_Parkinson"]; + } + return 0; +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6cd17c4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Almer Lucke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f38b7c --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# FCGenstrings + +## Why use FCGenstrings? + +### Keep existing string values + +Genstrings will always overwrite your previous strings file and destroy all the string values that were present. + +**FCGenstrings** will scan the existing strings file and keep the values of the strings that are already added. + +### Formatted strings file + +The genstrings tool fails to create a structured strings file. That is why **FCGenstrings** will keep track of where a localized string was found and if no comment was given, it will put the filename as comment. + +Furthermore strings with equal comment are put under one comment header. Comment sections are ordered by alphabet and within a comment section all string keys are also sorted on alphabet + +Example generated strings: + + /* CAPAboutViewController.xib */ + + "ABOUT_TITLE" = "MEER INFORMATIE"; + + + /* CAPFilterGameFactory.m */ + + "GAME4_TITLE" = "KIES DE JUISTE LIJST"; + + "GAME4_TUTORIAL_STRING1" = "1. Bovenin het scherm staat uw opdracht."; + + "GAME4_TUTORIAL_STRING2" = "2. Onderin het scherm ziet u een aantal mogelijkheden staan."; + + "GAME4_TUTORIAL_STRING3" = "3. Tik op het schilderij met de juiste eigenschappen."; + + "GAME4_TUTORIAL_TITLE" = "UITLEG SELECTEREN"; + + + /* CAPFilterGameScene.m */ + + "GAME4_SUBTITLE1" = "Kies een schilderij met de volgende eigenschappen"; + + "GAME4_SUBTITLE2" = "Kies uit de volgende schilderijen"; + +### Localized interface file elements + +**FCGenstrings** can scan interface files for occurrences of *localizableInterfaceElementKey* entries added by people using **FCLocalizableInterfaceElement**. These entries are also added to the strings file. + +## Usage + + diff --git a/Source/FCGenstrings.h b/Source/FCGenstrings.h new file mode 100644 index 0000000..bb06bcf --- /dev/null +++ b/Source/FCGenstrings.h @@ -0,0 +1,17 @@ +// +// FCGenstrings.h +// InterfaceGenstrings +// +// Created by Almer Lucke on 9/9/15. +// Copyright (c) 2015 AliensAreAmongUs. All rights reserved. +// + + +#import + + +@interface FCGenstrings : NSObject + ++ (void)genstringsForDirectory:(NSString *)directory; + +@end diff --git a/Source/FCGenstrings.m b/Source/FCGenstrings.m new file mode 100644 index 0000000..8417ec1 --- /dev/null +++ b/Source/FCGenstrings.m @@ -0,0 +1,434 @@ +// +// FCGenstrings.m +// InterfaceGenstrings +// +// Created by Almer Lucke on 9/9/15. +// Copyright (c) 2015 AliensAreAmongUs. All rights reserved. +// + + +#import "FCGenstrings.h" + + +@interface FCLocalizedStringEntry : NSObject + +@property (nonatomic, copy) NSString *entryKey; + +@property (nonatomic, copy) NSString *entryValue; + +@property (nonatomic, copy) NSString *entryComment; + +@end + + +@implementation FCLocalizedStringEntry + +@end + + + +@implementation FCGenstrings + +#pragma mark - Source Filed (.m) + ++ (NSArray *)strippedStringsFromSourceString:(NSString *)sourceString +{ + NSRange searchedRange = NSMakeRange(0, [sourceString length]); + + // regex for c strings including escape characters + NSString *pattern = @"\"(\\\\.|[^\"])*\""; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + NSMutableArray *strippedStrings = [NSMutableArray array]; + + if (regex) { + NSArray *matches = [regex matchesInString:sourceString options:0 range:searchedRange]; + NSCharacterSet *trimQuotesCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\""]; + + for (NSTextCheckingResult *match in matches) { + NSString* matchText = [sourceString substringWithRange:[match range]]; + + // trim " characters from begin and end + [strippedStrings addObject:[matchText stringByTrimmingCharactersInSet:trimQuotesCharacterSet]]; + } + } + + return [strippedStrings copy]; +} + ++ (NSArray *)localizedStringsForSourceFile:(NSString *)sourceFilePath +{ + // use filename as comment if comment is nil + NSString *fileName = [sourceFilePath lastPathComponent]; + + NSString *source = [NSString stringWithContentsOfFile:sourceFilePath encoding:NSUTF8StringEncoding error:nil]; + NSMutableArray *allStringPairs = [NSMutableArray array]; + + if (source) { + // first get all regex matches for all possible variants for NSLocalizedString(@"key", @"value") + NSRange searchedRange = NSMakeRange(0, [source length]); + NSString *pattern = @"NSLocalizedString\\s*\\(\\s*@\"(\\\\.|[^\"])*\"\\s*,\\s*(nil|@\"(\\\\.|[^\"])*\")\\s*\\)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + NSArray *matches = [regex matchesInString:source options:0 range:searchedRange]; + + for (NSTextCheckingResult *match in matches) { + // get string from matched range and strip it into key and optional comment pairs + NSString* matchText = [source substringWithRange:[match range]]; + NSArray *pair = [self strippedStringsFromSourceString:matchText]; + + if (pair.count == 1) { + // use filename as comment if comment is nil + pair = @[pair[0], fileName]; + } + + [allStringPairs addObject:pair]; + } + } + + return [allStringPairs copy]; +} + + +#pragma mark - Interface Files (.storyboard, .xib) + ++ (NSString *)strippedStringFromInterfaceString:(NSString *)interfaceString +{ + NSRange searchedRange = NSMakeRange(0, [interfaceString length]); + + // regex for c strings including escape characters + NSString *pattern = @"\"(\\\\.|[^\"])*\""; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + + if (regex) { + // we expect the interfaceString (generated by xcode in xib or storyboard file) to be like + // " + // so find all c strings and the last one should be the one we are looking for + NSArray *matches = [regex matchesInString:interfaceString options:0 range:searchedRange]; + + if (matches.count == 3) { + NSTextCheckingResult *match = matches[2]; + NSString* matchText = [interfaceString substringWithRange:[match range]]; + NSCharacterSet *trimQuotesCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\""]; + + // trim " characters from begin and end + return [matchText stringByTrimmingCharactersInSet:trimQuotesCharacterSet]; + } + } + + return nil; +} + ++ (NSArray *)localizedStringsForInterfaceFile:(NSString *)interfaceFilePath +{ + NSString *fileName = [interfaceFilePath lastPathComponent]; + NSError *error = nil; + NSString *source = [NSString stringWithContentsOfFile:interfaceFilePath encoding:NSUTF8StringEncoding error:&error]; + NSMutableArray *allStringPairs = [NSMutableArray array]; + + if (source) { + NSRange searchedRange = NSMakeRange(0, [source length]); + + // match xcode generated pattern generated for our user defined keyPath localizableInterfaceElementKey + NSString *pattern = @""; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; + + if (regex) { + NSArray *matches = [regex matchesInString:source options:0 range:searchedRange]; + + for (NSTextCheckingResult *match in matches) { + NSString* matchText = [source substringWithRange:[match range]]; + NSArray *pair = @[[self strippedStringFromInterfaceString:matchText], fileName]; + + [allStringPairs addObject:pair]; + } + } + } + + return [allStringPairs copy]; +} + + +#pragma mark - Scan Localized Strings For Directory + ++ (NSDictionary *)localizedStringsForDirectory:(NSString *)directory +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:directory error:nil]; + NSMutableDictionary *entries = [NSMutableDictionary dictionary]; + + for (NSString *path in subPaths) { + NSString *extension = [path pathExtension]; + NSString *fullPath = [directory stringByAppendingPathComponent:path]; + NSArray *pairs = nil; + + if ([extension isEqualToString:@"m"]) { + // good old fashioned NSLocalizedString + pairs = [self localizedStringsForSourceFile:fullPath]; + } else if ([extension isEqualToString:@"storyboard"] || [extension isEqualToString:@"xib"]) { + // storyboard and xib use the same pattern + pairs = [self localizedStringsForInterfaceFile:fullPath]; + } + + for (NSArray *pair in pairs) { + FCLocalizedStringEntry *entry = [[FCLocalizedStringEntry alloc] init]; + entry.entryKey = pair[0]; + entry.entryValue = @""; + entry.entryComment = pair[1]; + + entries[entry.entryKey] = entry; + } + } + + return [entries copy]; +} + + +#pragma mark - Existing Localizable.strings + ++ (NSArray *)localizableStringsFilesForDirectory:(NSString *)directory +{ + // get paths for all localizable string files in root directory (recursive) + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:directory error:nil]; + NSMutableArray *localizableStringsFiles = [NSMutableArray array]; + + for (NSString *path in subPaths) { + NSString *fullPath = [directory stringByAppendingPathComponent:path]; + NSString *lastPathComponent = [path lastPathComponent]; + + if ([lastPathComponent isEqualToString:@"Localizable.strings"]) { + [localizableStringsFiles addObject:fullPath]; + } + } + + return [localizableStringsFiles copy]; +} + ++ (NSString *)removeCommentsInLocalizableStrings:(NSString *)localizableStrings +{ + // remove all multi line comments and single line comments + NSMutableString *mutableLocalizableStrings = [localizableStrings mutableCopy]; + NSRange searchedRange = NSMakeRange(0, [mutableLocalizableStrings length]); + NSString *pattern = @"(/\\*([^*]|[\\r\\n]|(\\*+([^*/]|[\\r\\n])))*\\*+/)|(//.*)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + + if (regex) { + NSArray *matches = [regex matchesInString:localizableStrings options:0 range:searchedRange]; + NSInteger shift = 0; + + for (NSTextCheckingResult *match in matches) { + NSRange range = match.range; + + range.location -= shift; + + [mutableLocalizableStrings replaceCharactersInRange:range withString:@""]; + + shift += range.length; + } + } + + return [mutableLocalizableStrings copy]; +} + ++ (NSArray *)rawLocalizedStringEntriesFromLocalizableStrings:(NSString *)localizableStrings +{ + // get all entries in source string that match variations of "KEY" = "VALUE"; + NSRange searchedRange = NSMakeRange(0, [localizableStrings length]); + NSString *pattern = @"\"(\\\\.|[^\"])*\"\\s*=\\s*\"(\\\\.|[^\"])*\";"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + NSMutableArray *rawEntries = [NSMutableArray array]; + + if (regex) { + NSArray *matches = [regex matchesInString:localizableStrings options:0 range:searchedRange]; + + for (NSTextCheckingResult *match in matches) { + NSString* matchText = [localizableStrings substringWithRange:[match range]]; + + [rawEntries addObject:matchText]; + } + } + + return [rawEntries copy]; +} + ++ (NSArray *)splitRawLocalizedStringEntry:(NSString *)localizedStringEntry +{ + // split found "KEY" = "VALUE"; strings in key and value strings + NSRange searchedRange = NSMakeRange(0, [localizedStringEntry length]); + NSString *pattern = @"\"(\\\\.|[^\"])*\""; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:0 + error:nil]; + NSMutableArray *splitStrings = [NSMutableArray array]; + + if (regex) { + NSArray *matches = [regex matchesInString:localizedStringEntry options:0 range:searchedRange]; + NSCharacterSet *trimQuotesCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\""]; + + for (NSTextCheckingResult *match in matches) { + NSString* matchText = [localizedStringEntry substringWithRange:[match range]]; + + matchText = [matchText stringByTrimmingCharactersInSet:trimQuotesCharacterSet]; + + [splitStrings addObject:matchText]; + } + } + + return [splitStrings copy]; +} + ++ (NSDictionary *)localizedStringsForLocalizableStringsFile:(NSString *)localizableStringsFile +{ + // collect all key/value string pairs in Localizable.strings file into a dictionary + NSString *localizableStrings = [NSString stringWithContentsOfFile:localizableStringsFile encoding:NSUTF8StringEncoding error:nil]; + + if (!localizableStrings) { + localizableStrings = [NSString stringWithContentsOfFile:localizableStringsFile encoding:NSUTF16StringEncoding error:nil]; + } + + NSMutableDictionary *stringsDictionary = [NSMutableDictionary dictionary]; + + if (localizableStrings) { + localizableStrings = [self removeCommentsInLocalizableStrings:localizableStrings]; + + NSArray *rawEntries = [self rawLocalizedStringEntriesFromLocalizableStrings:localizableStrings]; + + for (NSString *rawEntry in rawEntries) { + NSArray *splitEntry = [self splitRawLocalizedStringEntry:rawEntry]; + NSString *key = nil; + NSString *value = @""; + FCLocalizedStringEntry *entry = [[FCLocalizedStringEntry alloc] init]; + + if (splitEntry.count > 0) { + key = splitEntry[0]; + } + + if (splitEntry.count > 1) { + value = splitEntry[1]; + } + + if (key) { + entry.entryKey = key; + entry.entryValue = value; + entry.entryComment = nil; + stringsDictionary[key] = entry; + } + } + } + + return [stringsDictionary copy]; +} + + +#pragma mark - Write Localized.strings file + ++ (void)writeEntries:(NSDictionary *)entries toFile:(NSString *)file +{ + // sort comment groups keys + NSArray *sortedCommentKeys = [[entries allKeys] sortedArrayUsingComparator:^NSComparisonResult(NSString *string1, NSString *string2) { + return [[string1 lowercaseString] compare:[string2 lowercaseString]]; + }]; + + NSMutableString *fileContent = [NSMutableString string]; + + NSInteger commentCount = 0; + + // write sorted comment groups + for (NSString *key in sortedCommentKeys) { + [fileContent appendFormat:@"/* %@ */\n\n", key]; + + NSArray *commentEntries = entries[key]; + NSInteger entryCount = 0; + + for (FCLocalizedStringEntry *entry in commentEntries) { + entryCount++; + + if (entryCount != commentEntries.count) { + [fileContent appendFormat:@"\"%@\" = \"%@\";\n\n", entry.entryKey, entry.entryValue]; + } else { + [fileContent appendFormat:@"\"%@\" = \"%@\";", entry.entryKey, entry.entryValue]; + } + } + + commentCount++; + + if (commentCount != sortedCommentKeys.count) { + [fileContent appendString:@"\n\n\n"]; + } + } + + [fileContent writeToFile:file atomically:YES encoding:NSUTF8StringEncoding error:nil]; +} + + +#pragma mark - Genstrings for directory + ++ (NSDictionary *)commentDictionaryForEntries:(NSArray *)entries +{ + // group string entries on comment + NSMutableDictionary *commentDictionary = [NSMutableDictionary dictionary]; + + for (FCLocalizedStringEntry *entry in entries) { + NSMutableArray *commentEntries = commentDictionary[entry.entryComment]; + + if (!commentEntries) { + commentEntries = [NSMutableArray array]; + commentDictionary[entry.entryComment] = commentEntries; + } + + [commentEntries addObject:entry]; + } + + // sort comment entries + for (NSString *commentKey in [commentDictionary allKeys]) { + NSMutableArray *commentEntries = commentDictionary[commentKey]; + + [commentEntries sortUsingComparator:^NSComparisonResult(FCLocalizedStringEntry *entry1, FCLocalizedStringEntry *entry2) { + return [[entry1.entryKey lowercaseString] compare:[entry2.entryKey lowercaseString]]; + }]; + } + + return [commentDictionary copy]; +} + ++ (void)genstringsForDirectory:(NSString *)directory +{ + // get collected strings from root directory .m, .xib and .storyboard files and compare + // them with existing Localizable.strings files, we keep the values already in Localizable.strings files + // for the collected keys found and then overwrite Localizable.strings files + + // get collected strings from root directory .m, .xib and .storyboard + NSDictionary *collectedStrings = [self localizedStringsForDirectory:directory]; + + // get all Localizable.strings paths + NSArray *localizableStringsFiles = [self localizableStringsFilesForDirectory:directory]; + + NSArray *collectedKeys = [collectedStrings allKeys]; + + for (NSString *localizableStringsFile in localizableStringsFiles) { + // get current strings in Localizable file + NSDictionary *currentStrings = [self localizedStringsForLocalizableStringsFile:localizableStringsFile]; + NSMutableArray *entries = [NSMutableArray array]; + + // set entry value to existing value if it is available + for (NSString *key in collectedKeys) { + FCLocalizedStringEntry *currentEntry = currentStrings[key]; + FCLocalizedStringEntry *collectedEntry = collectedStrings[key]; + + if (currentEntry) { + collectedEntry.entryValue = currentEntry.entryValue; + } else { + collectedEntry.entryValue = @""; + } + + [entries addObject:collectedEntry]; + } + + // group strings based on comment + NSDictionary *commentDictionary = [self commentDictionaryForEntries:entries]; + + // overwrite existing strings file with updated strings + [self writeEntries:commentDictionary toFile:localizableStringsFile]; + } +} + +@end