Skip to content

Instantly share code, notes, and snippets.

@mmysliwiec
Last active February 26, 2024 11:55
Show Gist options
  • Save mmysliwiec/cee71e726cd265b01daac8fd4c9966de to your computer and use it in GitHub Desktop.
Save mmysliwiec/cee71e726cd265b01daac8fd4c9966de to your computer and use it in GitHub Desktop.
Swift script for generating .strings file based on settings bundle .plist file.
#!/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
}
}
@mmysliwiec
Copy link
Author

The new version handles keys properly and generates proper comments based on structure (including groups).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment