From 3f7464161c81c10da13d1c6ec87fc65757ab2f7c Mon Sep 17 00:00:00 2001 From: Thibaut Schmitt Date: Thu, 17 Jul 2025 14:15:47 +0200 Subject: [PATCH] Optional generation of Strings extension (stringium command) --- .../Strings/Generated/sampleStrings.xcstrings | 150 ++++++++++++++++++ .../Generate/Model/ConfigurationFile.swift | 6 +- .../StringsConfiguration+Runnable.swift | 26 ++- .../Generator/StringsFileGenerator.swift | 13 +- .../Strings/Stringium/Stringium.swift | 41 +++-- .../Strings/Stringium/StringiumOptions.swift | 67 ++++++-- 6 files changed, 245 insertions(+), 58 deletions(-) create mode 100644 SampleFiles/Strings/Generated/sampleStrings.xcstrings diff --git a/SampleFiles/Strings/Generated/sampleStrings.xcstrings b/SampleFiles/Strings/Generated/sampleStrings.xcstrings new file mode 100644 index 0000000..323caba --- /dev/null +++ b/SampleFiles/Strings/Generated/sampleStrings.xcstrings @@ -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" +} \ No newline at end of file diff --git a/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift b/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift index ff3b680..44c5d62 100644 --- a/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift +++ b/Sources/ResgenSwift/Generate/Model/ConfigurationFile.swift @@ -280,7 +280,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { let outputPath: String let langs: String let defaultLang: String - let extensionOutputPath: String + let extensionOutputPath: String? let extensionName: String? let extensionSuffix: String? private let staticMembers: Bool? @@ -305,7 +305,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { outputPath: String, langs: String, defaultLang: String, - extensionOutputPath: String, + extensionOutputPath: String?, extensionName: String?, extensionSuffix: String?, staticMembers: Bool?, @@ -329,7 +329,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible { - Output path: \(outputPath) - Langs: \(langs) - Default lang: \(defaultLang) - - Extension output path: \(extensionOutputPath) + - Extension output path: \(extensionOutputPath ?? "-") - Extension name: \(extensionName ?? "-") - Extension suffix: \(extensionSuffix ?? "-") """ diff --git a/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift b/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift index 5e094e4..707fa08 100644 --- a/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift +++ b/Sources/ResgenSwift/Generate/Runnable/StringsConfiguration+Runnable.swift @@ -24,26 +24,24 @@ extension StringsConfiguration: Runnable { langs, "--default-lang", defaultLang, - "--extension-output-path", - extensionOutputPath.prependIfRelativePath(projectDirectory), "--static-members", "\(staticMembersOptions)", "--xc-strings", "\(xcStringsOptions)" ] - if let extensionName { - args += [ - "--extension-name", - extensionName - ] - } - - if let extensionSuffix { - args += [ - "--extension-suffix", - extensionSuffix - ] + // Add optional parameters + [ + ("--extension-output-path", extensionOutputPath?.prependIfRelativePath(projectDirectory)), + ("--extension-name", extensionName), + ("--extension-suffix", extensionSuffix) + ].forEach { argumentName, argumentValue in + if let argumentValue { + args += [ + argumentName, + argumentValue + ] + } } Stringium.main(args) diff --git a/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift b/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift index 4de70c2..67633f0 100644 --- a/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift +++ b/Sources/ResgenSwift/Strings/Generator/StringsFileGenerator.swift @@ -19,8 +19,7 @@ enum StringsFileGenerator { langs: [String], defaultLang: String, tags: [String], - outputPath: String, - inputFilenameWithoutExt: String + lprojPathFormat: String ) { var stringsFilesContent = [String: String]() @@ -37,7 +36,7 @@ enum StringsFileGenerator { langs.forEach { lang in 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) do { try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) @@ -54,8 +53,7 @@ enum StringsFileGenerator { langs: [String], defaultLang: String, tags: [String], - outputPath: String, - inputFilenameWithoutExt: String + xcStringsFilePath: String ) { let fileContent: String = Self.generateXcStringsFileContent( @@ -65,12 +63,11 @@ enum StringsFileGenerator { sections: sections ) - let stringsFilePath = "\(outputPath)/\(inputFilenameWithoutExt).xcstrings" - let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) + let stringsFilePathURL = URL(fileURLWithPath: xcStringsFilePath) do { try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) } catch { - let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) + let error = StringiumError.writeFile(error.localizedDescription, xcStringsFilePath) print(error.description) Stringium.exit(withError: error) } diff --git a/Sources/ResgenSwift/Strings/Stringium/Stringium.swift b/Sources/ResgenSwift/Strings/Stringium/Stringium.swift index 934060d..dd8289c 100644 --- a/Sources/ResgenSwift/Strings/Stringium/Stringium.swift +++ b/Sources/ResgenSwift/Strings/Stringium/Stringium.swift @@ -21,7 +21,6 @@ struct Stringium: ParsableCommand { // MARK: - Static static let toolName = "Stringium" - static let defaultExtensionName = "String" static let noTranslationTag: String = "notranslation" // MARK: - Command options @@ -49,8 +48,7 @@ struct Stringium: ParsableCommand { langs: options.langs, defaultLang: options.defaultLang, tags: options.tags, - outputPath: options.stringsFileOutputPath, - inputFilenameWithoutExt: options.inputFilenameWithoutExt + lprojPathFormat: options.lprojPathFormat ) } else { StringsFileGenerator.writeXcStringsFiles( @@ -58,22 +56,25 @@ struct Stringium: ParsableCommand { langs: options.langs, defaultLang: options.defaultLang, tags: options.tags, - outputPath: options.stringsFileOutputPath, - inputFilenameWithoutExt: options.inputFilenameWithoutExt + xcStringsFilePath: options.xcStringsFilePath ) } // Generate extension - StringsFileGenerator.writeExtensionFiles( - sections: sections, - defaultLang: options.defaultLang, - tags: options.tags, - staticVar: options.staticMembers, - inputFilename: options.inputFilenameWithoutExt, - extensionName: options.extensionName, - extensionFilePath: options.extensionFilePath, - extensionSuffix: options.extensionSuffix - ) + if let extensionName = options.extensionName, + let extensionFilePath = options.extensionFilePath { + print("Will generate extensions") + StringsFileGenerator.writeExtensionFiles( + sections: sections, + defaultLang: options.defaultLang, + tags: options.tags, + staticVar: options.staticMembers, + inputFilename: options.inputFilenameWithoutExt, + extensionName: extensionName, + extensionFilePath: extensionFilePath, + extensionSuffix: options.extensionSuffix ?? "" + ) + } print("[\(Self.toolName)] Strings generated") } @@ -104,10 +105,18 @@ struct Stringium: ParsableCommand { } // 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( force: options.forceGeneration, inputFilePath: options.inputFile, - extensionFilePath: options.extensionFilePath + extensionFilePath: fileToCompareToInput ) else { print("[\(Self.toolName)] Strings are already up to date :) ") return false diff --git a/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift b/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift index 8c1a05e..63f34ee 100644 --- a/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift +++ b/Sources/ResgenSwift/Strings/Stringium/StringiumOptions.swift @@ -15,35 +15,51 @@ struct StringiumOptions: ParsableArguments { @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") 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 - @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 - @Option(name: .customLong("langs"), help: "Langs to generate.") + @Option( + name: .customLong("langs"), + help: "Langs to generate." + ) fileprivate var langsRaw: String @Option(help: "Default langs.") 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" - @Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) - var extensionOutputPath: String - - @Option(help: "Tell if it will generate static properties or not") + @Option(help: "Generate static properties. False by default") var staticMembers: Bool = false - @Option(help: "Tell if it will generate xcStrings file or not") - var xcStrings: Bool = false + @Option(help: "Tell if it will generate xcStrings file or lproj file. True by default") + var xcStrings: Bool = true - @Option(help: "Extension name. If not specified, it will generate an String extension.") - var extensionName: String = Stringium.defaultExtensionName + @Option( + 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") - var extensionSuffix: String + var extensionSuffix: String? } // MARK: - Private var getter @@ -75,12 +91,21 @@ extension StringiumOptions { extension StringiumOptions { - var extensionFileName: String { - "\(extensionName)+\(extensionSuffix).swift" + private var extensionFileName: String? { + if let extensionName { + if let extensionSuffix { + return "\(extensionName)+\(extensionSuffix).swift" + } + return "\(extensionName).swift" + } + return nil } - var extensionFilePath: String { - "\(extensionOutputPath)/\(extensionFileName)" + var extensionFilePath: String? { + if let extensionOutputPath, let extensionFileName { + return "\(extensionOutputPath)/\(extensionFileName)" + } + return nil } var inputFilenameWithoutExt: String { @@ -88,4 +113,12 @@ extension StringiumOptions { .deletingPathExtension() .lastPathComponent } + + var xcStringsFilePath: String { + "\(stringsFileOutputPath)/\(inputFilenameWithoutExt).xcstrings" + } + + var lprojPathFormat: String { + "\(stringsFileOutputPath)/%@.lproj/\(inputFilenameWithoutExt).strings" + } }