init project

This commit is contained in:
Mihail Preis
2019-07-12 17:50:54 +05:00
commit 8992c709ea
11 changed files with 1147 additions and 0 deletions

109
.gitignore vendored Normal file
View File

@ -0,0 +1,109 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/
# File-based project format
*.iws
# IntelliJ
out/
### Swift template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Xcode Patch
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
*.swiftmodule/

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# StringGenerator
StringGenerator for Swift projects. Simple generation/add/converting localization files with enum.
## Requirements
- Swift 5
- Xcode 10.2
## How to use?
Build this and run in terminal: ``stringen [command] [params...]`` or simple `stringen` for get help.

View File

@ -0,0 +1,326 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
6511B41622CE162A007B5B23 /* BaseCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6511B41522CE162A007B5B23 /* BaseCommand.swift */; };
6511B41A22CE18CB007B5B23 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6511B41922CE18CB007B5B23 /* Colors.swift */; };
6511B41C22CE1ADD007B5B23 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6511B41B22CE1ADD007B5B23 /* Helpers.swift */; };
6511B41E22CE1B42007B5B23 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6511B41D22CE1B42007B5B23 /* Constants.swift */; };
65177E0822D85C9300D125EF /* AddStringCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65177E0722D85C9300D125EF /* AddStringCommand.swift */; };
A10CF0AF22339D820068E490 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10CF0AE22339D820068E490 /* main.swift */; };
C05FE96C63084AB3DF0DDFDB /* GenerateCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05FEAF5984674C7F1F657AA /* GenerateCommand.swift */; };
C05FEDB04CADDE3D52A8CF7D /* ConvertCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05FEB7E25F7F2601DE67B18 /* ConvertCommand.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
A10CF0A922339D820068E490 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6511B41522CE162A007B5B23 /* BaseCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCommand.swift; sourceTree = "<group>"; };
6511B41922CE18CB007B5B23 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
6511B41B22CE1ADD007B5B23 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
6511B41D22CE1B42007B5B23 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
65177E0722D85C9300D125EF /* AddStringCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddStringCommand.swift; sourceTree = "<group>"; };
A10CF0AB22339D820068E490 /* stringen */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = stringen; sourceTree = BUILT_PRODUCTS_DIR; };
A10CF0AE22339D820068E490 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C05FEAF5984674C7F1F657AA /* GenerateCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateCommand.swift; sourceTree = "<group>"; };
C05FEB7E25F7F2601DE67B18 /* ConvertCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertCommand.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
A10CF0A822339D820068E490 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
6511B41422CE1612007B5B23 /* Commands */ = {
isa = PBXGroup;
children = (
65177E0722D85C9300D125EF /* AddStringCommand.swift */,
6511B41522CE162A007B5B23 /* BaseCommand.swift */,
C05FEAF5984674C7F1F657AA /* GenerateCommand.swift */,
C05FEB7E25F7F2601DE67B18 /* ConvertCommand.swift */,
);
path = Commands;
sourceTree = "<group>";
};
A10CF0A222339D820068E490 = {
isa = PBXGroup;
children = (
A10CF0AD22339D820068E490 /* StringGenerator */,
A10CF0AC22339D820068E490 /* Products */,
);
sourceTree = "<group>";
};
A10CF0AC22339D820068E490 /* Products */ = {
isa = PBXGroup;
children = (
A10CF0AB22339D820068E490 /* stringen */,
);
path = Products;
sourceTree = "<group>";
};
A10CF0AD22339D820068E490 /* StringGenerator */ = {
isa = PBXGroup;
children = (
6511B41422CE1612007B5B23 /* Commands */,
A10CF0AE22339D820068E490 /* main.swift */,
6511B41922CE18CB007B5B23 /* Colors.swift */,
6511B41B22CE1ADD007B5B23 /* Helpers.swift */,
6511B41D22CE1B42007B5B23 /* Constants.swift */,
);
path = StringGenerator;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
A10CF0AA22339D820068E490 /* StringGenerator */ = {
isa = PBXNativeTarget;
buildConfigurationList = A10CF0B222339D820068E490 /* Build configuration list for PBXNativeTarget "StringGenerator" */;
buildPhases = (
A10CF0A722339D820068E490 /* Sources */,
A10CF0A822339D820068E490 /* Frameworks */,
A10CF0A922339D820068E490 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = StringGenerator;
productName = StringGenerator;
productReference = A10CF0AB22339D820068E490 /* stringen */;
productType = "com.apple.product-type.tool";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A10CF0A322339D820068E490 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = Waveaccess;
TargetAttributes = {
A10CF0AA22339D820068E490 = {
CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1020;
};
};
};
buildConfigurationList = A10CF0A622339D820068E490 /* Build configuration list for PBXProject "StringGenerator" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A10CF0A222339D820068E490;
productRefGroup = A10CF0AC22339D820068E490 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
A10CF0AA22339D820068E490 /* StringGenerator */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
A10CF0A722339D820068E490 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65177E0822D85C9300D125EF /* AddStringCommand.swift in Sources */,
6511B41622CE162A007B5B23 /* BaseCommand.swift in Sources */,
6511B41A22CE18CB007B5B23 /* Colors.swift in Sources */,
A10CF0AF22339D820068E490 /* main.swift in Sources */,
6511B41C22CE1ADD007B5B23 /* Helpers.swift in Sources */,
6511B41E22CE1B42007B5B23 /* Constants.swift in Sources */,
C05FE96C63084AB3DF0DDFDB /* GenerateCommand.swift in Sources */,
C05FEDB04CADDE3D52A8CF7D /* ConvertCommand.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A10CF0B022339D820068E490 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
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.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
A10CF0B122339D820068E490 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "Mac Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
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.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
A10CF0B322339D820068E490 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6PQJ88HC63;
PRODUCT_NAME = stringen;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
A10CF0B422339D820068E490 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6PQJ88HC63;
PRODUCT_NAME = stringen;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
A10CF0A622339D820068E490 /* Build configuration list for PBXProject "StringGenerator" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A10CF0B022339D820068E490 /* Debug */,
A10CF0B122339D820068E490 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A10CF0B222339D820068E490 /* Build configuration list for PBXNativeTarget "StringGenerator" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A10CF0B322339D820068E490 /* Debug */,
A10CF0B422339D820068E490 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A10CF0A322339D820068E490 /* Project object */;
}

View File

@ -0,0 +1,43 @@
//
// Colors.swift
// StringGenerator
//
// Created by Ekaterina Lapkovskaja on 04/07/2019.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
public protocol ModeCode {
var value: UInt8 { get }
}
public enum Color: UInt8, ModeCode {
case black = 30
case red
case green
case yellow
case blue
case magenta
case cyan
case white
case `default` = 39
case lightBlack = 90
case lightRed
case lightGreen
case lightYellow
case lightBlue
case lightMagenta
case lightCyan
case lightWhite
public var value: UInt8 {
return rawValue
}
}
extension String {
func color(_ color: Color) -> String {
return "\u{001B}[0;\(color.rawValue)m\(self)\u{001B}[0;\(Color.default.rawValue)m"
}
}

View File

@ -0,0 +1,77 @@
//
// AddStringCommand.swift
// StringGenerator
//
// Created by Ekaterina Lapkovskaja on 12/07/2019.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
class AddStringCommand: CommandProtocol {
private let fileManager = FileManager.default
var name: String = "add"
var description: String = """
Adds/Updates localizable strings in plist-files
type 'stringen add [Key.Key.Key...] [project resources folder path]'
if [Key.Key.Key...] already exists, updates the value
"""
func perform(arguments: [String]) {
if arguments.count == 0 {
print(error: "no arguments provided")
exit(1)
}
else if arguments.count == 1 {
print(error: "too few arguments")
exit(1)
}
else if arguments.count == 2 {
let key = arguments[0]
let resourcesPath = URL(fileURLWithPath: arguments[1])
guard let lprojFoldersPaths = try? fileManager.contentsOfDirectory(atPath: resourcesPath.path)
.filter({ $0.contains(Constants.lproj) })
.compactMap({ resourcesPath.appendingPathComponent($0) }),
!lprojFoldersPaths.isEmpty else {
print(error: "*.lproj folders not found.")
exit(1)
}
let stringFiles = lprojFoldersPaths.map { $0.appendingPathComponent(Constants.stringFileName) }
.filter({ fileManager.fileExists(atPath: $0.path) })
guard stringFiles.count == lprojFoldersPaths.count else {
print(error: "\(Constants.stringFileName)'s count is not equal to all languages.")
exit(1)
}
stringFiles.forEach({ addKey(key, fileURL: $0) })
}
else {
print(error: "too many arguments")
exit(1)
}
}
private func addKey(_ key: String, fileURL: URL) {
guard var plistDictionary = NSDictionary(contentsOf: fileURL) as? [String: Any] else {
print(error: "Can't parse file \(getTwoLastComponents(path: fileURL))")
exit(1)
}
print("Enter localized string for \("\(getTwoLastComponents(path: fileURL))".color(.lightCyan))\("".color(.default)).")
guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) else {
addKey(key, fileURL: fileURL)
exit(1)
}
plistDictionary[keyPath: key] = input
if !NSDictionary(dictionary: plistDictionary).write(toFile: fileURL.path, atomically: true) {
print(error: fileURL.path.color(.lightCyan) + " create error.")
exit(1)
}
print(success: "\(fileURL.path) successfully created.")
}
}

