Optional generation of Strings extension (stringium command)

This commit is contained in:
2025-07-17 14:15:47 +02:00
parent aaeca93cbc
commit 3f7464161c
6 changed files with 245 additions and 58 deletions

View File

@@ -0,0 +1,150 @@
{
"sourceLanguage" : "en",
"strings" : {
"generic_back" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Back"
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "Back"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Retour"
}
}
}
},
"generic_loading_data" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Loading data..."
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "Loading data..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Chargement des données..."
}
}
}
},
"generic_welcome_firstname_format" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Welcome \\\"%@\\\" !"
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "Welcome \\\"%@\\\" !"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bienvenue \\\"%@\\\" !"
}
}
}
},
"param_lang" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "en"
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "en-us"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "fr"
}
}
}
},
"placeholders_test_one" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You %%: %2$@ %1$@ Age: %3$d"
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "You %%: %2$@ %1$@ Age: %3$d"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vous %%: %1$@ %2$@ Age: %3$d"
}
}
}
},
"test_equal_symbol" : {
"comment" : "",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "1€ = 1 point !"
}
},
"en-us" : {
"stringUnit" : {
"state" : "translated",
"value" : "1€ = 1 point !"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "1€ = 1 point !"
}
}
}
}
},
"version" : "1.0"
}

View File

@@ -280,7 +280,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
let outputPath: String let outputPath: String
let langs: String let langs: String
let defaultLang: String let defaultLang: String
let extensionOutputPath: String let extensionOutputPath: String?
let extensionName: String? let extensionName: String?
let extensionSuffix: String? let extensionSuffix: String?
private let staticMembers: Bool? private let staticMembers: Bool?
@@ -305,7 +305,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
outputPath: String, outputPath: String,
langs: String, langs: String,
defaultLang: String, defaultLang: String,
extensionOutputPath: String, extensionOutputPath: String?,
extensionName: String?, extensionName: String?,
extensionSuffix: String?, extensionSuffix: String?,
staticMembers: Bool?, staticMembers: Bool?,
@@ -329,7 +329,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
- Output path: \(outputPath) - Output path: \(outputPath)
- Langs: \(langs) - Langs: \(langs)
- Default lang: \(defaultLang) - Default lang: \(defaultLang)
- Extension output path: \(extensionOutputPath) - Extension output path: \(extensionOutputPath ?? "-")
- Extension name: \(extensionName ?? "-") - Extension name: \(extensionName ?? "-")
- Extension suffix: \(extensionSuffix ?? "-") - Extension suffix: \(extensionSuffix ?? "-")
""" """

View File

@@ -24,26 +24,24 @@ extension StringsConfiguration: Runnable {
langs, langs,
"--default-lang", "--default-lang",
defaultLang, defaultLang,
"--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members", "--static-members",
"\(staticMembersOptions)", "\(staticMembersOptions)",
"--xc-strings", "--xc-strings",
"\(xcStringsOptions)" "\(xcStringsOptions)"
] ]
if let extensionName { // Add optional parameters
args += [ [
"--extension-name", ("--extension-output-path", extensionOutputPath?.prependIfRelativePath(projectDirectory)),
extensionName ("--extension-name", extensionName),
] ("--extension-suffix", extensionSuffix)
} ].forEach { argumentName, argumentValue in
if let argumentValue {
if let extensionSuffix { args += [
args += [ argumentName,
"--extension-suffix", argumentValue
extensionSuffix ]
] }
} }
Stringium.main(args) Stringium.main(args)

View File

