Première implémentation des xcstrings
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
//
|
||||
// StringsFileGenerator.swift
|
||||
//
|
||||
//
|
||||
//
|
||||
// Created by Thibaut Schmitt on 04/01/2022.
|
||||
//
|
||||
@ -9,28 +9,49 @@ import Foundation
|
||||
import ToolCore
|
||||
|
||||
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"
|
||||
inputFilenameWithoutExt: String,
|
||||
isXcString: Bool = false) {
|
||||
|
||||
if !isXcString {
|
||||
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: false, encoding: .utf8)
|
||||
} catch let error {
|
||||
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
|
||||
print(error.description)
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let fileContent: String = Self.generateXcStringsFileContent(
|
||||
langs: langs,
|
||||
defaultLang: defaultLang,
|
||||
tags: tags,
|
||||
sections: sections
|
||||
)
|
||||
|
||||
let stringsFilePath = "\(outputPath)/Localizable.xcstrings"
|
||||
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
|
||||
do {
|
||||
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
|
||||
@ -41,7 +62,7 @@ class StringsFileGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func generateStringsFileContent(lang: String,
|
||||
defaultLang: String,
|
||||
tags inputTags: [String],
|
||||
@ -53,13 +74,13 @@ class StringsFileGenerator {
|
||||
* 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
|
||||
var skipDefinition = false // Set to true if not matching tag
|
||||
@ -69,16 +90,16 @@ class StringsFileGenerator {
|
||||
skipDefinition = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// If tags contains `noTranslationTag` => get default lang
|
||||
if definition.tags.contains(Stringium.noTranslationTag) {
|
||||
return definition.translations[defaultLang]
|
||||
}
|
||||
|
||||
|
||||
// Else: get specific lang
|
||||
return definition.translations[lang]
|
||||
}()
|
||||
|
||||
|
||||
if let translation = translationOpt {
|
||||
stringsFileContent += "\"\(definition.name)\" = \"\(translation)\";\n\n"
|
||||
} else if skipDefinition == false {
|
||||
@ -88,12 +109,101 @@ class StringsFileGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return stringsFileContent
|
||||
}
|
||||
|
||||
|
||||
// MARK: - XcStrings Generation
|
||||
|
||||
static func generateXcStringsFileContent(langs: [String],
|
||||
defaultLang: String,
|
||||
tags inputTags: [String],
|
||||
sections: [Section]) -> String {
|
||||
let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections)
|
||||
let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
static func generateXcStringsFileContentFromRootObject(rootObject: Root) -> String {
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted]
|
||||
|
||||
let json = try encoder.encode(rootObject)
|
||||
|
||||
if let jsonString = String(data: json, encoding: .utf8) {
|
||||
return jsonString
|
||||
}
|
||||
|
||||
} catch {
|
||||
debugPrint("Failed to encode: \(error)")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
static func generateRootObject(langs: [String],
|
||||
defaultLang: String,
|
||||
tags inputTags: [String],
|
||||
sections: [Section]) -> Root {
|
||||
|
||||
var xcStringDefinitionTab: [XCStringDefinition] = []
|
||||
|
||||
sections.forEach { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
section.definitions.forEach { definition in
|
||||
var skipDefinition = false
|
||||
|
||||
var localizationTab: [XCStringLocalization] = []
|
||||
|
||||
if definition.hasOneOrMoreMatchingTags(inputTags: inputTags) == false {
|
||||
skipDefinition = true
|
||||
}
|
||||
|
||||
if !skipDefinition {
|
||||
|
||||
for (lang, value) in definition.translations {
|
||||
let localization = XCStringLocalization(
|
||||
lang: lang,
|
||||
content: XCStringLocalizationLangContent(
|
||||
stringUnit: DefaultStringUnit(state: "translated", value: value)
|
||||
)
|
||||
)
|
||||
|
||||
localizationTab.append(localization)
|
||||
}
|
||||
|
||||
let xcStringDefinition = XCStringDefinition(
|
||||
title: definition.name,
|
||||
content: XCStringDefinitionContent(
|
||||
extractionState: "manual",
|
||||
localizations: XCStringLocalizationContainer(
|
||||
localizations: localizationTab
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
xcStringDefinitionTab.append(xcStringDefinition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab)
|
||||
|
||||
return Root(
|
||||
sourceLanguage: defaultLang,
|
||||
strings: xcStringContainer,
|
||||
version: "1.0"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Extension file
|
||||
|
||||
|
||||
static func writeExtensionFiles(sections: [Section],
|
||||
defaultLang lang: String,
|
||||
tags: [String],
|
||||
@ -110,7 +220,7 @@ class StringsFileGenerator {
|
||||
inputFilename: inputFilename,
|
||||
extensionName: extensionName,
|
||||
extensionSuffix: extensionSuffix)
|
||||
|
||||
|
||||
// Write content
|
||||
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
|
||||
do {
|
||||
@ -121,9 +231,9 @@ class StringsFileGenerator {
|
||||
Stringium.exit(withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extension content
|
||||
|
||||
|
||||
static func getExtensionContent(sections: [Section],
|
||||
defaultLang lang: String,
|
||||
tags: [String],
|
||||
@ -139,31 +249,31 @@ class StringsFileGenerator {
|
||||
]
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extension part
|
||||
|
||||
|
||||
private static func getHeader(stringsFilename: String, extensionClassname: String) -> String {
|
||||
"""
|
||||
// Generated by ResgenSwift.Strings.\(Stringium.toolName) \(ResgenSwiftVersion)
|
||||
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
fileprivate let kStringsFileName = "\(stringsFilename)"
|
||||
|
||||
|
||||
extension \(extensionClassname) {
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
private static func getEnumKey(sections: [Section], tags: [String], extensionClassname: String, extensionSuffix: String) -> String {
|
||||
var enumDefinition = "\n enum Key\(extensionSuffix.uppercasedFirst()): String {\n"
|
||||
|
||||
|
||||
// Enum
|
||||
sections.forEach { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
|
||||
section.definitions.forEach { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return // Go to next definition
|
||||
@ -172,7 +282,7 @@ class StringsFileGenerator {
|
||||
enumDefinition += " case \(definition.name) = \"\(definition.name)\"\n"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// KeyPath accessors
|
||||
enumDefinition += "\n"
|
||||
enumDefinition += " var keyPath: KeyPath<\(extensionClassname), String> {\n"
|
||||
@ -182,7 +292,7 @@ class StringsFileGenerator {
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return // Go to next section
|
||||
}
|
||||
|
||||
|
||||
section.definitions.forEach { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return // Go to next definition
|
||||
@ -194,23 +304,23 @@ class StringsFileGenerator {
|
||||
enumDefinition += " }\n" // Switch
|
||||
enumDefinition += " }\n" // var keyPath
|
||||
enumDefinition += " }" // Enum
|
||||
|
||||
|
||||
return enumDefinition
|
||||
}
|
||||
|
||||
|
||||
private static func getProperties(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool) -> String {
|
||||
sections.compactMap { section in
|
||||
// Check that at least one string will be generated
|
||||
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
|
||||
return nil // Go to next section
|
||||
}
|
||||
|
||||
|
||||
var res = "\n // MARK: - \(section.name)\n"
|
||||
res += section.definitions.compactMap { definition in
|
||||
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
|
||||
return nil // Go to next definition
|
||||
}
|
||||
|
||||
|
||||
if staticVar {
|
||||
return "\n\(definition.getNSLocalizedStringStaticProperty(forLang: lang))"
|
||||
}
|
||||
@ -221,11 +331,11 @@ class StringsFileGenerator {
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
|
||||
private static func getFooter() -> String {
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
93
Sources/ResgenSwift/Strings/Model/XcString.swift
Normal file
93
Sources/ResgenSwift/Strings/Model/XcString.swift
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// XcString.swift
|
||||
//
|
||||
//
|
||||
// Created by Quentin Bandera on 12/04/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicKey: CodingKey {
|
||||
var intValue: Int?
|
||||
init?(intValue: Int) {
|
||||
self.intValue = intValue
|
||||
self.stringValue = "\(intValue)"
|
||||
}
|
||||
|
||||
var stringValue: String
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
}
|
||||
|
||||
struct Root: Codable, Equatable {
|
||||
let sourceLanguage: String
|
||||
let strings: XCStringDefinitionContainer
|
||||
let version: String
|
||||
}
|
||||
|
||||
struct XCStringDefinitionContainer: Codable, Equatable {
|
||||
let strings: [XCStringDefinition]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: DynamicKey.self)
|
||||
|
||||
for str in strings {
|
||||
if let codingKey = DynamicKey(stringValue: str.title) {
|
||||
try container.encode(str.content, forKey: codingKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct XCStringDefinition: Codable, Equatable {
|
||||
let title: String // json key -> custom encoding methods
|
||||
let content: XCStringDefinitionContent
|
||||
}
|
||||
|
||||
struct XCStringDefinitionContent: Codable, Equatable {
|
||||
let extractionState: String
|
||||
var localizations: XCStringLocalizationContainer
|
||||
|
||||
}
|
||||
|
||||
struct XCStringLocalizationContainer: Codable, Equatable {
|
||||
let localizations: [XCStringLocalization]
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: DynamicKey.self)
|
||||
|
||||
for loca in localizations {
|
||||
if let codingKey = DynamicKey(stringValue: loca.lang) {
|
||||
try container.encode(loca.content, forKey: codingKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct XCStringLocalization: Codable, Equatable {
|
||||
let lang: String // json key -> custom encoding method
|
||||
let content: XCStringLocalizationLangContent
|
||||
}
|
||||
|
||||
struct XCStringLocalizationLangContent: Codable, Equatable {
|
||||
let stringUnit: DefaultStringUnit
|
||||
}
|
||||
|
||||
//enum VarationOrStringUnit: Encodable {
|
||||
// case variations([Varation])
|
||||
// case stringUnit: (DefaultStringUnit)
|
||||
//
|
||||
// func encode(to encoder: any Encoder) throws {
|
||||
// if let varations {
|
||||
//
|
||||
// } else if let {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
struct DefaultStringUnit: Codable, Equatable {
|
||||
let state: String
|
||||
let value: String
|
||||
}
|
@ -48,8 +48,9 @@ struct Stringium: ParsableCommand {
|
||||
defaultLang: options.defaultLang,
|
||||
tags: options.tags,
|
||||
outputPath: options.stringsFileOutputPath,
|
||||
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
|
||||
|
||||
inputFilenameWithoutExt: options.inputFilenameWithoutExt,
|
||||
isXcString: options.isXcstring)
|
||||
|
||||
// Generate extension
|
||||
StringsFileGenerator.writeExtensionFiles(sections: sections,
|
||||
defaultLang: options.defaultLang,
|
||||
|
@ -11,7 +11,10 @@ import ArgumentParser
|
||||
struct StringiumOptions: ParsableArguments {
|
||||
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
|
||||
var forceGeneration = false
|
||||
|
||||
|
||||
@Flag(name: [.customShort("x"), .customLong("xcstrings")], help: "Generate xcstrings catalog")
|
||||
var isXcstring = false
|
||||
|
||||
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
|
||||
var inputFile: String
|
||||
|
||||
|
Reference in New Issue
Block a user