View File

@ -0,0 +1,15 @@
//
// BaseCommand.swift
// StringGenerator
//
// Created by Ekaterina Lapkovskaja on 04/07/2019.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
protocol CommandProtocol {
var name: String { get }
var description: String { get }
func perform(arguments: [String])
}

View File

@ -0,0 +1,119 @@
//
// ConvertCommand.swift
// StringGenerator
//
// Created by Mike Price on 2019-07-11.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
class ConvertCommand: CommandProtocol {
private let fileManager = FileManager.default
private lazy var rowRegex: NSRegularExpression = {
guard let regex = try? NSRegularExpression(pattern: Constants.localizationRowPattern) else {
print(error: "Regexp error.")
exit(1)
}
return regex
}()
var name: String = "convert"
var description: String = """
Converting all '\(Constants.generatedStringsFileName)' from resources folder to plists for all language (to selected output folder)
"""
func perform(arguments: [String]) {
guard arguments.count == 2 else {
print(error: "Please, provide resources folder & Localization output folder.")
exit(1)
}
let resourcesPath = URL(fileURLWithPath: arguments[0])
let outputPath = URL(fileURLWithPath: arguments[1])
guard let lprojFoldersPaths = try? fileManager
.contentsOfDirectory(atPath: resourcesPath.path)
.filter({ $0.contains(Constants.lproj) })
.compactMap({ resourcesPath.appendingPathComponent($0) }),
!lprojFoldersPaths.isEmpty else {
print(error: "*.lproj folders not found.")
exit(1)
}
let langFiles = lprojFoldersPaths.map { $0.appendingPathComponent(Constants.generatedStringsFileName) }
.filter({ fileManager.fileExists(atPath: $0.path) })
guard langFiles.count == lprojFoldersPaths.count else {
print(error: "\(Constants.generatedStringsFileName)'s count is not equal to all languages.")
exit(1)
}
if !fileManager.fileExists(atPath: outputPath.path) {
try? fileManager.createDirectory(at: outputPath, withIntermediateDirectories: true)
}
langFiles.forEach { (url: URL) in
guard let fileData = try? String(contentsOfFile: url.path)
else {
print(error: url.path.color(.lightCyan) + " parsing text error.")
exit(1)
}
let nameWithoutExt: String = url.pathComponents[url.pathComponents.count - 2]
var firstName = nameWithoutExt.split(separator: ".").dropLast()
firstName.append("plist")
let resultFileName = firstName.joined(separator: ".")
let stringPlistFilePath = outputPath.appendingPathComponent(resultFileName)
let dataStrings = fileData.split(separator: "\n")
.filter({ rowRegex.matches(String($0)) })
.map({ String($0) })
validate(name: stringPlistFilePath.path, dataStrings: dataStrings)
var plistData = [String: Any]()
parsing(dataStrings, in: &plistData)
if !NSDictionary(dictionary: plistData).write(toFile: stringPlistFilePath.path, atomically: true) {
print(error: stringPlistFilePath.path.color(.lightCyan) + " create error.")
exit(1)
}
print(success: "\(stringPlistFilePath.path) successfully created.")
}
}
private func validate(name: String, dataStrings: [String]) {
if !dataStrings.filter({ !rowRegex.matches($0) }).isEmpty {
print(error: "\(name) is invalid.")
exit(1)
}
}
private func parsing(_ dataStrings: [String], in result: inout [String: Any]) {
dataStrings.forEach { row in
let path = getGroup(Constants.rowPathComponent, from: row)
let value = getGroup(Constants.rowValueComponent, from: row)
result[keyPath: path] = value
}
}
private func getGroup(_ key: String, from row: String) -> String {
let fullRange = NSRange(row.startIndex..<row.endIndex, in: row)
guard let match = rowRegex.firstMatch(in: row, range: fullRange) else {
print(error: "Error get full range from '\(row)'")
exit(1)
}
let valueRange = match.range(withName: key)
if valueRange.location != NSNotFound, let valueRangeInRow = Range(valueRange, in: row) {
return String(row[valueRangeInRow])
}
else {
print(error: "Invalid value in '\(row)'")
exit(1)
}
}
}

