Swiftgram/tools/GenerateLocalization.swift
2018-11-14 23:03:33 +04:00

525 lines
17 KiB
Swift

import Foundation
struct Entry {
let key: String
let value: String
}
enum ArgumentType {
case any
case integer(decimalNumbers: Int)
case float
init(_ control: String, decimalNumbers: Int) {
switch control {
case "d":
self = .integer(decimalNumbers: decimalNumbers)
case "f":
self = .float
case "@":
self = .any
default:
preconditionFailure()
}
}
}
struct Argument {
let index: Int
let type: ArgumentType
}
func escapedIdentifier(_ value: String) -> String {
return value.replacingOccurrences(of: ".", with: "_").replacingOccurrences(of: "#", with: "_").replacingOccurrences(of: " ", with: "_").replacingOccurrences(of: "'", with: "_")
}
func functionArguments(_ arguments: [Argument]) -> String {
var result = ""
var existingIndices = Set<Int>()
for argument in arguments.sorted(by: { $0.index < $1.index }) {
if existingIndices.contains(argument.index) {
continue
}
existingIndices.insert(argument.index)
if !result.isEmpty {
result += ", "
}
result += "_ _\(argument.index): "
switch argument.type {
case .any:
result += "String"
case .float:
result += "Float"
case .integer:
result += "Int"
}
}
return result
}
func formatArguments(_ arguments: [Argument]) -> String {
var result = ""
for argument in arguments.sorted(by: { $0.index < $1.index }) {
if !result.isEmpty {
result += ", "
}
switch argument.type {
case .any:
result += "_\(argument.index)"
case .float:
result += "\"\\(_\(argument.index))\""
case let .integer(decimalNumbers):
if decimalNumbers == 0 {
result += "\"\\(_\(argument.index))\""
} else {
result += "String(format: \"%.\(decimalNumbers)d\", _\(argument.index))"
}
}
}
return result
}
let argumentRegex = try! NSRegularExpression(pattern: "%((\\.(\\d+))?)(((\\d+)\\$)?)([@df])", options: [])
func parseArguments(_ value: String) -> [Argument] {
let string = value as NSString
let matches = argumentRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length))
var arguments: [Argument] = []
var index = 0
if value.range(of: ".2d") != nil {
print(value)
}
for match in matches {
var currentIndex = index
var decimalNumbers = 0
if match.range(at: 3).location != NSNotFound {
decimalNumbers = Int(string.substring(with: match.range(at: 3)))!
}
if match.range(at: 6).location != NSNotFound {
currentIndex = Int(string.substring(with: match.range(at: 6)))!
}
arguments.append(Argument(index: currentIndex, type: ArgumentType(string.substring(with: match.range(at: 7)), decimalNumbers: decimalNumbers)))
index += 1
}
return arguments
}
func addCode(_ lines: [String]) -> String {
var result: String = ""
for line in lines {
result += line
result += "\n"
}
return result
}
enum PluralizationForm: Int32 {
case zero = 0
case one = 1
case two = 2
case few = 3
case many = 4
case other = 5
static var formCount = Int(PluralizationForm.other.rawValue + 1)
static var all: [PluralizationForm] = [.zero, .one, .two, .few, .many, .other]
var name: String {
switch self {
case .zero:
return "zero"
case .one:
return "one"
case .two:
return "two"
case .few:
return "few"
case .many:
return "many"
case .other:
return "other"
}
}
}
let pluralizationFormRegex = try! NSRegularExpression(pattern: "(.*?)_(0|zero|1|one|2|two|3_10|few|many|any|other)$", options: [])
func pluralizationForm(_ key: String) -> (String, PluralizationForm)? {
let string = key as NSString
let matches = pluralizationFormRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length))
for match in matches {
if match.range(at: 1).location != NSNotFound && match.range(at: 2).location != NSNotFound {
let base = string.substring(with: match.range(at: 1))
let value = string.substring(with: match.range(at: 2))
let form: PluralizationForm
switch value {
case "0", "zero":
form = .zero
case "1", "one":
form = .one
case "2", "two":
form = .two
case "3_10", "few":
form = .few
case "many":
form = .many
case "any", "other":
form = .other
default:
return nil
}
return (base, form)
}
}
return nil
}
final class WriteBuffer {
var data = Data()
init() {
}
func append(_ value: Int32) {
let ptr = self.data.count
self.data.count += 4
self.data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Void in
var value = value
memcpy(bytes.advanced(by: ptr), &value, 4)
}
}
func append(_ string: String) {
let bytes = string.data(using: .utf8)!
self.append(Int32(bytes.count))
self.data.append(bytes)
}
}
if CommandLine.arguments.count != 4 {
print("Usage: swift GenerateLocalization.swift Localizable.strings Strings.swift Strings.mapping")
} else {
if let rawDict = NSDictionary(contentsOfFile: CommandLine.arguments[1]) {
var result = "import Foundation\n\n"
result +=
"""
private let fallbackDict: [String: String] = {
guard let mainPath = Bundle.main.path(forResource: \"en\", ofType: \"lproj\"), let bundle = Bundle(path: mainPath) else {
return [:]
}
guard let path = bundle.path(forResource: \"Localizable\", ofType: \"strings\") else {
return [:]
}
guard let dict = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] else {
return [:]
}
return dict
}()
private extension PluralizationForm {
var canonicalSuffix: String {
switch self {
case .zero:
return \"_0\"
case .one:
return \"_1\"
case .two:
return \"_2\"
case .few:
return \"_3_10\"
case .many:
return \"_many\"
case .other:
return \"_any\"
}
}
}
public final class PresentationStringsComponent {
public let languageCode: String
public let localizedName: String
public let pluralizationRulesCode: String?
public let dict: [String: String]
public init(languageCode: String, localizedName: String, pluralizationRulesCode: String?, dict: [String: String]) {
self.languageCode = languageCode
self.localizedName = localizedName
self.pluralizationRulesCode = pluralizationRulesCode
self.dict = dict
}
}
private func getValue(_ primaryComponent: PresentationStringsComponent, _ secondaryComponent: PresentationStringsComponent?, _ key: String) -> String {
if let value = primaryComponent.dict[key] {
return value
} else if let secondaryComponent = secondaryComponent, let value = secondaryComponent.dict[key] {
return value
} else if let value = fallbackDict[key] {
return value
} else {
return key
}
}
private func getValueWithForm(_ primaryComponent: PresentationStringsComponent, _ secondaryComponent: PresentationStringsComponent?, _ key: String, _ form: PluralizationForm) -> String {
let builtKey = key + form.canonicalSuffix
if let value = primaryComponent.dict[builtKey] {
return value
} else if let secondaryComponent = secondaryComponent, let value = secondaryComponent.dict[builtKey] {
return value
} else if let value = fallbackDict[builtKey] {
return value
}
return key
}
private let argumentRegex = try! NSRegularExpression(pattern: \"%(((\\\\d+)\\\\$)?)([@df])\", options: [])
private func extractArgumentRanges(_ value: String) -> [(Int, NSRange)] {
var result: [(Int, NSRange)] = []
let string = value as NSString
let matches = argumentRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length))
var index = 0
for match in matches {
var currentIndex = index
if match.range(at: 3).location != NSNotFound {
currentIndex = Int(string.substring(with: match.range(at: 3)))! - 1
}
result.append((currentIndex, match.range(at: 0)))
index += 1
}
result.sort(by: { $0.1.location < $1.1.location })
return result
}
func formatWithArgumentRanges(_ value: String, _ ranges: [(Int, NSRange)], _ arguments: [String]) -> (String, [(Int, NSRange)]) {
let string = value as NSString
var resultingRanges: [(Int, NSRange)] = []
var currentLocation = 0
let result = NSMutableString()
for (index, range) in ranges {
if currentLocation < range.location {
result.append(string.substring(with: NSRange(location: currentLocation, length: range.location - currentLocation)))
}
resultingRanges.append((index, NSRange(location: result.length, length: (arguments[index] as NSString).length)))
result.append(arguments[index])
currentLocation = range.location + range.length
}
if currentLocation != string.length {
result.append(string.substring(with: NSRange(location: currentLocation, length: string.length - currentLocation)))
}
return (result as String, resultingRanges)
}
private final class DataReader {
private let data: Data
private var ptr: Int = 0
init(_ data: Data) {
self.data = data
}
func readInt32() -> Int32 {
assert(self.ptr + 4 <= self.data.count)
let result = self.data.withUnsafeBytes { (bytes: UnsafePointer<Int8>) -> Int32 in
var value: Int32 = 0
memcpy(&value, bytes.advanced(by: self.ptr), 4)
return value
}
self.ptr += 4
return result
}
func readString() -> String {
let length = Int(self.readInt32())
assert(self.ptr + length <= self.data.count)
let value = String(data: self.data.subdata(in: self.ptr ..< self.ptr + length), encoding: .utf8)!
self.ptr += length
return value
}
}
private func loadMapping() -> ([Int], [String], [Int], [Int], [String]) {
guard let filePath = frameworkBundle.path(forResource: "PresentationStrings", ofType: "mapping") else {
fatalError()
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
fatalError()
}
let reader = DataReader(data)
let idCount = Int(reader.readInt32())
var sIdList: [Int] = []
var sKeyList: [String] = []
var sArgIdList: [Int] = []
for _ in 0 ..< idCount {
let id = Int(reader.readInt32())
sIdList.append(id)
sKeyList.append(reader.readString())
if reader.readInt32() != 0 {
sArgIdList.append(id)
}
}
let pCount = Int(reader.readInt32())
var pIdList: [Int] = []
var pKeyList: [String] = []
for _ in 0 ..< Int(pCount) {
pIdList.append(Int(reader.readInt32()))
pKeyList.append(reader.readString())
}
return (sIdList, sKeyList, sArgIdList, pIdList, pKeyList)
}
private let keyMapping: ([Int], [String], [Int], [Int], [String]) = loadMapping()
public final class PresentationStrings {
public let lc: UInt32
public let primaryComponent: PresentationStringsComponent
public let secondaryComponent: PresentationStringsComponent?
public let baseLanguageCode: String
private let _s: [Int: String]
private let _r: [Int: [(Int, NSRange)]]
private let _ps: [Int: String]
"""
let rawKeyPairs = rawDict.map({ ($0 as! String, $1 as! String) })
let idKeyPairs = zip(rawKeyPairs, 0 ..< rawKeyPairs.count).map({ pair, index in (pair.0, pair.1, index) })
var pluralizationKeys = Set<String>()
var pluralizationBaseKeys = Set<String>()
for (key, _, _) in idKeyPairs {
if let (base, _) = pluralizationForm(key) {
pluralizationKeys.insert(key)
pluralizationBaseKeys.insert(base)
}
}
let pluralizationKeyPairs = zip(pluralizationBaseKeys, 0 ..< pluralizationBaseKeys.count).map({ ($0, $1) })
for (key, value, id) in idKeyPairs {
if pluralizationKeys.contains(key) {
continue
}
let arguments = parseArguments(value)
if !arguments.isEmpty {
result += " public func \(escapedIdentifier(key))(\(functionArguments(arguments))) -> (String, [(Int, NSRange)]) {\n"
result += " return formatWithArgumentRanges(self._s[\(id)]!, self._r[\(id)]!, [\(formatArguments(arguments))])\n"
result += " }\n"
} else {
result += " public var \(escapedIdentifier(key)): String { return self._s[\(id)]! }\n"
}
}
for (key, id) in pluralizationKeyPairs {
result +=
"""
public func \(escapedIdentifier(key))(_ value: Int32) -> String {
let form = presentationStringsPluralizationForm(self.lc, value)
return String(format: self._ps[\(id) * \(PluralizationForm.formCount) + Int(form.rawValue)]!, \"\\(value)\")
}
"""
}
result +=
"""
init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?) {
self.primaryComponent = primaryComponent
self.secondaryComponent = secondaryComponent
self.baseLanguageCode = secondaryComponent?.languageCode ?? primaryComponent.languageCode
let languageCode = primaryComponent.pluralizationRulesCode ?? primaryComponent.languageCode
var rawCode = languageCode as NSString
var range = rawCode.range(of: \"_\")
if range.location != NSNotFound {
rawCode = rawCode.substring(to: range.location) as NSString
}
range = rawCode.range(of: \"-\")
if range.location != NSNotFound {
rawCode = rawCode.substring(to: range.location) as NSString
}
rawCode = rawCode.lowercased as NSString
var lc: UInt32 = 0
for i in 0 ..< rawCode.length {
lc = (lc << 8) + UInt32(rawCode.character(at: i))
}
self.lc = lc
var _s: [Int: String] = [:]
var _r: [Int: [(Int, NSRange)]] = [:]
let loadedKeyMapping = keyMapping
let sIdList: [Int] = loadedKeyMapping.0
let sKeyList: [String] = loadedKeyMapping.1
let sArgIdList: [Int] = loadedKeyMapping.2
"""
let mappingResult = WriteBuffer()
let mappingKeyPairs = idKeyPairs.filter({ !pluralizationKeys.contains($0.0) })
mappingResult.append(Int32(mappingKeyPairs.count))
for (key, value, id) in mappingKeyPairs {
mappingResult.append(Int32(id))
mappingResult.append(key)
let arguments = parseArguments(value)
mappingResult.append(arguments.isEmpty ? 0 : 1)
}
result +=
"""
for i in 0 ..< sIdList.count {
_s[sIdList[i]] = getValue(primaryComponent, secondaryComponent, sKeyList[i])
}
for i in 0 ..< sArgIdList.count {
_r[sArgIdList[i]] = extractArgumentRanges(_s[sArgIdList[i]]!)
}
self._s = _s
self._r = _r
var _ps: [Int: String] = [:]
let pIdList: [Int] = loadedKeyMapping.3
let pKeyList: [String] = loadedKeyMapping.4
"""
mappingResult.append(Int32(pluralizationKeyPairs.count))
for (key, id) in pluralizationKeyPairs {
mappingResult.append(Int32(id))
mappingResult.append(key)
}
result +=
"""
for i in 0 ..< pIdList.count {
for form in 0 ..< \(PluralizationForm.formCount) {
_ps[pIdList[i] * \(PluralizationForm.formCount) + form] = getValueWithForm(primaryComponent, secondaryComponent, pKeyList[i], PluralizationForm(rawValue: Int32(form))!)
}
}
self._ps = _ps
"""
result += " }\n"
result += "}\n\n"
let _ = try? FileManager.default.removeItem(atPath: CommandLine.arguments[2])
let _ = try? FileManager.default.removeItem(atPath: CommandLine.arguments[3])
let _ = try? result.write(toFile: CommandLine.arguments[2], atomically: true, encoding: .utf8)
let _ = try? mappingResult.data.write(to: URL(fileURLWithPath: CommandLine.arguments[3]))
} else {
print("Couldn't read file")
exit(1)
}
}