diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ResgenSwift-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ResgenSwift-Package.xcscheme
index b573efc..63f224c 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/ResgenSwift-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ResgenSwift-Package.xcscheme
@@ -76,6 +76,48 @@
ReferencedContainer = "container:">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
diff --git a/Package.swift b/Package.swift
index 14cab04..5933c50 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,6 +5,7 @@ import PackageDescription
let package = Package(
name: "ResgenSwift",
+ platforms: [.macOS(.v10_12)],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
@@ -14,7 +15,7 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "ResgenSwift",
- dependencies: ["FontToolCore", "ColorToolCore"]
+ dependencies: ["FontToolCore", "ColorToolCore", "TwineToolCore", "StringToolCore", "Strings"]
),
.target(
name: "FontToolCore",
@@ -30,6 +31,27 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
+ .target(
+ name: "TwineToolCore",
+ dependencies: [
+ "CLIToolCore",
+ .product(name: "ArgumentParser", package: "swift-argument-parser")
+ ]
+ ),
+ .target(
+ name: "StringToolCore",
+ dependencies: [
+ "CLIToolCore",
+ .product(name: "ArgumentParser", package: "swift-argument-parser")
+ ]
+ ),
+ .target(
+ name: "Strings",
+ dependencies: [
+ "CLIToolCore",
+ .product(name: "ArgumentParser", package: "swift-argument-parser")
+ ]
+ ),
// Helper targets
.target(name: "CLIToolCore"),
// Test targets
diff --git a/SampleFiles/Colors/Generated/UIColor+ColorGenAllScript.swift b/SampleFiles/Colors/Generated/UIColor+ColorGenAllScript.swift
index 6d934e1..5b8cce8 100644
--- a/SampleFiles/Colors/Generated/UIColor+ColorGenAllScript.swift
+++ b/SampleFiles/Colors/Generated/UIColor+ColorGenAllScript.swift
@@ -1,4 +1,4 @@
-// Generated from ColorToolCore at 2021-12-22 09:32:10 +0000
+// Generated from ColorToolCore at 2022-01-10 10:57:08 +0000
import UIKit
diff --git a/SampleFiles/Fonts/Generated/UIFont+FontGenAllScript.swift b/SampleFiles/Fonts/Generated/UIFont+FontGenAllScript.swift
new file mode 100644
index 0000000..398823b
--- /dev/null
+++ b/SampleFiles/Fonts/Generated/UIFont+FontGenAllScript.swift
@@ -0,0 +1,62 @@
+// Generated from FontToolCore
+
+import UIKit
+
+extension UIFont {
+
+ enum FontName: String {
+ case LatoItalic = "Lato-Italic"
+ case LatoLightItalic = "Lato-LightItalic"
+ case LatoHairline = "Lato-Hairline"
+ case LatoBold = "Lato-Bold"
+ case LatoBlack = "Lato-Black"
+ case LatoRegular = "Lato-Regular"
+ case LatoBlackItalic = "Lato-BlackItalic"
+ case LatoBoldItalic = "Lato-BoldItalic"
+ case LatoLight = "Lato-Light"
+ case LatoHairlineItalic = "Lato-HairlineItalic"
+ }
+
+ // MARK: - Getter
+
+ static let LatoItalic: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoItalic.rawValue, size: size)!
+ }
+
+ static let LatoLightItalic: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
+ }
+
+ static let LatoHairline: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoHairline.rawValue, size: size)!
+ }
+
+ static let LatoBold: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoBold.rawValue, size: size)!
+ }
+
+ static let LatoBlack: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoBlack.rawValue, size: size)!
+ }
+
+ static let LatoRegular: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoRegular.rawValue, size: size)!
+ }
+
+ static let LatoBlackItalic: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
+ }
+
+ static let LatoBoldItalic: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
+ }
+
+ static let LatoLight: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoLight.rawValue, size: size)!
+ }
+
+ static let LatoHairlineItalic: ((_ size: CGFloat) -> UIFont) = { size in
+ UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
+ }
+
+}
\ No newline at end of file
diff --git a/SampleFiles/Strings/Generated/MyString+StringTargetSuffix.swift b/SampleFiles/Strings/Generated/MyString+StringTargetSuffix.swift
new file mode 100644
index 0000000..38795d5
--- /dev/null
+++ b/SampleFiles/Strings/Generated/MyString+StringTargetSuffix.swift
@@ -0,0 +1,37 @@
+// Generated from StringToolCore at 2022-01-10 08:27:11 +0000
+
+import UIKit
+
+fileprivate let kStringsFileName = "sampleStrings"
+
+extension MyString {
+
+ // MARK: - Webservice
+
+ /// Translation in en :
+ /// en
+ var param_lang: String {
+ NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
+ }
+
+ // MARK: - Generic
+
+ /// Translation in en :
+ /// Back
+ var generic_back: String {
+ NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
+ }
+
+ /// Translation in en :
+ /// Loading data...
+ var generic_loading_data: String {
+ NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
+ }
+
+ /// Translation in en :
+ /// Welcome %@ !
+ var generic_welcome_firstname_format: String {
+ NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome %@ !", comment: "")
+ }
+
+}
\ No newline at end of file
diff --git a/SampleFiles/Strings/Generated/String+StringGenAllScript.swift b/SampleFiles/Strings/Generated/String+StringGenAllScript.swift
new file mode 100644
index 0000000..a2b90cf
--- /dev/null
+++ b/SampleFiles/Strings/Generated/String+StringGenAllScript.swift
@@ -0,0 +1,37 @@
+// Generated from Strings-Stringium at 2022-01-10 10:57:09 +0000
+
+import UIKit
+
+fileprivate let kStringsFileName = "sampleStrings"
+
+extension String {
+
+ // MARK: - Webservice
+
+ /// Translation in en :
+ /// en
+ static var param_lang: String {
+ NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
+ }
+
+ // MARK: - Generic
+
+ /// Translation in en :
+ /// Back
+ static var generic_back: String {
+ NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
+ }
+
+ /// Translation in en :
+ /// Loading data...
+ static var generic_loading_data: String {
+ NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
+ }
+
+ /// Translation in en :
+ /// Welcome \"%@\" !
+ static var generic_welcome_firstname_format: String {
+ NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
+ }
+
+}
\ No newline at end of file
diff --git a/SampleFiles/Strings/Generated/String+StringTargetSuffix.swift b/SampleFiles/Strings/Generated/String+StringTargetSuffix.swift
new file mode 100644
index 0000000..fcf6786
--- /dev/null
+++ b/SampleFiles/Strings/Generated/String+StringTargetSuffix.swift
@@ -0,0 +1,37 @@
+// Generated from StringToolCore at 2022-01-10 08:39:52 +0000
+
+import UIKit
+
+fileprivate let kStringsFileName = "sampleStrings"
+
+extension String {
+
+ // MARK: - Webservice
+
+ /// Translation in en :
+ /// en
+ static var param_lang: String {
+ NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
+ }
+
+ // MARK: - Generic
+
+ /// Translation in en :
+ /// Back
+ static var generic_back: String {
+ NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
+ }
+
+ /// Translation in en :
+ /// Loading data...
+ static var generic_loading_data: String {
+ NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
+ }
+
+ /// Translation in en :
+ /// Welcome \"%@\" !
+ static var generic_welcome_firstname_format: String {
+ NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
+ }
+
+}
\ No newline at end of file
diff --git a/SampleFiles/Strings/Generated/en-us.lproj/sampleStrings.strings b/SampleFiles/Strings/Generated/en-us.lproj/sampleStrings.strings
new file mode 100644
index 0000000..6277aac
--- /dev/null
+++ b/SampleFiles/Strings/Generated/en-us.lproj/sampleStrings.strings
@@ -0,0 +1,19 @@
+/**
+ * Apple Strings File
+ * Generated by ResgenSwift 1.0.0
+ * Language: en-us
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "en-us"
+
+
+/********** Generic **********/
+
+"generic_back" = "Back"
+
+"generic_loading_data" = "Loading data..."
+
+"generic_welcome_firstname_format" = "Welcome \"%@\" !"
+
diff --git a/SampleFiles/Strings/Generated/en.lproj/sampleStrings.strings b/SampleFiles/Strings/Generated/en.lproj/sampleStrings.strings
new file mode 100644
index 0000000..06a479e
--- /dev/null
+++ b/SampleFiles/Strings/Generated/en.lproj/sampleStrings.strings
@@ -0,0 +1,19 @@
+/**
+ * Apple Strings File
+ * Generated by ResgenSwift 1.0.0
+ * Language: en
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "en"
+
+
+/********** Generic **********/
+
+"generic_back" = "Back"
+
+"generic_loading_data" = "Loading data..."
+
+"generic_welcome_firstname_format" = "Welcome \"%@\" !"
+
diff --git a/SampleFiles/Strings/Generated/fr.lproj/sampleStrings.strings b/SampleFiles/Strings/Generated/fr.lproj/sampleStrings.strings
new file mode 100644
index 0000000..ad5e541
--- /dev/null
+++ b/SampleFiles/Strings/Generated/fr.lproj/sampleStrings.strings
@@ -0,0 +1,19 @@
+/**
+ * Apple Strings File
+ * Generated by ResgenSwift 1.0.0
+ * Language: fr
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "fr"
+
+
+/********** Generic **********/
+
+"generic_back" = "Retour"
+
+"generic_loading_data" = "Chargement des données..."
+
+"generic_welcome_firstname_format" = "Bienvenue \"%@\" !"
+
diff --git a/SampleFiles/Strings/sampleStrings.txt b/SampleFiles/Strings/sampleStrings.txt
new file mode 100644
index 0000000..bfcb217
--- /dev/null
+++ b/SampleFiles/Strings/sampleStrings.txt
@@ -0,0 +1,27 @@
+[[Webservice]]
+ [param_lang]
+ en = en
+ tags = droid,ios
+ comments =
+ fr = fr
+ en-us = en-us
+
+[[Generic]]
+ [generic_back]
+ en = Back
+ tags = droid,ios
+ comments =
+ fr = Retour
+ en-us = Back
+ [generic_loading_data]
+ en = Loading data...
+ tags = droid,ios
+ comments =
+ fr = Chargement des données...
+ en-us = Loading data...
+ [generic_welcome_firstname_format]
+ en = Welcome "%@" !
+ tags = droid,ios
+ comments =
+ fr = Bienvenue "%@" !
+ en-us = Welcome "%@" !
diff --git a/SampleFiles/Tags/Generated/Tags+TagGenAllScript.swift b/SampleFiles/Tags/Generated/Tags+TagGenAllScript.swift
new file mode 100644
index 0000000..e0b2cda
--- /dev/null
+++ b/SampleFiles/Tags/Generated/Tags+TagGenAllScript.swift
@@ -0,0 +1,23 @@
+// Generated from Strings-Tags at 2022-01-10 10:57:09 +0000
+
+import UIKit
+
+// typelias Tags = String
+
+extension Tags {
+
+ // MARK: - ScreenTag
+
+ /// Translation in ium :
+ /// Ecran un
+ static var screen_one: String {
+ "Ecran un"
+ }
+
+ /// Translation in ium :
+ /// Ecran deux
+ static var screen_two: String {
+ "Ecran deux"
+ }
+
+}
diff --git a/SampleFiles/Tags/sampleTags.txt b/SampleFiles/Tags/sampleTags.txt
new file mode 100644
index 0000000..98f67d4
--- /dev/null
+++ b/SampleFiles/Tags/sampleTags.txt
@@ -0,0 +1,7 @@
+[[ScreenTag]]
+ [screen_one]
+ ium = Ecran un
+ tags = droid,ios
+ [screen_two]
+ ium = Ecran deux
+ tags = droid,ios
diff --git a/SampleFiles/Twine/Generated/R2String+sampleStrings.swift b/SampleFiles/Twine/Generated/R2String+sampleStrings.swift
new file mode 100644
index 0000000..3d044de
--- /dev/null
+++ b/SampleFiles/Twine/Generated/R2String+sampleStrings.swift
@@ -0,0 +1,39 @@
+//
+// Generated by Twine 1.0.4
+//
+
+import UIKit
+
+fileprivate let kStringsFileName = "sampleStrings"
+
+extension R2String {
+
+ // MARK: - Webservice
+
+ /// Translation in en :
+ /// en
+ var param_lang: String {
+ return NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
+ }
+
+ // MARK: - Generic
+
+ /// Translation in en :
+ /// Back
+ var generic_back: String {
+ return NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
+ }
+
+ /// Translation in en :
+ /// "Loading" data...
+ var generic_loading_data: String {
+ return NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "\"Loading\" data...", comment: "")
+ }
+
+ /// Translation in en :
+ /// Other
+ var generic_other: String {
+ return NSLocalizedString("generic_other", tableName: kStringsFileName, bundle: Bundle.main, value: "Other", comment: "")
+ }
+
+}
diff --git a/SampleFiles/Twine/Generated/en-us.lproj/sampleStrings.strings b/SampleFiles/Twine/Generated/en-us.lproj/sampleStrings.strings
new file mode 100644
index 0000000..4fda80e
--- /dev/null
+++ b/SampleFiles/Twine/Generated/en-us.lproj/sampleStrings.strings
@@ -0,0 +1,18 @@
+/**
+ * Apple Strings File
+ * Generated by Twine 1.0.4
+ * Language: en-us
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "en-us";
+
+
+/********** Generic **********/
+
+"generic_back" = "Back";
+
+"generic_loading_data" = "\"Loading\" data...";
+
+"generic_other" = "Other";
diff --git a/SampleFiles/Twine/Generated/en.lproj/sampleStrings.strings b/SampleFiles/Twine/Generated/en.lproj/sampleStrings.strings
new file mode 100644
index 0000000..8336af5
--- /dev/null
+++ b/SampleFiles/Twine/Generated/en.lproj/sampleStrings.strings
@@ -0,0 +1,18 @@
+/**
+ * Apple Strings File
+ * Generated by Twine 1.0.4
+ * Language: en
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "en";
+
+
+/********** Generic **********/
+
+"generic_back" = "Back";
+
+"generic_loading_data" = "\"Loading\" data...";
+
+"generic_other" = "Other";
diff --git a/SampleFiles/Twine/Generated/fr.lproj/sampleStrings.strings b/SampleFiles/Twine/Generated/fr.lproj/sampleStrings.strings
new file mode 100644
index 0000000..42e99ec
--- /dev/null
+++ b/SampleFiles/Twine/Generated/fr.lproj/sampleStrings.strings
@@ -0,0 +1,18 @@
+/**
+ * Apple Strings File
+ * Generated by Twine 1.0.4
+ * Language: fr
+ */
+
+/********** Webservice **********/
+
+"param_lang" = "fr";
+
+
+/********** Generic **********/
+
+"generic_back" = "Retour";
+
+"generic_loading_data" = "\"Chargement\" des données...";
+
+"generic_other" = "Autre";
diff --git a/SampleFiles/Twine/sampleStrings.txt b/SampleFiles/Twine/sampleStrings.txt
new file mode 100644
index 0000000..3878b33
--- /dev/null
+++ b/SampleFiles/Twine/sampleStrings.txt
@@ -0,0 +1,27 @@
+[[Webservice]]
+ [param_lang]
+ en = en
+ tags = droid,ios
+ comments =
+ fr = fr
+ en-us = en-us
+
+[[Generic]]
+ [generic_back]
+ en = Back
+ tags = droid,ios
+ comments =
+ fr = Retour
+ en-us = Back
+ [generic_loading_data]
+ en = "Loading" data...
+ tags = droid,ios
+ comments =
+ fr = "Chargement" des données...
+ en-us = "Loading" data...
+ [generic_other]
+ en = Other
+ tags = droid,ios
+ comments =
+ fr = Autre
+ en-us = Other
diff --git a/SampleFiles/genAllRessources.sh b/SampleFiles/genAllRessources.sh
index c1d139c..e1f5b72 100755
--- a/SampleFiles/genAllRessources.sh
+++ b/SampleFiles/genAllRessources.sh
@@ -1,15 +1,42 @@
#/bin/bash
+FORCE_FLAG="$1"
+
# Font
-swift run -c release FontToolCore "./Fonts/sampleFontsAl.txt" \
+swift run -c release FontToolCore $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
--extension-output-path "./Fonts/Generated" \
- --extension-name "R2Font" \
+ --extension-name "UIFont" \
--extension-suffix "GenAllScript"
# Color
-swift run -c release ColorToolCore "./Colors/sampleColors1.txt" \
+swift run -c release ColorToolCore $FORCE_FLAG "./Colors/sampleColors1.txt" \
--style all \
--xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \
--extension-name "UIColor" \
--extension-suffix "GenAllScript"
+
+# Twine
+swift run -c release Strings twine $FORCE_FLAG "./Twine/sampleStrings.txt" \
+ --output-path "./Twine/Generated" \
+ --langs "fr en en-us" \
+ --default-lang "en" \
+ --extension-output-path "./Twine/Generated"
+
+# Strings
+swift run -c release Strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
+ --output-path "./Strings/Generated" \
+ --langs "fr en en-us" \
+ --default-lang "en" \
+ --extension-output-path "./Strings/Generated" \
+ --extension-name "String" \
+ --extension-suffix "GenAllScript"
+
+# Tags
+swift run -c release Strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
+ --lang "ium" \
+ --extension-output-path "./Tags/Generated" \
+ --extension-name "Tags" \
+ --extension-suffix "GenAllScript"
+
+# Images
diff --git a/Sources/CLIToolCore/SequenceExtensions.swift b/Sources/CLIToolCore/SequenceExtensions.swift
new file mode 100644
index 0000000..2608276
--- /dev/null
+++ b/Sources/CLIToolCore/SequenceExtensions.swift
@@ -0,0 +1,15 @@
+//
+// SequenceExtension.swift
+//
+//
+// Created by Thibaut Schmitt on 04/01/2022.
+//
+
+import Foundation
+
+public extension Sequence where Iterator.Element: Hashable {
+ func unique() -> [Iterator.Element] {
+ var seen: [Iterator.Element: Bool] = [:]
+ return self.filter { seen.updateValue(true, forKey: $0) == nil }
+ }
+}
diff --git a/Sources/CLIToolCore/Shell.swift b/Sources/CLIToolCore/Shell.swift
index 40ffbbe..4cb573d 100644
--- a/Sources/CLIToolCore/Shell.swift
+++ b/Sources/CLIToolCore/Shell.swift
@@ -1,5 +1,5 @@
//
-// File.swift
+// Shell.swift
//
//
// Created by Thibaut Schmitt on 22/12/2021.
diff --git a/Sources/CLIToolCore/Extensions.swift b/Sources/CLIToolCore/StringExtensions.swift
similarity index 59%
rename from Sources/CLIToolCore/Extensions.swift
rename to Sources/CLIToolCore/StringExtensions.swift
index bbe4a58..47a7327 100644
--- a/Sources/CLIToolCore/Extensions.swift
+++ b/Sources/CLIToolCore/StringExtensions.swift
@@ -7,8 +7,6 @@
import Foundation
-// MARK: - String
-
public extension String {
func removeCharacters(from forbiddenChars: CharacterSet) -> String {
let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) }
@@ -19,7 +17,15 @@ public extension String {
return removeCharacters(from: CharacterSet(charactersIn: from))
}
- func removeTrailingWhitespace() -> String {
+ func replacingOccurrences(of: [String], with: String) -> Self {
+ var tmp = self
+ for e in of {
+ tmp = tmp.replacingOccurrences(of: e, with: with)
+ }
+ return tmp
+ }
+
+ func removeTrailingWhitespace() -> Self {
var newString = self
while newString.last?.isWhitespace == true {
@@ -29,6 +35,33 @@ public extension String {
return newString
}
+ func removeLeadingWhitespace() -> Self {
+ var newString = self
+
+ while newString.first?.isWhitespace == true {
+ newString = String(newString.dropFirst())
+ }
+
+ return newString
+ }
+
+ func removeLeadingTrailingWhitespace() -> Self {
+ var newString = self
+
+ newString = newString.removeLeadingWhitespace()
+ newString = newString.removeTrailingWhitespace()
+
+ return newString
+ }
+
+ func escapeDoubleQuote() -> Self {
+ replacingOccurrences(of: "\"", with: "\\\"")
+ }
+
+ func replaceTiltWithHomeDirectoryPath() -> Self {
+ replacingOccurrences(of: "~", with: "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)")
+ }
+
func colorComponent() -> (alpha: String, red: String, green: String, blue: String) {
var alpha: String = "FF"
var red: String
@@ -52,13 +85,3 @@ public extension String {
return (alpha: alpha, red: red, green: green, blue: blue)
}
}
-
-// MARK: - Sequence
-
-extension Sequence where Iterator.Element: Hashable {
- public func unique() -> [Iterator.Element] {
- var seen: [Iterator.Element: Bool] = [:]
- return self.filter { seen.updateValue(true, forKey: $0) == nil }
- }
-}
-
diff --git a/Sources/ColorToolCore/main.swift b/Sources/ColorToolCore/main.swift
index f6aa216..b20301e 100644
--- a/Sources/ColorToolCore/main.swift
+++ b/Sources/ColorToolCore/main.swift
@@ -21,19 +21,19 @@ struct ColorTool: ParsableCommand {
@Flag(name: .customShort("f"), help: "Should force generation")
var forceGeneration = false
- @Argument(help: "Input files where colors ared defined.")
+ @Argument(help: "Input files where colors ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String
@Option(help: "Color style to generate: light for light colors only, or all for dark and light colors")
var style: String
- @Option(help: "Path of xcassets where to generate colors")
+ @Option(help: "Path of xcassets where to generate colors", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var xcassetsPath: String
- @Option(help: "Path where to generate the extension.")
+ @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var extensionOutputPath: String
- @Option(help: "Extension name. If not specified, it will generate an UIFont extension")
+ @Option(help: "Extension name. If not specified, it will generate an UIColor extension. Using default extension name will generate static property.")
var extensionName: String = Self.defaultExtensionName
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift")
diff --git a/Sources/FontToolCore/FontToolError.swift b/Sources/FontToolCore/FontToolError.swift
index d932155..1eb6512 100644
--- a/Sources/FontToolCore/FontToolError.swift
+++ b/Sources/FontToolCore/FontToolError.swift
@@ -1,5 +1,5 @@
//
-// File.swift
+// FontToolError.swift
//
//
// Created by Thibaut Schmitt on 13/12/2021.
@@ -7,7 +7,6 @@
import Foundation
-
enum FontToolError: Error {
case fcScan(String, Int32, String?)
case inputFolderNotFound(String)
@@ -22,7 +21,7 @@ enum FontToolError: Error {
return " error:[FontTool] Input folder not found: \(inputFolder)"
case .fileNotExists(let filename):
- return " error:[FontTool] File \(filename) does not exists "
+ return " error:[FontTool] File \(filename) does not exists"
}
}
}
diff --git a/Sources/FontToolCore/main.swift b/Sources/FontToolCore/main.swift
index 099a788..a2ee033 100644
--- a/Sources/FontToolCore/main.swift
+++ b/Sources/FontToolCore/main.swift
@@ -9,27 +9,22 @@ import Foundation
import CLIToolCore
import ArgumentParser
-/*
- Lire l'infoPlist et check si les fonts dedans sont les memes que celles à générer
- */
-
-//swift run -c release FontToolCore ./SampleFiles/Fonts/sampleFonts.txt --extension-output-path ~/Desktop --extension-name R2Font
struct FontTool: ParsableCommand {
static let defaultExtensionName = "UIFont"
@Flag(name: .customShort("f"), help: "Should force generation")
var forceGeneration = false
- @Argument(help: "Input files where fonts ared defined.")
+ @Argument(help: "Input files where fonts ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String
- @Option(help: "Path where to generate the extension.")
+ @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var extensionOutputPath: String
- @Option(help: "Extension name. If not specified, it will generate an UIFont extension")
+ @Option(help: "Extension name. If not specified, it will generate an UIFont extension. Using default extension name will generate static property.")
var extensionName: String = Self.defaultExtensionName
- @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift")
+ @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift")
var extensionSuffix: String = ""
var extensionFileName: String { "\(extensionName)+Font\(extensionSuffix).swift" }
diff --git a/Sources/Strings/Generator/StringsFileGenerator.swift b/Sources/Strings/Generator/StringsFileGenerator.swift
new file mode 100644
index 0000000..35ff919
--- /dev/null
+++ b/Sources/Strings/Generator/StringsFileGenerator.swift
@@ -0,0 +1,143 @@
+//
+// StringsFileGenerator.swift
+//
+//
+// Created by Thibaut Schmitt on 04/01/2022.
+//
+
+import Foundation
+import CLIToolCore
+
+extension Strings {
+ class StringsFileGenerator {
+
+ // MARK: - Strings Files
+
+ static func writeStringsFiles(sections: [Section], langs: [String], defaultLang: String, tags: [String], outputPath: String, inputFilenameWithoutExt: String) {
+ var stringsFilesContent = [String: String]()
+ for lang in langs {
+ stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang,
+ defaultLang: defaultLang,
+ tags: tags,
+ sections: sections)
+ }
+
+ // Write strings file content
+ langs.forEach { lang in
+ guard let fileContent = stringsFilesContent[lang] else { return }
+
+ let stringsFilePath = "\(outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings"
+ let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
+ do {
+ try fileContent.write(to: stringsFilePathURL, atomically: true, encoding: .utf8)
+ } catch (let error) {
+ let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+ }
+ }
+
+ private static func generateStringsFileContent(lang: String, defaultLang: String, tags inputTags: [String], sections: [Section]) -> String {
+ var stringsFileContent = """
+ /**
+ * Apple Strings File
+ * Generated by ResgenSwift 1.0.0
+ * Language: \(lang)
+ */\n
+ """
+
+ sections.forEach { section in
+ // Check that at least one string will be generated
+ guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
+ return // Go to next section
+ }
+
+ stringsFileContent += "\n/********** \(section.name) **********/\n\n"
+ section.definitions.forEach { definition in
+ let translationOpt: String? = {
+ if definition.tags.contains(Stringium.noTranslationTag) {
+ return definition.translations[defaultLang]
+ }
+ return definition.translations[lang]
+ }()
+
+ if let translation = translationOpt {
+ stringsFileContent += "\"\(definition.name)\" = \"\(translation)\"\n\n"
+ } else {
+ let error = StringiumError.langNotDefined(lang, definition.name, definition.reference != nil)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+ }
+ }
+
+ return stringsFileContent
+ }
+
+ // MARK: - Extension file
+
+ static func writeExtensionFiles(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool, inputFilename: String, extensionName: String, extensionFilePath: String) {
+ let extensionHeader = Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName)
+ let extensionFooter = Self.getFooter()
+
+ let extensionContent: String = {
+ var content = ""
+ sections.forEach { section in
+ // Check that at least one string will be generated
+ guard section.hasOneOrMoreMatchingTags(tags: tags) else {
+ return // Go to next section
+ }
+
+ content += "\n\t// MARK: - \(section.name)"
+ section.definitions.forEach { definition in
+ if staticVar {
+ content += "\n\n\(definition.getNSLocalizedStringStaticProperty(forLang: lang))"
+ } else {
+ content += "\n\n\(definition.getNSLocalizedStringProperty(forLang: lang))"
+ }
+ }
+ content += "\n"
+ }
+ return content
+ }()
+
+ // Create file if not exists
+ let fileManager = FileManager()
+ if fileManager.fileExists(atPath: extensionFilePath) == false {
+ Shell.shell("touch", "\(extensionFilePath)")
+ }
+
+ // Create extension content
+ let extensionFileContent = [extensionHeader, extensionContent, extensionFooter].joined(separator: "\n")
+
+ // Write content
+ let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
+ do {
+ try extensionFileContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
+ } catch (let error) {
+ let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+ }
+
+ private static func getHeader(stringsFilename: String, extensionClassname: String) -> String {
+ """
+ // Generated from Strings-Stringium at \(Date())
+
+ import UIKit
+
+ fileprivate let kStringsFileName = "\(stringsFilename)"
+
+ extension \(extensionClassname) {
+ """
+ }
+
+ private static func getFooter() -> String {
+ """
+ }
+ """
+ }
+ }
+}
diff --git a/Sources/Strings/Generator/TagsGenerator.swift b/Sources/Strings/Generator/TagsGenerator.swift
new file mode 100644
index 0000000..925c1d8
--- /dev/null
+++ b/Sources/Strings/Generator/TagsGenerator.swift
@@ -0,0 +1,77 @@
+//
+// TagsGenerator.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import CLIToolCore
+import CoreVideo
+
+extension Strings {
+ class TagsGenerator {
+ static func writeExtensionFiles(sections: [Section], lang: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
+ let extensionHeader = Self.getHeader(extensionClassname: extensionName)
+ let extensionFooter = Self.getFooter()
+
+ let extensionContent: String = {
+ var content = ""
+ sections.forEach { section in
+ // Check that at least one string will be generated
+ guard section.hasOneOrMoreMatchingTags(tags: tags) else {
+ return // Go to next section
+ }
+
+ content += "\n\t// MARK: - \(section.name)"
+ section.definitions.forEach { definition in
+ if staticVar {
+ content += "\n\n\(definition.getStaticProperty(forLang: lang))"
+ } else {
+ content += "\n\n\(definition.getProperty(forLang: lang))"
+ }
+ }
+ content += "\n"
+ }
+ return content
+ }()
+
+ // Create file if not exists
+ let fileManager = FileManager()
+ if fileManager.fileExists(atPath: extensionFilePath) == false {
+ Shell.shell("touch", "\(extensionFilePath)")
+ }
+
+ // Create extension content
+ let extensionFileContent = [extensionHeader, extensionContent, extensionFooter].joined(separator: "\n")
+
+ // Write content
+ let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
+ do {
+ try extensionFileContent.write(to: extensionFilePathURL, atomically: true, encoding: .utf8)
+ } catch (let error) {
+ let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+ }
+
+ private static func getHeader(extensionClassname: String) -> String {
+ """
+ // Generated from Strings-Tags at \(Date())
+
+ // typelias Tags = String
+
+ import UIKit
+
+ extension \(extensionClassname) {
+ """
+ }
+
+ private static func getFooter() -> String {
+ """
+ }
+ """
+ }
+ }
+}
diff --git a/Sources/Strings/Model/Definition.swift b/Sources/Strings/Model/Definition.swift
new file mode 100644
index 0000000..b556dcc
--- /dev/null
+++ b/Sources/Strings/Model/Definition.swift
@@ -0,0 +1,108 @@
+//
+// Definition.swift
+//
+//
+// Created by Thibaut Schmitt on 04/01/2022.
+//
+
+import Foundation
+
+extension Strings {
+ class Definition {
+ let name: String
+ var tags = [String]()
+ var comment: String?
+ var translations = [String: String]()
+ var reference: String?
+ var isPlurals = false
+
+ var isValid: Bool {
+ name.isEmpty == false &&
+ translations.isEmpty == false
+ }
+
+ init(name: String) {
+ self.name = name
+ }
+
+ static func match(_ line: String) -> Definition? {
+ guard line.range(of: "\\[(.*?)]", options: .regularExpression, range: nil, locale: nil) != nil else {
+ return nil
+ }
+
+ let definitionName = line
+ .replacingOccurrences(of: ["[", "]"], with: "")
+ .removeLeadingTrailingWhitespace()
+
+ return Definition(name: definitionName)
+ }
+
+ // MARK: -
+
+ func getNSLocalizedStringProperty(forLang lang: String) -> String {
+ guard let translation = translations[lang] else {
+ let error = StringiumError.langNotDefined(lang, name, reference != nil)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ return """
+ /// Translation in \(lang) :
+ /// \(translation)
+ var \(name): String {
+ NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "")
+ }
+ """
+ }
+
+ func getNSLocalizedStringStaticProperty(forLang lang: String) -> String {
+ guard let translation = translations[lang] else {
+ let error = StringiumError.langNotDefined(lang, name, reference != nil)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ return """
+ /// Translation in \(lang) :
+ /// \(translation)
+ static var \(name): String {
+ NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "")
+ }
+ """
+ }
+
+ // MARK: - Raw strings
+
+ func getProperty(forLang lang: String) -> String {
+ guard let translation = translations[lang] else {
+ let error = StringiumError.langNotDefined(lang, name, reference != nil)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ return """
+ /// Translation in \(lang) :
+ /// \(translation)
+ var \(name): String {
+ "\(translation)"
+ }
+ """
+ }
+
+ func getStaticProperty(forLang lang: String) -> String {
+ guard let translation = translations[lang] else {
+ let error = StringiumError.langNotDefined(lang, name, reference != nil)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ return """
+ /// Translation in \(lang) :
+ /// \(translation)
+ static var \(name): String {
+ "\(translation)"
+ }
+ """
+ }
+ }
+}
diff --git a/Sources/Strings/Model/Section.swift b/Sources/Strings/Model/Section.swift
new file mode 100644
index 0000000..2d598b5
--- /dev/null
+++ b/Sources/Strings/Model/Section.swift
@@ -0,0 +1,41 @@
+//
+// Section.swift
+//
+//
+// Created by Thibaut Schmitt on 04/01/2022.
+//
+
+import Foundation
+
+extension Strings {
+ class Section {
+ let name: String // OnBoarding
+ var definitions = [Definition]()
+
+ init(name: String) {
+ self.name = name
+ }
+
+ static func match(_ line: String) -> Section? {
+ guard line.range(of: "\\[\\[(.*?)]]", options: .regularExpression, range: nil, locale: nil) != nil else {
+ return nil
+ }
+
+ let sectionName = line
+ .replacingOccurrences(of: ["[", "]"], with: "")
+ .removeLeadingTrailingWhitespace()
+ return Section(name: sectionName)
+ }
+
+ func hasOneOrMoreMatchingTags(tags: [String]) -> Bool {
+ let allTags = definitions.flatMap { $0.tags }
+
+ for tag in tags {
+ if allTags.contains(tag) {
+ return true
+ }
+ }
+ return false
+ }
+ }
+}
diff --git a/Sources/Strings/Parser/TwineFileParser.swift b/Sources/Strings/Parser/TwineFileParser.swift
new file mode 100644
index 0000000..cd075e1
--- /dev/null
+++ b/Sources/Strings/Parser/TwineFileParser.swift
@@ -0,0 +1,94 @@
+//
+// TwineFileParser.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+
+extension Strings {
+
+ class TwineFileParser {
+ static func parse(_ inputFile: String) -> [Section] {
+ let inputFileContent = try! String(contentsOfFile: inputFile, encoding: .utf8)
+ let stringsByLines = inputFileContent.components(separatedBy: .newlines)
+
+ var sections = [Section]()
+
+ // Parse file
+ stringsByLines.forEach {
+ // Section
+ if let section = Section.match($0) {
+ sections.append(section)
+ return
+ }
+
+ // Definition
+ if let definition = Definition.match($0) {
+ sections.last?.definitions.append(definition)
+ return
+ }
+
+ // Definition content
+ if $0.isEmpty == false {
+ // fr = Test => ["fr ", " Test"]
+ let splitLine = $0
+ .removeLeadingTrailingWhitespace()
+ .split(separator: "=")
+
+ guard let lastDefinition = sections.last?.definitions.last,
+ let leftElement = splitLine.first,
+ let rightElement = splitLine.last else {
+ return
+ }
+
+ // "fr " => "fr"
+ let leftHand = String(leftElement.dropLast())
+ // " Test" => "Test"
+ let rightHand = String(rightElement.dropFirst())
+
+ // Handle comments, tags and translation
+ switch leftHand {
+ case "comments":
+ lastDefinition.comment = rightHand
+
+ case "tags":
+ lastDefinition.tags = rightHand
+ .split(separator: ",")
+ .map { String($0) }
+
+ case "ref":
+ lastDefinition.reference = rightHand
+
+ default:
+ lastDefinition.translations[leftHand] = rightHand.escapeDoubleQuote()
+ // Is a plurals strings (fr:one = Test)
+ // Will be handle later
+ //if leftHand.split(separator: ":").count > 1 {
+ // lastDefinition.isPlurals = true
+ //}
+ }
+ }
+ }
+
+ // Keep only valid definition
+ var invalidDefinitionNames = [String]()
+ sections.forEach { section in
+ section.definitions = section.definitions
+ .filter {
+ if $0.isValid == false {
+ invalidDefinitionNames.append($0.name)
+ return false
+ }
+ return true
+ }
+ }
+ if invalidDefinitionNames.count > 0 {
+ print(" warning:[\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
+ }
+
+ return sections
+ }
+ }
+}
diff --git a/Sources/Strings/Stringium/Stringium.swift b/Sources/Strings/Stringium/Stringium.swift
new file mode 100644
index 0000000..3b6c5b9
--- /dev/null
+++ b/Sources/Strings/Stringium/Stringium.swift
@@ -0,0 +1,111 @@
+//
+// Stringium.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import CLIToolCore
+import ArgumentParser
+
+extension Strings {
+
+ struct Stringium: ParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Generate strings with custom scripts.")
+
+ static let toolName = "Stringium"
+ static let defaultExtensionName = "String"
+ static let noTranslationTag: String = "notranslation"
+
+ var extensionFileName: String { "\(options.extensionName)+String\(options.extensionSuffix).swift" }
+ var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
+
+ var langs: [String] {
+ options.langsRaw
+ .split(separator: " ")
+ .map { String($0) }
+ }
+ var inputFilenameWithoutExt: String {
+ URL(fileURLWithPath: options.inputFile)
+ .deletingPathExtension()
+ .lastPathComponent
+ }
+ var stringsFileOutputPath: String {
+ var outputPath = options.outputPathRaw
+ if outputPath.last == "/" {
+ outputPath = String(outputPath.dropLast())
+ }
+ return outputPath
+ }
+
+ // The `@OptionGroup` attribute includes the flags, options, and
+ // arguments defined by another `ParsableArguments` type.
+ @OptionGroup var options: StringiumOptions
+
+ mutating func run() {
+ print("[\(Self.toolName)] Starting strings generation")
+
+ // Check requirements
+ guard checkRequirements() else { return }
+
+ print("[\(Self.toolName)] Will generate strings")
+
+ // Parse input file
+ let sections = TwineFileParser.parse(options.inputFile)
+
+ // Generate strings files
+ StringsFileGenerator.writeStringsFiles(sections: sections,
+ langs: langs,
+ defaultLang: options.defaultLang,
+ tags: ["ios", "iosonly", Self.noTranslationTag],
+ outputPath: stringsFileOutputPath,
+ inputFilenameWithoutExt: inputFilenameWithoutExt)
+
+ // Generate extension
+ StringsFileGenerator.writeExtensionFiles(sections: sections,
+ defaultLang: options.defaultLang,
+ tags: ["ios", "iosonly", Self.noTranslationTag],
+ staticVar: options.extensionName == Self.defaultExtensionName,
+ inputFilename: inputFilenameWithoutExt,
+ extensionName: options.extensionName,
+ extensionFilePath: extensionFilePath)
+
+ print("[\(Self.toolName)] Strings generated")
+ }
+
+ // MARK: - Requirements
+
+ private func checkRequirements() -> Bool {
+ let fileManager = FileManager()
+
+ // Input file
+ guard fileManager.fileExists(atPath: options.inputFile) else {
+ let error = StringiumError.fileNotExists(options.inputFile)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ // Langs
+ guard langs.isEmpty == false else {
+ let error = StringiumError.langsListEmpty
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ guard langs.contains(options.defaultLang) else {
+ let error = StringiumError.defaultLangsNotInLangs
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ // Check if needed to regenerate
+ guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
+ print("[\(Self.toolName)] Strings are already up to date :) ")
+ return false
+ }
+
+ return true
+ }
+ }
+}
diff --git a/Sources/Strings/Stringium/StringiumError.swift b/Sources/Strings/Stringium/StringiumError.swift
new file mode 100644
index 0000000..988ba9d
--- /dev/null
+++ b/Sources/Strings/Stringium/StringiumError.swift
@@ -0,0 +1,40 @@
+//
+// StringToolError.swift
+//
+//
+// Created by Thibaut Schmitt on 05/01/2022.
+//
+
+import Foundation
+
+extension Strings {
+ enum StringiumError: Error {
+ case fileNotExists(String)
+ case langsListEmpty
+ case defaultLangsNotInLangs
+ case writeFile(String, String)
+ case langNotDefined(String, String, Bool)
+
+ var localizedDescription: String {
+ switch self {
+ case .fileNotExists(let filename):
+ return " error:[\(Stringium.toolName)] File \(filename) does not exists "
+
+ case .langsListEmpty:
+ return " error:[\(Stringium.toolName)] Langs list is empty"
+
+ case .defaultLangsNotInLangs:
+ return " error:[\(Stringium.toolName)] Langs list does not contains the default lang"
+
+ case .writeFile(let subErrorDescription, let filename):
+ return " error:[\(Stringium.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
+
+ case .langNotDefined(let lang, let definitionName, let isReference):
+ if isReference {
+ return " error:[\(Stringium.toolName)] Reference are handled only by TwineTool. Please use it or remove reference from you strings file."
+ }
+ return " error:[\(Stringium.toolName)] Lang \"\(lang)\" not found for \"\(definitionName)\""
+ }
+ }
+ }
+}
diff --git a/Sources/Strings/Stringium/StringiumOptions.swift b/Sources/Strings/Stringium/StringiumOptions.swift
new file mode 100644
index 0000000..9d14ed1
--- /dev/null
+++ b/Sources/Strings/Stringium/StringiumOptions.swift
@@ -0,0 +1,37 @@
+//
+// StringiumOptions.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import ArgumentParser
+
+extension Strings {
+ struct StringiumOptions: ParsableArguments {
+ @Flag(name: .customShort("f"), help: "Should force generation")
+ var forceGeneration = false
+
+ @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var inputFile: String
+
+ @Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var outputPathRaw: String
+
+ @Option(name: .customLong("langs"), help: "Langs to generate.")
+ var langsRaw: String
+
+ @Option(help: "Default langs.")
+ var defaultLang: String
+
+ @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var extensionOutputPath: String
+
+ @Option(help: "Extension name. If not specified, it will generate an String extension. Using default extension name will generate static property.")
+ var extensionName: String = Stringium.defaultExtensionName
+
+ @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+String{extensionSuffix}.swift")
+ var extensionSuffix: String = ""
+ }
+}
diff --git a/Sources/Strings/Tag/Tags.swift b/Sources/Strings/Tag/Tags.swift
new file mode 100644
index 0000000..efe3028
--- /dev/null
+++ b/Sources/Strings/Tag/Tags.swift
@@ -0,0 +1,71 @@
+//
+// Tag.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import CLIToolCore
+import ArgumentParser
+
+extension Strings {
+
+ struct Tags: ParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Generate tags extension file.")
+
+ static let toolName = "Tags"
+ static let defaultExtensionName = "Tags"
+ static let noTranslationTag: String = "notranslation"
+
+ var extensionFileName: String { "\(options.extensionName)+Tag\(options.extensionSuffix).swift" }
+ var extensionFilePath: String { "\(options.extensionOutputPath)/\(extensionFileName)" }
+
+ // The `@OptionGroup` attribute includes the flags, options, and
+ // arguments defined by another `ParsableArguments` type.
+ @OptionGroup var options: TagsOptions
+
+ mutating func run() {
+ print("[\(Self.toolName)] Starting tagss generation")
+
+ // Check requirements
+ guard checkRequirements() else { return }
+
+ print("[\(Self.toolName)] Will generate tags")
+
+ // Parse input file
+ let sections = TwineFileParser.parse(options.inputFile)
+
+ // Generate extension
+ TagsGenerator.writeExtensionFiles(sections: sections,
+ lang: options.lang,
+ tags: ["ios", "iosonly", Self.noTranslationTag],
+ staticVar: options.extensionName == Self.defaultExtensionName,
+ extensionName: options.extensionName,
+ extensionFilePath: extensionFilePath)
+
+ print("[\(Self.toolName)] Tags generated")
+ }
+
+ // MARK: - Requirements
+
+ private func checkRequirements() -> Bool {
+ let fileManager = FileManager()
+
+ // Input file
+ guard fileManager.fileExists(atPath: options.inputFile) else {
+ let error = StringiumError.fileNotExists(options.inputFile)
+ print(error.localizedDescription)
+ Stringium.exit(withError: error)
+ }
+
+ // Check if needed to regenerate
+ guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePath) else {
+ print("[\(Self.toolName)] Tags are already up to date :) ")
+ return false
+ }
+
+ return true
+ }
+ }
+}
diff --git a/Sources/Strings/Tag/TagsOptions.swift b/Sources/Strings/Tag/TagsOptions.swift
new file mode 100644
index 0000000..527ea57
--- /dev/null
+++ b/Sources/Strings/Tag/TagsOptions.swift
@@ -0,0 +1,31 @@
+//
+// TagOptions.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import ArgumentParser
+
+extension Strings {
+ struct TagsOptions: ParsableArguments {
+ @Flag(name: .customShort("f"), help: "Should force generation")
+ var forceGeneration = false
+
+ @Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var inputFile: String
+
+ @Option(help: "Lang to generate. (\"ium\" by default)")
+ var lang: String = "ium"
+
+ @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var extensionOutputPath: String
+
+ @Option(help: "Extension name. If not specified, it will generate a Tag extension. Using default extension name will generate static property.")
+ var extensionName: String = Tags.defaultExtensionName
+
+ @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Tag{extensionSuffix}.swift")
+ var extensionSuffix: String = ""
+ }
+}
diff --git a/Sources/Strings/Twine/Twine.swift b/Sources/Strings/Twine/Twine.swift
new file mode 100644
index 0000000..74f89cc
--- /dev/null
+++ b/Sources/Strings/Twine/Twine.swift
@@ -0,0 +1,97 @@
+//
+// Twine.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import CLIToolCore
+import ArgumentParser
+
+extension Strings {
+
+ struct Twine: ParsableCommand {
+ static var configuration = CommandConfiguration(abstract: "Generate strings with twine.")
+
+ static let toolName = "Twine"
+ static let defaultExtensionName = "String"
+ static let twineExecutable = "\(FileManager.default.homeDirectoryForCurrentUser.relativePath)/scripts/twine/twine"
+
+ var langs: [String] { options.langsRaw.split(separator: " ").map { String($0) } }
+ var inputFilenameWithoutExt: String { URL(fileURLWithPath: options.inputFile)
+ .deletingPathExtension()
+ .lastPathComponent
+ }
+
+ // The `@OptionGroup` attribute includes the flags, options, and
+ // arguments defined by another `ParsableArguments` type.
+ @OptionGroup var options: TwineOptions
+
+ mutating func run() {
+ print("[\(Self.toolName)] Starting strings generation")
+
+ // Check requirements
+ guard checkRequirements() else { return }
+
+ print("[\(Self.toolName)] Will generate strings")
+
+ // Generate strings files (lproj files)
+ for lang in langs {
+ Shell.shell(Self.twineExecutable,
+ "generate-localization-file", options.inputFile,
+ "--lang", "\(lang)",
+ "\(options.outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings",
+ "--tags=ios,iosonly,iosOnly")
+ }
+
+ // Generate extension
+ var extensionFilePath: String { "\(options.extensionOutputPath)/\(inputFilenameWithoutExt).swift" }
+ Shell.shell(Self.twineExecutable,
+ "generate-localization-file", options.inputFile,
+ "--format", "apple-swift",
+ "--lang", "\(options.defaultLang)",
+ extensionFilePath,
+ "--tags=ios,iosonly,iosOnly")
+
+ print("[\(Self.toolName)] Strings generated")
+ }
+
+ // MARK: - Requirements
+
+ private func checkRequirements() -> Bool {
+ let fileManager = FileManager()
+
+ // Input file
+ guard fileManager.fileExists(atPath: options.inputFile) else {
+ let error = TwineError.fileNotExists(options.inputFile)
+ print(error.localizedDescription)
+ Twine.exit(withError: error)
+ }
+
+ // Langs
+ guard langs.isEmpty == false else {
+ let error = TwineError.langsListEmpty
+ print(error.localizedDescription)
+ Twine.exit(withError: error)
+ }
+
+ guard langs.contains(options.defaultLang) else {
+ let error = TwineError.defaultLangsNotInLangs
+ print(error.localizedDescription)
+ Twine.exit(withError: error)
+ }
+
+ // "R2String+" is hardcoded in Twine formatter
+ let extensionFilePathGenerated = "\(options.extensionOutputPath)/R2String+\(inputFilenameWithoutExt).swift"
+
+ // Check if needed to regenerate
+ guard GeneratorChecker.shouldGenerate(force: options.forceGeneration, inputFilePath: options.inputFile, extensionFilePath: extensionFilePathGenerated) else {
+ print("[\(Self.toolName)] Strings are already up to date :) ")
+ return false
+ }
+
+ return true
+ }
+ }
+}
diff --git a/Sources/Strings/Twine/TwineError.swift b/Sources/Strings/Twine/TwineError.swift
new file mode 100644
index 0000000..2f0c7af
--- /dev/null
+++ b/Sources/Strings/Twine/TwineError.swift
@@ -0,0 +1,31 @@
+//
+// TwineError.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+
+extension Strings {
+
+ enum TwineError: Error {
+ case fileNotExists(String)
+ case langsListEmpty
+ case defaultLangsNotInLangs
+
+ var localizedDescription: String {
+ switch self {
+ case .fileNotExists(let filename):
+ return " error:[\(Twine.toolName)] File \(filename) does not exists "
+
+ case .langsListEmpty:
+ return " error:[\(Twine.toolName)] Langs list is empty"
+
+ case .defaultLangsNotInLangs:
+ return " error:[\(Twine.toolName)] Langs list does not contains the default lang"
+ }
+ }
+ }
+
+}
diff --git a/Sources/Strings/Twine/TwineOptions.swift b/Sources/Strings/Twine/TwineOptions.swift
new file mode 100644
index 0000000..167f956
--- /dev/null
+++ b/Sources/Strings/Twine/TwineOptions.swift
@@ -0,0 +1,29 @@
+//
+// File.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import ArgumentParser
+
+struct TwineOptions: ParsableArguments {
+ @Flag(name: .customShort("f"), help: "Should force generation")
+ var forceGeneration = false
+
+ @Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var inputFile: String
+
+ @Option(help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var outputPath: String
+
+ @Option(name: .customLong("langs"), help: "Langs to generate.")
+ var langsRaw: String
+
+ @Option(help: "Default langs.")
+ var defaultLang: String
+
+ @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
+ var extensionOutputPath: String
+}
diff --git a/Sources/Strings/main.swift b/Sources/Strings/main.swift
new file mode 100644
index 0000000..9b70151
--- /dev/null
+++ b/Sources/Strings/main.swift
@@ -0,0 +1,28 @@
+//
+// main.swift
+//
+//
+// Created by Thibaut Schmitt on 10/01/2022.
+//
+
+import Foundation
+import ArgumentParser
+
+struct Strings: ParsableCommand {
+
+ static var configuration = CommandConfiguration(
+ abstract: "A utility for generate strings.",
+ version: "0.1.0",
+
+ // Pass an array to `subcommands` to set up a nested tree of subcommands.
+ // With language support for type-level introspection, this could be
+ // provided by automatically finding nested `ParsableCommand` types.
+ subcommands: [Twine.self, Stringium.self, Tags.self]
+
+ // A default subcommand, when provided, is automatically selected if a
+ // subcommand is not given on the command line.
+ //defaultSubcommand: Twine.self
+ )
+}
+
+Strings.main()