@@ -19,8 +19,7 @@ enum StringsFileGenerator {
langs: [String], langs: [String],
defaultLang: String, defaultLang: String,
tags: [String], tags: [String],
outputPath: String, lprojPathFormat: String
inputFilenameWithoutExt: String
) { ) {
var stringsFilesContent = [String: String]() var stringsFilesContent = [String: String]()
@@ -37,7 +36,7 @@ enum StringsFileGenerator {
langs.forEach { lang in langs.forEach { lang in
guard let fileContent = stringsFilesContent[lang] else { return } guard let fileContent = stringsFilesContent[lang] else { return }
let stringsFilePath = "\(outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings" let stringsFilePath = String(format: lprojPathFormat, lang)
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do { do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
@@ -54,8 +53,7 @@ enum StringsFileGenerator {
langs: [String], langs: [String],
defaultLang: String, defaultLang: String,
tags: [String], tags: [String],
outputPath: String, xcStringsFilePath: String
inputFilenameWithoutExt: String
) { ) {
let fileContent: String = Self.generateXcStringsFileContent( let fileContent: String = Self.generateXcStringsFileContent(
@@ -65,12 +63,11 @@ enum StringsFileGenerator {
sections: sections sections: sections
) )
let stringsFilePath = "\(outputPath)/\(inputFilenameWithoutExt).xcstrings" let stringsFilePathURL = URL(fileURLWithPath: xcStringsFilePath)
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do { do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
} catch { } catch {
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) let error = StringiumError.writeFile(error.localizedDescription, xcStringsFilePath)
print(error.description) print(error.description)
Stringium.exit(withError: error) Stringium.exit(withError: error)
} }

View File

@@ -21,7 +21,6 @@ struct Stringium: ParsableCommand {
// MARK: - Static // MARK: - Static
static let toolName = "Stringium" static let toolName = "Stringium"
static let defaultExtensionName = "String"
static let noTranslationTag: String = "notranslation" static let noTranslationTag: String = "notranslation"
// MARK: - Command options // MARK: - Command options
@@ -49,8 +48,7 @@ struct Stringium: ParsableCommand {
langs: options.langs, langs: options.langs,
defaultLang: options.defaultLang, defaultLang: options.defaultLang,
tags: options.tags, tags: options.tags,
outputPath: options.stringsFileOutputPath, lprojPathFormat: options.lprojPathFormat
inputFilenameWithoutExt: options.inputFilenameWithoutExt
) )
} else { } else {
StringsFileGenerator.writeXcStringsFiles( StringsFileGenerator.writeXcStringsFiles(
@@ -58,22 +56,25 @@ struct Stringium: ParsableCommand {
langs: options.langs, langs: options.langs,
defaultLang: options.defaultLang, defaultLang: options.defaultLang,
tags: options.tags, tags: options.tags,
outputPath: options.stringsFileOutputPath, xcStringsFilePath: options.xcStringsFilePath
inputFilenameWithoutExt: options.inputFilenameWithoutExt
) )
} }
// Generate extension // Generate extension
StringsFileGenerator.writeExtensionFiles( if let extensionName = options.extensionName,
sections: sections, let extensionFilePath = options.extensionFilePath {
defaultLang: options.defaultLang, print("Will generate extensions")
tags: options.tags, StringsFileGenerator.writeExtensionFiles(
staticVar: options.staticMembers, sections: sections,
inputFilename: options.inputFilenameWithoutExt, defaultLang: options.defaultLang,
extensionName: options.extensionName, tags: options.tags,
extensionFilePath: options.extensionFilePath, staticVar: options.staticMembers,
extensionSuffix: options.extensionSuffix inputFilename: options.inputFilenameWithoutExt,
) extensionName: extensionName,
extensionFilePath: extensionFilePath,
extensionSuffix: options.extensionSuffix ?? ""
)
}
print("[\(Self.toolName)] Strings generated") print("[\(Self.toolName)] Strings generated")
} }
@@ -104,10 +105,18 @@ struct Stringium: ParsableCommand {
} }
// Check if needed to regenerate // Check if needed to regenerate
let fileToCompareToInput: String = {
// If there is no extension file to compare
// Then check the xcassets file instead
if options.xcStrings {
return options.xcStringsFilePath
}
return String(format: options.lprojPathFormat, options.defaultLang)
}()
guard GeneratorChecker.shouldGenerate( guard GeneratorChecker.shouldGenerate(
force: options.forceGeneration, force: options.forceGeneration,
inputFilePath: options.inputFile, inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath extensionFilePath: fileToCompareToInput
) else { ) else {
print("[\(Self.toolName)] Strings are already up to date :) ") print("[\(Self.toolName)] Strings are already up to date :) ")
return false return false

View File

@@ -15,35 +15,51 @@ struct StringiumOptions: ParsableArguments {
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
var forceGeneration = false var forceGeneration = false
@Argument(help: "Input files where strings are defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) @Argument(
help: "Input files where strings are defined.",
transform: { $0.replaceTiltWithHomeDirectoryPath() }
)
var inputFile: String var inputFile: String
@Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) @Option(
name: .customLong("output-path"),
help: "Path where to strings file.",
transform: { $0.replaceTiltWithHomeDirectoryPath()}
)
fileprivate var outputPathRaw: String fileprivate var outputPathRaw: String
@Option(name: .customLong("langs"), help: "Langs to generate.") @Option(
name: .customLong("langs"),
help: "Langs to generate."
)
fileprivate var langsRaw: String fileprivate var langsRaw: String
@Option(help: "Default langs.") @Option(help: "Default langs.")
var defaultLang: String var defaultLang: String
@Option(name: .customLong("tags"), help: "Tags to generate.") @Option(
name: .customLong("tags"),
help: "Tags to generate."
)
fileprivate var tagsRaw: String = "ios iosonly iosOnly notranslation" fileprivate var tagsRaw: String = "ios iosonly iosOnly notranslation"
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) @Option(help: "Generate static properties. False by default")
var extensionOutputPath: String
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false var staticMembers: Bool = false
@Option(help: "Tell if it will generate xcStrings file or not") @Option(help: "Tell if it will generate xcStrings file or lproj file. True by default")
var xcStrings: Bool = false var xcStrings: Bool = true
@Option(help: "Extension name. If not specified, it will generate an String extension.") @Option(
var extensionName: String = Stringium.defaultExtensionName help: "Path where to generate the extension.",
transform: { $0.replaceTiltWithHomeDirectoryPath() }
)
var extensionOutputPath: String?
@Option(help: "Extension name. If not specified, no extension will be generated.")
var extensionName: String?
@Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift") @Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift")
var extensionSuffix: String var extensionSuffix: String?
} }
// MARK: - Private var getter // MARK: - Private var getter
@@ -75,12 +91,21 @@ extension StringiumOptions {
extension StringiumOptions { extension StringiumOptions {
var extensionFileName: String { private var extensionFileName: String? {
"\(extensionName)+\(extensionSuffix).swift" if let extensionName {
if let extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
}
return nil
} }
var extensionFilePath: String { var extensionFilePath: String? {
"\(extensionOutputPath)/\(extensionFileName)" if let extensionOutputPath, let extensionFileName {
return "\(extensionOutputPath)/\(extensionFileName)"
}
return nil
} }
var inputFilenameWithoutExt: String { var inputFilenameWithoutExt: String {
@@ -88,4 +113,12 @@ extension StringiumOptions {
.deletingPathExtension() .deletingPathExtension()
.lastPathComponent .lastPathComponent
} }
var xcStringsFilePath: String {
"\(stringsFileOutputPath)/\(inputFilenameWithoutExt).xcstrings"
}
var lprojPathFormat: String {
"\(stringsFileOutputPath)/%@.lproj/\(inputFilenameWithoutExt).strings"
}
} }