View File

@ -0,0 +1,290 @@
//
// GenerateCommand.swift
// StringGenerator
//
// Created by Mike Price on 2019-07-11.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
class GenerateCommand: CommandProtocol {
private let fileManager = FileManager.default
var name: String = "generate"
var description: String = """
Parameters: [project resources folder path] [dir for generated enum path]
Generation localizable resources on steps:
- check on valid and compare all \(Constants.stringFileName) (for all languages) in resources folder
- generate \(Constants.generatedFileName) enum for selected directory
- generate \(Constants.generatedStringsFileName)'s for all languages
"""
func perform(arguments: [String]) {
guard arguments.count == 2 else {
print(error: "Please, provide resources folder & Localization output folder.")
return
}
let resourcesPath = URL(fileURLWithPath: arguments[0])
let localizationPath = URL(fileURLWithPath: arguments[1])
guard let lprojFoldersPaths = try? fileManager.contentsOfDirectory(atPath: resourcesPath.path)
.filter({ $0.contains(Constants.lproj) })
.compactMap({ resourcesPath.appendingPathComponent($0) }),
!lprojFoldersPaths.isEmpty else {
print(error: "*.lproj folders not found.")
exit(1)
}
let stringFiles = lprojFoldersPaths.map { $0.appendingPathComponent(Constants.stringFileName) }
.filter({ fileManager.fileExists(atPath: $0.path) })
guard stringFiles.count == lprojFoldersPaths.count else {
print(error: "\(Constants.stringFileName)'s count is not equal to all languages.")
exit(1)
}
validate(stringFiles: stringFiles)
compare(stringFiles: stringFiles)
makeLocalizationFile(stringFiles[0], at: localizationPath)
generationLocalizationStrings(stringFiles: stringFiles)
}
// MARK: Validation block
private func validate(stringFiles: [URL]) {
var isFailure = false
for stringFile in stringFiles {
guard let plistDictionary = NSDictionary(contentsOf: stringFile) as? [String: Any] else {
print(error: "Can't parse the file. Maybe, it's not a plist?")
exit(1)
}
let errorKeys = isValid(dict: plistDictionary)
if !errorKeys.isEmpty {
print("")
print(error: "Plist '\(getTwoLastComponents(path: stringFile).color(.lightCyan))' have next invalid keys:")
errorKeys.forEach { print(error: $0) }
isFailure = true
}
}
if isFailure {
exit(1)
}
print(success: "Key values in Plists is valid.")
}
private func isValid(dict: [String: Any], keyPath: String = "") -> [String] {
var errors = [String]()
for item in dict {
if let child = item.value as? [String: Any] {
if child.isEmpty {
errors.append("value for key \("\(keyPath)\(item.key)".color(.lightRed)) is empty.")
}
else {
errors.append(contentsOf: isValid(dict: child, keyPath: "\(keyPath)\(item.key)."))
}
}
else if (item.value as? String)?.isEmpty == true {
errors.append("value for key \("\(keyPath)\(item.key)".color(.lightRed)) is empty.")
}
else if item.value as? [String: Any] == nil && item.value as? String == nil {
errors.append("value for key \("\(keyPath)\(item.key)".color(.lightRed)) is not a String.")
}
}
return errors
}
// MARK: Compare block
private func compare(stringFiles: [URL]) {
var dictionaries: [(String, [String: Any])] = stringFiles.map { fileUrl in
guard let plistDictionary = NSDictionary(contentsOf: fileUrl) as? [String: Any] else {
print(error: "Can't parse \(getTwoLastComponents(path: fileUrl).color(.lightCyan)). Maybe, it's not a plist?")
exit(1)
}
return (fileUrl.path, plistDictionary)
}
var equalityResults: [Bool] = []
for index in 0...dictionaries.count - 1 {
let (firstPlistName, firstPlist) = dictionaries[index]
let (secondPlistName, secondPlist) = dictionaries[index != dictionaries.count - 1 ? index + 1 : 0]
equalityResults.append(comparePlists(firstName: firstPlistName, firstPlist: firstPlist, secondName: secondPlistName, secondPlist: secondPlist))
}
if equalityResults.contains(false) {
exit(1)
}
print(success: "Plists keys are equal to each other.")
}
private func comparePlists(firstName: String, firstPlist: [String: Any], secondName: String, secondPlist: [String: Any], keyPath: String = "") -> Bool {
var areEqual: Bool = true
for item in firstPlist {
if let child = item.value as? [String: Any] {
if let secondChild = secondPlist[item.key] as? [String: Any] {
areEqual = areEqual && comparePlists(firstName: firstName, firstPlist: child, secondName: secondName, secondPlist: secondChild, keyPath: "\(keyPath)\(item.key).")
}
else {
print(error: """
plist \("\(getTwoLastComponents(path: secondName))".color(.lightCyan))
does not contain\(" \(keyPath)\(item.key) ".color(.lightRed))key.
But exists in \("\(getTwoLastComponents(path: firstName))".color(.lightCyan)).
""")
areEqual = areEqual && comparePlists(firstName: firstName, firstPlist: child, secondName: secondName, secondPlist: secondPlist, keyPath: "\(keyPath)\(item.key).")
}
}
else if item.value as? String != nil {
if secondPlist[item.key] as? String == nil {
areEqual = false
print(error: """
plist \("\(getTwoLastComponents(path: secondName))".color(.lightCyan))
does not contain\(" \(keyPath)\(item.key) ".color(.lightRed))key.
But exists in \("\(getTwoLastComponents(path: firstName))".color(.lightCyan)).
""")
}
}
}
return areEqual
}
// MARK: LocalizedStringsKeys.swift generation block
private func makeLocalizationFile(_ path: URL, at localizationPath: URL) {
guard let plistDictionary = NSDictionary(contentsOf: path) as? [String: Any] else {
print(error: "Can't parse \(getTwoLastComponents(path: path).color(.lightCyan)). Maybe, it's not a plist?")
exit(1)
}
let startTime = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yy h:mm a Z"
var swiftFileString: String = ""
swiftFileString += Constants.comment
swiftFileString += "///Generated: \(dateFormatter.string(from: startTime))\n\n"
swiftFileString += enumStart(name: Constants.enumName)
swiftFileString += parseBody(dict: plistDictionary).0
swiftFileString += enumEnd()
let savePath = localizationPath.appendingPathComponent(Constants.generatedFileName).path
if !fileManager.fileExists(atPath: savePath) {
fileManager.createFile(atPath: savePath, contents: swiftFileString.data(using: .utf8), attributes: nil)
}
else {
do {
try swiftFileString.write(toFile: savePath, atomically: true, encoding: .utf8)
print(success: "\(Constants.generatedFileName) enum in '\(localizationPath.path)' was created successfully.")
}
catch {
print(error: error.localizedDescription)
exit(1)
}
}
}
private func parseBody(dict: [String: Any], level: Int = 1, keyPath: String = "") -> (String, Bool) {
var res = ""
var levelHasKey = false
for item in dict {
if let child = item.value as? [String: Any] {
let tmp = parseBody(dict: child, level: level + 1, keyPath: "\(keyPath)\(item.key).")
res += tab(level)
res += enumStart(name: item.key, isLocalized: tmp.1)
res += tmp.0
res += tab(level)
res += enumEnd()
}
if item.value as? String != nil {
levelHasKey = true
res += tab(level)
res += caseFor(name: item.key, value: "\(keyPath)\(item.key)")
}
}
if levelHasKey {
res += localized(key: keyPath, level: level)
}
return (res, levelHasKey)
}
private func enumStart(name: String, isLocalized: Bool = false) -> String {
return "enum \(name)\(isLocalized ? ": String, LocalizedProtocol" : "" ) {\n"
}
private func enumEnd() -> String {
return "}\n"
}
private func caseFor(name: String, value: String) -> String {
return "case \(name) = \"\(value)\"\n"
}
private func tab(_ level: Int) -> String {
var res = ""
for _ in 0 ... level - 1 {
res += "\t"
}
return res
}
private func localized(key: String, level: Int) -> String {
var res = "\n"
res += tab(level)
res += "var localized: String {\n"
res += tab(level + 1)
res += "return rawValue.localized\n"
res += tab(level)
res += "}\n"
return res
}
// MARK: Localized.strings generation block
private func generationLocalizationStrings(stringFiles: [URL]) {
stringFiles.forEach { url in
guard let plistDictionary = NSDictionary(contentsOf: url) as? [String: Any] else {
print(error: "Can't parse \(getTwoLastComponents(path: url).color(.lightCyan)). Maybe, it's not a plist?")
exit(1)
}
var content = [String]()
content.append(Constants.comment + "/// from: \(getTwoLastComponents(path: url))")
calculationStrings(plistDictionary, content: &content)
let body = content.enumerated().map({ (index: Int, item: String) -> String in
if index == content.count - 1 { return item }
return item.split(separator: ".").first == content[index+1].split(separator: ".").first
? item
: item + "\n"
}).joined(separator: "\n")
let savePath = url.deletingLastPathComponent().appendingPathComponent(Constants.generatedStringsFileName).path
if !fileManager.fileExists(atPath: savePath) {
fileManager.createFile(atPath: savePath, contents: body.data(using: .utf8), attributes: nil)
print(success: "\(savePath) was created successfully.")
}
else {
do {
try body.write(toFile: savePath, atomically: true, encoding: .utf8)
print(success: "\(savePath) was created successfully.")
}
catch {
print(error: error.localizedDescription)
exit(1)
}
}
}
}
private func calculationStrings(_ dict: [String: Any], content: inout [String], path: String = "") {
dict.forEach { key, value in
if let value = value as? Dictionary<String, Any> {
calculationStrings(value, content: &content, path: "\(path + key).")
}
else if let value = value as? String {
content.append("\"\(path + key)\" = \"\(value.replacingOccurrences(of: "\"", with: "\\\""))\";")
}
}
}
}

