|
#!/usr/bin/swift |
|
|
|
import Foundation |
|
|
|
extension FileHandle { |
|
|
|
public func read (encoding: String.Encoding = String.Encoding.utf8) -> String { |
|
let data: Data = self.readDataToEndOfFile() |
|
|
|
guard let result = String(data: data, encoding: encoding) else { |
|
fatalError("Could not convert binary data to text.") |
|
} |
|
|
|
return result |
|
} |
|
|
|
} |
|
|
|
|
|
/** |
|
* Filesystem node (either a file or directory) |
|
* Is used as a base class for the file and directory classes |
|
*/ |
|
class DiskNode { |
|
|
|
let manager: FileManager = FileManager.default |
|
|
|
let path: String |
|
|
|
init?(path: String) { |
|
self.path = path |
|
|
|
if type(of: self) == DiskNode.Type.self { |
|
return nil |
|
} |
|
if !exists() { |
|
return nil |
|
} |
|
} |
|
|
|
func exists() -> Bool { |
|
var isDir : ObjCBool = false |
|
|
|
let result: Bool |
|
|
|
if manager.fileExists(atPath: path, isDirectory: &isDir) { |
|
if type(of: self) == Directory.Type.self { |
|
if isDir.boolValue { |
|
result = true |
|
} |
|
else { |
|
result = false |
|
} |
|
} |
|
else { |
|
result = true |
|
} |
|
} |
|
else { |
|
result = false |
|
} |
|
|
|
return result |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Local filesystem file |
|
*/ |
|
class File: DiskNode { |
|
|
|
let handle: FileHandle |
|
|
|
let name: String |
|
|
|
let ext: String |
|
|
|
override init?(path: String) { |
|
guard let handle = FileHandle(forReadingAtPath: path) else { |
|
return nil |
|
} |
|
|
|
let uri = URL(fileURLWithPath: path) |
|
|
|
self.handle = handle |
|
self.ext = URL(fileURLWithPath: path).pathExtension |
|
self.name = uri.pathComponents.last ?? "" |
|
|
|
super.init(path: path) |
|
} |
|
|
|
func contents() -> String { |
|
return handle.read() |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Local filesystem directory |
|
*/ |
|
class Directory: DiskNode { |
|
|
|
func findFilesRecursively(byExtension: String) -> [File]? { |
|
guard let enumerator = manager.enumerator(atPath: path) else { |
|
return nil |
|
} |
|
|
|
var result: [File] = [] |
|
|
|
enumerator.forEach({ (path) in |
|
guard let path = path as? String else { |
|
return |
|
} |
|
|
|
let ext = URL(fileURLWithPath: path).pathExtension |
|
|
|
guard ext == byExtension else { |
|
return |
|
} |
|
guard let file = File(path: self.path + "/" + path) else { |
|
return |
|
} |
|
|
|
result.append(file) |
|
}) |
|
|
|
return result |
|
} |
|
|
|
func findFilesRecursively(byName: String) -> [File]? { |
|
guard let enumerator = manager.enumerator(atPath: path) else { |
|
return nil |
|
} |
|
|
|
var result: [File] = [] |
|
|
|
enumerator.forEach({ (path) in |
|
guard let path = path as? String else { |
|
return |
|
} |
|
|
|
let filename = URL(fileURLWithPath: path).lastPathComponent |
|
|
|
guard filename == byName else { |
|
return |
|
} |
|
guard let file = File(path: self.path + "/" + path) else { |
|
return |
|
} |
|
|
|
result.append(file) |
|
}) |
|
|
|
return result |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Recursively walk over all leafs of the |
|
* provided JSON object. |
|
*/ |
|
class Recursive { |
|
|
|
typealias JsonObject = Dictionary<String, Any> |
|
|
|
typealias JsonArray = Array<AnyObject> |
|
|
|
typealias LeafCallback = (_ key: Any, _ value: Any, _ path: Recursive.Path) -> Swift.Void |
|
|
|
/** |
|
* Keeps track of JsonObjects found along the way. |
|
*/ |
|
class Path { |
|
|
|
class Step { |
|
|
|
var key: Any |
|
|
|
var value: JsonObject |
|
|
|
init(key: Any, value: JsonObject) { |
|
self.key = key |
|
self.value = value |
|
} |
|
|
|
} |
|
|
|
var steps: [Step] = [] |
|
|
|
func append(key: Any, value: JsonObject) { |
|
steps.append(Step(key: key, value: value)) |
|
} |
|
|
|
func last(count: Int = 0) -> JsonObject? { |
|
if count == 0 { |
|
if let last: Step = steps.last { |
|
return last.value |
|
} |
|
} |
|
|
|
let index = (steps.count - 1) - count |
|
|
|
if index >= 0 { |
|
return steps[index].value |
|
} |
|
|
|
return nil |
|
} |
|
|
|
init() {} |
|
|
|
} |
|
|
|
var path = Path() |
|
|
|
let callback: LeafCallback |
|
|
|
func select(key: Any, value: Any) { |
|
if let object = value as? JsonObject { |
|
path.append(key: key, value: object) |
|
enumerate(object: object) |
|
} |
|
else if let array = value as? JsonArray { |
|
enumerate(array: array) |
|
} |
|
else if let string = value as? String { |
|
callback(key, string, path) |
|
} |
|
else if let number = value as? NSNumber { |
|
callback(key, number, path) |
|
} |
|
} |
|
|
|
func enumerate(array:JsonArray) { |
|
for (key, value) in array.enumerated() { |
|
select(key: key, value: value) |
|
} |
|
} |
|
|
|
func enumerate(object:JsonObject) { |
|
for (key, value) in object { |
|
select(key: key, value: value) |
|
} |
|
} |
|
|
|
init(json: Any, leaf: @escaping LeafCallback) { |
|
callback = leaf |
|
select(key: "", value: json) |
|
} |
|
|
|
} |
|
|
|
|
|
extension String { |
|
|
|
func index(from: Int) -> Index { |
|
return self.index(startIndex, offsetBy: from) |
|
} |
|
|
|
func substring(from: Int) -> String { |
|
let fromIndex = index(from: from) |
|
return substring(from: fromIndex) |
|
} |
|
|
|
func substring(to: Int) -> String { |
|
let toIndex = index(from: to) |
|
return substring(to: toIndex) |
|
} |
|
|
|
func substring(with r: Range<Int>) -> String { |
|
let startIndex = index(from: r.lowerBound) |
|
let endIndex = index(from: r.upperBound) |
|
return substring(with: startIndex..<endIndex) |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func shell(launchPath: String, arguments: [String]) -> String { |
|
let task = Process() // will throw unresolved identifier warning for iOS projects: IGNORE |
|
task.launchPath = launchPath |
|
task.arguments = arguments |
|
|
|
let pipe = Pipe() |
|
task.standardOutput = pipe |
|
task.launch() |
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile() |
|
let output = String(data: data, encoding: String.Encoding.utf8)! |
|
|
|
if output.characters.count > 0 { |
|
//remove newline character. |
|
let lastIndex = output.index(before: output.endIndex) |
|
return output[output.startIndex ..< lastIndex] |
|
} |
|
|
|
return output |
|
} |
|
|
|
|
|
let manager = FileManager.default |
|
let args = CommandLine.arguments |
|
let path = manager.currentDirectoryPath |
|
let dir = Directory(path: path) |
|
|
|
//guard let files = dir?.findFilesRecursively(byName: "example.swift") else { |
|
guard let files = dir?.findFilesRecursively(byExtension: "swift") else { |
|
abort() |
|
} |
|
|
|
|
|
let stringsFilePath = path + "/base.strings" |
|
|
|
// create new strings file |
|
_ = shell( |
|
launchPath: "/usr/bin/env", |
|
arguments: [ |
|
"touch", |
|
"\(stringsFilePath)" |
|
] |
|
) |
|
|
|
guard let stringsFileHandle = FileHandle(forWritingAtPath: stringsFilePath) else { |
|
print("no file to write to \(stringsFilePath) was not created or available") |
|
abort() |
|
} |
|
|
|
for file in files { |
|
// get AST output from sourcekitten library |
|
let output = shell( |
|
launchPath: "/usr/bin/env", |
|
arguments: [ |
|
"sourcekitten", |
|
"structure", |
|
"--file", |
|
"\(file.path)" |
|
] |
|
) |
|
|
|
let data = output.data(using: .utf8) |
|
|
|
var contents: String? = nil |
|
|
|
do { |
|
let json = try JSONSerialization.jsonObject( |
|
with: data!, |
|
options: JSONSerialization.ReadingOptions.allowFragments |
|
) |
|
|
|
struct Translation { |
|
let string: String |
|
let comment: String |
|
} |
|
|
|
var translations: [Translation] = [] |
|
|
|
_ = Recursive(json: json) { |
|
key, value, path in |
|
|
|
guard let key = key as? String else { |
|
return |
|
} |
|
guard let value = value as? String else { |
|
return |
|
} |
|
guard value.contains("\".localizedWith") else { |
|
return |
|
} |
|
|
|
// this part gets the actual string that needs translating |
|
var parts = value.components(separatedBy: ".") |
|
_ = parts.popLast() |
|
var string = parts.joined(separator: ".") |
|
string = string.trimmingCharacters(in: CharacterSet(["\""])) |
|
|
|
// now extract the comment if possible |
|
var comment: String = "" |
|
if let argument = path.last() , argument["key.name"] != nil { |
|
if let argumentName = argument["key.name"] as? String , argumentName == "comment" { |
|
|
|
if let argumentBodyOffset = argument["key.bodyoffset"] as? NSNumber , |
|
let argumentBodyLength = argument["key.bodylength"] as? NSNumber { |
|
|
|
if contents == nil { |
|
contents = file.contents() |
|
} |
|
|
|
if let contents = contents { |
|
comment = contents.substring( |
|
with: (argumentBodyOffset.intValue) |
|
..< (argumentBodyOffset.intValue + argumentBodyLength.intValue) |
|
) |
|
|
|
comment = comment.trimmingCharacters(in: CharacterSet(["\""])) |
|
} |
|
} |
|
} |
|
|
|
translations.append(Translation(string: string, comment: comment)) |
|
print("\(file.name) found string \(string) with comment \(comment)") |
|
} |
|
} |
|
|
|
|
|
let newline = "\r\n" |
|
|
|
for translation in translations { |
|
let rule = "\(newline)/* File = \"\(file.name)\"; comment = \"\(translation.comment)\" */\(newline)\"\(translation.string)\" = \"\(translation.string)\";\(newline)" |
|
|
|
stringsFileHandle.write(rule.data(using: .utf8)!) |
|
} |
|
|
|
|
|
} |
|
catch let error as NSError { |
|
print(error.localizedDescription) |
|
} |
|
} |
|
|
|
print("Outputted base.strings file in \(stringsFilePath)") |
|
stringsFileHandle.closeFile() |