Generation des strings sans twine et generation des tags. Refactor de toutes les commandes strings (Twine, CustomStrings, Tags) dans une commande avec des sous commandes

This commit is contained in:
Thibaut Schmitt 2022-01-10 12:01:09 +01:00
parent 4973b245ad
commit b0900c10cd
39 changed files with 1519 additions and 40 deletions

View File

@ -76,6 +76,48 @@
ReferencedContainer = "container:"> ReferencedContainer = "container:">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "StringToolCore"
BuildableName = "StringToolCore"
BlueprintName = "StringToolCore"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TwineToolCore"
BuildableName = "TwineToolCore"
BlueprintName = "TwineToolCore"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Strings"
BuildableName = "Strings"
BlueprintName = "Strings"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
@ -106,6 +148,16 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Strings"
BuildableName = "Strings"
BlueprintName = "Strings"
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@ -122,15 +174,16 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<MacroExpansion> <BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "ResgenSwift" BlueprintIdentifier = "Strings"
BuildableName = "ResgenSwift" BuildableName = "Strings"
BlueprintName = "ResgenSwift" BlueprintName = "Strings"
ReferencedContainer = "container:"> ReferencedContainer = "container:">
</BuildableReference> </BuildableReference>
</MacroExpansion> </BuildableProductRunnable>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">

View File

@ -5,6 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "ResgenSwift", name: "ResgenSwift",
platforms: [.macOS(.v10_12)],
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") .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. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "ResgenSwift", name: "ResgenSwift",
dependencies: ["FontToolCore", "ColorToolCore"] dependencies: ["FontToolCore", "ColorToolCore", "TwineToolCore", "StringToolCore", "Strings"]
), ),
.target( .target(
name: "FontToolCore", name: "FontToolCore",
@ -30,6 +31,27 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser") .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 // Helper targets
.target(name: "CLIToolCore"), .target(name: "CLIToolCore"),
// Test targets // Test targets

View File

@ -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 import UIKit

View File

@ -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)!
}
}

View File

@ -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: "")
}
}

View File

@ -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: "")
}
}

View File

@ -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: "")
}
}

View File

@ -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 \"%@\" !"

View File

@ -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 \"%@\" !"

View File

@ -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 \"%@\" !"

View File

@ -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 "%@" !

View File

@ -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"
}
}

View File

@ -0,0 +1,7 @@
[[ScreenTag]]
[screen_one]
ium = Ecran un
tags = droid,ios
[screen_two]
ium = Ecran deux
tags = droid,ios

View File

@ -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: "")
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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

View File

@ -1,15 +1,42 @@
#/bin/bash #/bin/bash
FORCE_FLAG="$1"
# Font # 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-output-path "./Fonts/Generated" \
--extension-name "R2Font" \ --extension-name "UIFont" \
--extension-suffix "GenAllScript" --extension-suffix "GenAllScript"
# Color # Color
swift run -c release ColorToolCore "./Colors/sampleColors1.txt" \ swift run -c release ColorToolCore $FORCE_FLAG "./Colors/sampleColors1.txt" \
--style all \ --style all \
--xcassets-path "./Colors/colors.xcassets" \ --xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \ --extension-output-path "./Colors/Generated/" \
--extension-name "UIColor" \ --extension-name "UIColor" \
--extension-suffix "GenAllScript" --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

View File

@ -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 }
}
}

View File

@ -1,5 +1,5 @@
// //
// File.swift // Shell.swift
// //
// //
// Created by Thibaut Schmitt on 22/12/2021. // Created by Thibaut Schmitt on 22/12/2021.

View File

@ -7,8 +7,6 @@
import Foundation import Foundation
// MARK: - String
public extension String { public extension String {
func removeCharacters(from forbiddenChars: CharacterSet) -> String { func removeCharacters(from forbiddenChars: CharacterSet) -> String {
let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) } let passed = self.unicodeScalars.filter { !forbiddenChars.contains($0) }
@ -19,7 +17,15 @@ public extension String {
return removeCharacters(from: CharacterSet(charactersIn: from)) 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 var newString = self
while newString.last?.isWhitespace == true { while newString.last?.isWhitespace == true {
@ -29,6 +35,33 @@ public extension String {
return newString 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) { func colorComponent() -> (alpha: String, red: String, green: String, blue: String) {
var alpha: String = "FF" var alpha: String = "FF"
var red: String var red: String
@ -52,13 +85,3 @@ public extension String {
return (alpha: alpha, red: red, green: green, blue: blue) 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 }
}
}

View File

@ -21,19 +21,19 @@ struct ColorTool: ParsableCommand {
@Flag(name: .customShort("f"), help: "Should force generation") @Flag(name: .customShort("f"), help: "Should force generation")
var forceGeneration = false 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 var inputFile: String
@Option(help: "Color style to generate: light for light colors only, or all for dark and light colors") @Option(help: "Color style to generate: light for light colors only, or all for dark and light colors")
var style: String 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 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 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 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}+ColorsMyApp.swift")

View File

@ -1,5 +1,5 @@
// //
// File.swift // FontToolError.swift
// //
// //
// Created by Thibaut Schmitt on 13/12/2021. // Created by Thibaut Schmitt on 13/12/2021.
@ -7,7 +7,6 @@
import Foundation import Foundation
enum FontToolError: Error { enum FontToolError: Error {
case fcScan(String, Int32, String?) case fcScan(String, Int32, String?)
case inputFolderNotFound(String) case inputFolderNotFound(String)
@ -22,7 +21,7 @@ enum FontToolError: Error {
return " error:[FontTool] Input folder not found: \(inputFolder)" return " error:[FontTool] Input folder not found: \(inputFolder)"
case .fileNotExists(let filename): case .fileNotExists(let filename):
return " error:[FontTool] File \(filename) does not exists " return " error:[FontTool] File \(filename) does not exists"
} }
} }
} }

View File

@ -9,27 +9,22 @@ import Foundation
import CLIToolCore import CLIToolCore
import ArgumentParser 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 { struct FontTool: ParsableCommand {
static let defaultExtensionName = "UIFont" static let defaultExtensionName = "UIFont"
@Flag(name: .customShort("f"), help: "Should force generation") @Flag(name: .customShort("f"), help: "Should force generation")
var forceGeneration = false 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 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 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 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 extensionSuffix: String = ""
var extensionFileName: String { "\(extensionName)+Font\(extensionSuffix).swift" } var extensionFileName: String { "\(extensionName)+Font\(extensionSuffix).swift" }

View File

@ -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 {
"""
}
"""
}
}
}

View File

@ -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 {
"""
}
"""
}
}
}

View File

@ -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)"
}
"""
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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)\""
}
}
}
}

View File

@ -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 = ""
}
}

View File

@ -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
}
}
}

View File

@ -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 = ""
}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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
}

View File

@ -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()