View File

@ -0,0 +1,22 @@
//
// Constants.swift
// StringGenerator
//
// Created by Ekaterina Lapkovskaja on 04/07/2019.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
class Constants {
static let stringFileName = "strings.plist"
static let lproj = ".lproj"
static let enumName: String = "Localized"
static let generatedFileName = "LocalizedStringsKeys.swift"
static let generatedStringsFileName = "Localizable.strings"
static let comment = "///This file is automatically generated. Yout must not edit it manually\n"
static let localizationRowPattern = #"\"(?<path>(.+))\" = \"(?<value>(.+))\";"#
static let rowPathComponent = "path"
static let rowValueComponent = "value"
}

View File

@ -0,0 +1,88 @@
//
// Helpers.swift
// StringGenerator
//
// Created by Ekaterina Lapkovskaja on 04/07/2019.
// Copyright © 2019 Waveaccess. All rights reserved.
//
import Foundation
func print(error: String) {
print("\("Error".color(.red)): \(error)")
}
func print(success: String) {
print(success.color(.green))
}
func getTwoLastComponents(path: String) -> String {
return getTwoLastComponents(path: URL(fileURLWithPath: path))
}
func getTwoLastComponents(path: URL) -> String {
let excludeCount = path.pathComponents.count - 2
return path.pathComponents.dropFirst(excludeCount).joined(separator: "/")
}
extension NSRegularExpression {
func matches(_ string: String) -> Bool {
let range = NSRange(location: 0, length: string.utf16.count)
return firstMatch(in: string, options: [], range: range) != nil
}
}
extension Dictionary {
subscript(keyPath keyPath: String) -> Any? {
get {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath)
else { return nil }
return getValue(forKeyPath: keyPath)
}
set {
guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath),
let newValue = newValue
else { return }
self.setValue(newValue, forKeyPath: keyPath)
}
}
static private func keyPathKeys(forKeyPath: String) -> [Key]? {
let keys = forKeyPath.components(separatedBy: ".").reversed().compactMap({ $0 as? Key })
return keys.isEmpty ? nil : keys
}
private func getValue(forKeyPath keyPath: [Key]) -> Any? {
guard let lastKey = keyPath.last, let value = self[lastKey]
else { return nil }
return keyPath.count == 1 ? value : (value as? [Key: Any])
.flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) }
}
private mutating func setValue(_ value: Any, forKeyPath keyPath: [Key]) {
var keyPath = keyPath
guard let firstKey = keyPath.popLast() else { return }
if keyPath.isEmpty {
self[firstKey] = value as? Value
}
else {
var dict: [Key : Any] = self[firstKey] as? [Key : Any] ?? [:]
deepSet(value, keyPath: keyPath, in: &dict)
self[firstKey] = dict as? Value
}
}
private func deepSet(_ value: Any, keyPath: [Key], in dict: inout [Key : Any]) {
var keyPath = keyPath
guard let key: Key = keyPath.popLast() else {
return
}
if keyPath.isEmpty {
dict[key] = value
return
}
var bufferDict: [Key: Any] = dict[key] as? [Key : Any] ?? [:]
deepSet(value, keyPath: keyPath, in: &bufferDict)
dict[key] = bufferDict
}
}

