Last active
December 9, 2024 23:15
-
-
Save mmysliwiec/cee71e726cd265b01daac8fd4c9966de to your computer and use it in GitHub Desktop.
Swift script for generating .strings file based on settings bundle .plist file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env swift | |
// This is a script for extracting all localizable strings from the Root.plist (Settings.bundle) | |
// | |
import Foundation | |
typealias Plist = [String: Any] | |
var currentGroup: String? | |
// Parameters | |
let params = CommandLine.arguments | |
guard params.count == 3 else { | |
print("ERROR: Add .plist file name for settings bundle and .strings file name for output strings.") | |
print("Usage: genstringsforsettingsbundle.swift Root.plist Root.strings.") | |
exit(1) | |
} | |
// Path to the given Root.plist file | |
let rootPath = params[1] | |
let stringsPath = params[2] | |
// Load content of Root.plist file | |
guard let plist = getPlist(withPath: rootPath) else { | |
print("ERROR: File '\(rootPath)' is not a valid setting bundle plist.") | |
exit(2) | |
} | |
let localizables = extractLocalizables(from: plist) | |
// Generate text for all our localizables | |
var finalText = "" | |
for loc in localizables { | |
// Comment first | |
finalText.append("/* \(loc.comment) */\n") | |
// Key = Content | |
finalText.append("\"\(loc.key)\" = \"\(loc.content)\";\n\n") | |
} | |
// Save the content to .strings file | |
let stringsUrl = URL(fileURLWithPath: stringsPath) | |
do { | |
try finalText.write(to: stringsUrl, atomically: true, encoding: .utf8) | |
} catch { | |
print("Error writing: \(error.localizedDescription)") | |
exit(3) | |
} | |
print("\(localizables.count) localizable strings saved to \(stringsUrl.path)") | |
// Helpers | |
struct StringEntry { | |
let key: String | |
let content: String | |
let comment: String | |
} | |
// Load and deserialize Root.plist file | |
func getPlist(withPath path: String) -> Plist? { | |
if let xml = FileManager.default.contents(atPath: path) { | |
let plistData = try? PropertyListSerialization.propertyList(from: xml, options: [], format: nil) | |
return plistData as? Plist | |
} | |
return nil | |
} | |
// Extract localizable content | |
func extractLocalizables(from plistNode: Plist) -> [StringEntry] { | |
var allLocalizables = [StringEntry]() | |
// Is it a root level? | |
if let root = plistNode["StringsTable"] as? String, root == "Root", let title = plistNode.title { | |
allLocalizables.append(StringEntry(key: "StringsTableRoot", content: title, comment: "Title for the Root.plist root level. Usually the app name.")) | |
} | |
// Process array of PreferenceSpecifiers | |
guard let prefs = plistNode["PreferenceSpecifiers"] as? [Plist] else { return allLocalizables } | |
for pref in prefs { | |
// All entries must have a type | |
guard let type = pref.type else { continue } | |
if type == "PSGroupSpecifier", let title = pref.title { | |
// Group entry will be treated in a special way | |
currentGroup = title | |
let key = "'\(title)' group" | |
let comment = "Group title for \(key)" | |
allLocalizables.append(StringEntry(key: key, content: title, comment: comment)) | |
// There can be FooterText for the group | |
if let footer = pref.footerText { | |
let footerKey = "'\(title)' group footer" | |
let comment = "Group footer for \(key)" | |
allLocalizables.append(StringEntry(key: footerKey, content: footer, comment: comment)) | |
} | |
} else { | |
// Other entries than group always have at least key and title | |
guard let key = pref.key, let title = pref.title else { continue } | |
// swiftlint:disable:next force_unwrapping | |
let comment = (currentGroup == nil) ? "Top level main settings group - '\(key)'" : "'\(currentGroup!)' group - '\(key)', title for type \(type)" | |
allLocalizables.append(StringEntry(key: key, content: title, comment: comment)) | |
// If there are other Titles | |
if let titles = pref.titles { | |
for (index, subtitle) in titles.enumerated() { | |
let subkey = "\(key) sub-element no \(index + 1)" | |
let subcomment = comment + " sub-element '\(subtitle)'" | |
allLocalizables.append(StringEntry(key: subkey, content: subtitle, comment: subcomment)) | |
} | |
} | |
} | |
} | |
return allLocalizables | |
} | |
extension Plist { | |
var key: String? { | |
return self["Key"] as? String | |
} | |
var type: String? { | |
return self["Type"] as? String | |
} | |
var title: String? { | |
return self["Title"] as? String | |
} | |
var titles: [String]? { | |
return self["Titles"] as? [String] | |
} | |
var footerText: String? { | |
return self["FooterText"] as? String | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The new version handles keys properly and generates proper comments based on structure (including groups).