View File

@ -0,0 +1,49 @@
//
// LocalizedStringKeyGen.swift
// TaxFree
//
// Created by Ekaterina Lapkovskaja on 05/03/2019.
//
import Foundation
class Run {
var commands: [CommandProtocol] = []
func perform(arguments: [String]) {
guard arguments.count > 0, let commandName = arguments.first, let command = commands.first(where: { (row) -> Bool in
return row.name == commandName
}) else {
showHelp()
return
}
var args = arguments
args.removeFirst()
command.perform(arguments: args)
}
private func showHelp() {
for command in commands {
var desc = ""
for (index, key) in command.description.split(separator: "\n").enumerated() {
desc.append(String(format: index == 0 ? "%s%@\n" : "%-16s%@\n", ("" as NSString).utf8String!, String(key)))
}
let str = String(format: "%-22s\("".color(.default)) - %@", (command.name.color(.green) as NSString).utf8String!, desc)
print(str)
}
}
func command<T>(class: T.Type) -> T? {
return commands.first(where: { command in
return command is T
}) as? T
}
}
let run = Run()
run.commands.append(GenerateCommand())
run.commands.append(AddStringCommand())
run.commands.append(ConvertCommand())
var arguments = CommandLine.arguments
arguments.removeFirst()
run.perform(arguments: arguments)