Split out format specifiers

This commit is contained in:
Tom Lokhorst
2016-04-20 08:12:51 +02:00
parent 86cb234c9e
commit b2e3435971
10 changed files with 206 additions and 109 deletions

View File

@@ -96,6 +96,8 @@
D5F97E4C1C1819160066D7C0 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F97E411C1816360066D7C0 /* Image.swift */; };
D5F97E4D1C1819160066D7C0 /* ResourceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F97E431C1816790066D7C0 /* ResourceFile.swift */; };
D5F97E4E1C1819160066D7C0 /* Nib.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F97E451C18169E0066D7C0 /* Nib.swift */; };
E2156B8E1CC5254A00F341DC /* FormatSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2156B8D1CC5254A00F341DC /* FormatSpecifier.swift */; };
E2156B8F1CC5255000F341DC /* FormatSpecifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2156B8D1CC5254A00F341DC /* FormatSpecifier.swift */; };
E22D43631C9582CA00692FFF /* ColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22D43621C9582CA00692FFF /* ColorGenerator.swift */; };
E22D43651C95845200692FFF /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22D43641C95845200692FFF /* ColorPalette.swift */; };
E24720CB1C96B6A600DF291D /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22D43641C95845200692FFF /* ColorPalette.swift */; };
@@ -151,6 +153,7 @@
D5F97E411C1816360066D7C0 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
D5F97E431C1816790066D7C0 /* ResourceFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceFile.swift; sourceTree = "<group>"; };
D5F97E451C18169E0066D7C0 /* Nib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Nib.swift; sourceTree = "<group>"; };
E2156B8D1CC5254A00F341DC /* FormatSpecifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatSpecifier.swift; sourceTree = "<group>"; };
E22D43621C9582CA00692FFF /* ColorGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorGenerator.swift; sourceTree = "<group>"; };
E22D43641C95845200692FFF /* ColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -273,6 +276,7 @@
D5F97E3F1C1815E70066D7C0 /* AssetFolder.swift */,
E22D43641C95845200692FFF /* ColorPalette.swift */,
D5F97E391C1812AE0066D7C0 /* Font.swift */,
E2156B8D1CC5254A00F341DC /* FormatSpecifier.swift */,
D5F97E411C1816360066D7C0 /* Image.swift */,
5D997C961C7C2BEE00B2F376 /* LocalizableStrings.swift */,
D5F97E451C18169E0066D7C0 /* Nib.swift */,
@@ -444,6 +448,7 @@
D5B799721C199755009EA901 /* ImageGenerator.swift in Sources */,
D5B7997A1C19C1BD009EA901 /* WhiteListedExtensionsResourceType.swift in Sources */,
D59F722A1C1963EA0089767C /* Struct.swift in Sources */,
E2156B8F1CC5255000F341DC /* FormatSpecifier.swift in Sources */,
D5A0A82E1C4793C20089ED2C /* TypePrinter.swift in Sources */,
D5A0A82D1C4793C20089ED2C /* SwiftCodeConverible.swift in Sources */,
D5B799731C199755009EA901 /* NibGenerator.swift in Sources */,
@@ -465,6 +470,7 @@
files = (
5D997C951C7C291900B2F376 /* StringsGenerator.swift in Sources */,
D5B799831C1B8C78009EA901 /* Module.swift in Sources */,
E2156B8E1CC5254A00F341DC /* FormatSpecifier.swift in Sources */,
D5C5A8EF1BB7196000163E71 /* Core.swift in Sources */,
D58672491C21FC9700A760EC /* TypeSequenceProvider.swift in Sources */,
D5F97E461C18169E0066D7C0 /* Nib.swift in Sources */,

View File

@@ -41,7 +41,7 @@ struct StringsGenerator: Generator {
private static func stringStructFromLocalizableStrings(filename: String, strings: [LocalizableStrings]) -> Struct? {
var allParams: [String: [Type]] = [:]
var allParams: [String: [FormatSpecifier]] = [:]
let baseKeys = strings
.filter { $0.locale.isBase }
.map { Set($0.dictionary.keys) }
@@ -91,17 +91,28 @@ struct StringsGenerator: Generator {
warn("Strings file \(filenameLocale) is missing translations for keys: \(paddedKeysString)")
}
func includeParam(key: String) -> Bool {
if let baseKeys = baseKeys {
return baseKeys.contains(key)
}
return true
}
return Struct(
type: Type(module: .Host, name: sanitizedSwiftName(filename)),
implements: [],
typealiasses: [],
properties: [],
functions: allParams.map { ($0.0, $0.1, filename)}.map(StringsGenerator.stringFunction),
functions: allParams
.filter { includeParam($0.0) }
.map { ($0.0, $0.1, filename)}
.map(StringsGenerator.stringFunction),
structs: []
)
}
private static func stringFunction(key: String, params: [Type], tableName: String) -> Function {
private static func stringFunction(key: String, params: [FormatSpecifier], tableName: String) -> Function {
if params.isEmpty {
return stringFunctionNoParams(key, tableName: tableName)
}
@@ -134,16 +145,16 @@ struct StringsGenerator: Generator {
)
}
private static func stringFunctionParams(key: String, params: [Type], tableName: String) -> Function {
private static func stringFunctionParams(key: String, params: [FormatSpecifier], tableName: String) -> Function {
let params = params.enumerate().map { ix, type -> Function.Parameter in
let params = params.enumerate().map { ix, formatSpecifier -> Function.Parameter in
let name = "value\(ix + 1)"
if ix == 0 {
return Function.Parameter(name: name, type: type)
return Function.Parameter(name: name, type: formatSpecifier.type)
}
else {
return Function.Parameter(name: "_", localName: name, type: type)
return Function.Parameter(name: "_", localName: name, type: formatSpecifier.type)
}
}

View File

@@ -0,0 +1,146 @@
//
// FormatSpecifier.swift
// R.swift
//
// Created by Tom Lokhorst on 2016-04-18.
// Copyright © 2016 Mathijs Kadijk. All rights reserved.
//
import Foundation
// https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW1
enum FormatSpecifier {
case Object
case Double
case Int
case UInt
case Character
case CStringPointer
case VoidPointer
case TopType
var type: Type {
switch self {
case .Object:
return Type._String
case .Double:
return Type._Double
case .Int:
return Type._Int
case .UInt:
return Type._UInt
case .Character:
return Type._Character
case .CStringPointer:
return Type._CStringPointer
case .VoidPointer:
return Type._VoidPointer
case .TopType:
return Type._Any
}
}
}
extension FormatSpecifier {
init?(formatChar char: Swift.Character) {
let lcChar = Swift.String(char).lowercaseString.characters.first!
switch lcChar {
case "@":
self = .Object
case "a", "e", "f", "g":
self = .Double
case "d", "i":
self = .Int
case "o", "u", "x":
self = .UInt
case "c":
self = .Character
case "s":
self = .CStringPointer
case "p":
self = .VoidPointer
default:
return nil
}
}
static func formatSpecifiersFromFormatString(formatString: String) -> [FormatSpecifier] {
return _formatSpecifiersFromFormatString(formatString)
}
}
// Based on StringsFileParser.swift from SwiftGen
private let formatTypesRegEx: NSRegularExpression = {
let pattern_int = "(?:h|hh|l|ll|q|z|t|j)?([dioux])" // %d/%i/%o/%u/%x with their optional length modifiers like in "%lld"
let pattern_float = "[aefg]"
let position = "([1-9]\\d*\\$)?" // like in "%3$" to make positional specifiers
let precision = "[-+]?\\d?(?:\\.\\d)?" // precision like in "%1.2f"
do {
return try NSRegularExpression(pattern: "(?<!%)%\(position)\(precision)(@|\(pattern_int)|\(pattern_float)|[csp])", options: [.CaseInsensitive])
} catch {
fatalError("Error building the regular expression used to match string formats")
}
}()
// "I give %d apples to %@" --> [.Int, .String]
private func _formatSpecifiersFromFormatString(formatString: String) -> [FormatSpecifier] {
let nsString = formatString as NSString
let range = NSRange(location: 0, length: nsString.length)
// Extract the list of chars (conversion specifiers) and their optional positional specifier
let chars = formatTypesRegEx.matchesInString(formatString, options: [], range: range).map { match -> (String, Int?) in
let range: NSRange
if match.rangeAtIndex(3).location != NSNotFound {
// [dioux] are in range #3 because in #2 there may be length modifiers (like in "lld")
range = match.rangeAtIndex(3)
} else {
// otherwise, no length modifier, the conversion specifier is in #2
range = match.rangeAtIndex(2)
}
let char = nsString.substringWithRange(range)
let posRange = match.rangeAtIndex(1)
if posRange.location == NSNotFound {
// No positional specifier
return (char, nil)
} else {
// Remove the "$" at the end of the positional specifier, and convert to Int
let posRange1 = NSRange(location: posRange.location, length: posRange.length-1)
let pos = nsString.substringWithRange(posRange1)
return (char, Int(pos))
}
}
// enumerate the conversion specifiers and their optionally forced position and build the array of format specifiers accordingly
var list = [FormatSpecifier]()
var nextNonPositional = 1
for (str, pos) in chars {
if let char = str.characters.first, let p = FormatSpecifier(formatChar: char) {
let insertionPos: Int
if let pos = pos {
insertionPos = pos
}
else {
insertionPos = nextNonPositional
nextNonPositional += 1
}
if insertionPos > 0 {
while list.count <= insertionPos-1 {
list.append(FormatSpecifier.TopType)
}
list[insertionPos-1] = p
}
}
}
return list
}

View File

@@ -27,7 +27,7 @@ struct LocalizableStrings: WhiteListedExtensionsResourceType {
let filename: String
let locale: Locale
let dictionary: [String : (value: String, params: [Type])]
let dictionary: [String : (value: String, params: [FormatSpecifier])]
init(url: NSURL) throws {
try LocalizableStrings.throwIfUnsupportedExtension(url.pathExtension)
@@ -55,13 +55,13 @@ struct LocalizableStrings: WhiteListedExtensionsResourceType {
}
// Parse strings from NSDictionary
var dictionary: [String : (value: String, params: [Type])] = [:]
var dictionary: [String : (value: String, params: [FormatSpecifier])] = [:]
for (key, obj) in nsDictionary {
if let
key = key as? String,
val = obj as? String
{
dictionary[key] = (val, typesFromFormatString(val))
dictionary[key] = (val, FormatSpecifier.formatSpecifiersFromFormatString(val))
}
else {
throw ResourceParsingError.ParsingFailed("Non-string value in \(url.absoluteString): \(key) = \(obj)")
@@ -73,97 +73,3 @@ struct LocalizableStrings: WhiteListedExtensionsResourceType {
self.dictionary = dictionary
}
}
// https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW1
extension Type {
init?(formatChar char: Character) {
let lcChar = String(char).lowercaseString.characters.first!
switch lcChar {
case "@":
self = Type._String
case "a", "e", "f", "g":
self = Type._Double
case "d", "i":
self = Type._Int
case "o", "u", "x":
self = Type._UInt
case "c":
self = Type._Character
case "s":
self = Type._CStringPointer
case "p":
self = Type._VoidPointer
default:
return nil
}
}
}
// Based on StringsFileParser.swift from SwiftGen
private let formatTypesRegEx: NSRegularExpression = {
let pattern_int = "(?:h|hh|l|ll|q|z|t|j)?([dioux])" // %d/%i/%o/%u/%x with their optional length modifiers like in "%lld"
let pattern_float = "[aefg]"
let position = "([1-9]\\d*\\$)?" // like in "%3$" to make positional specifiers
let precision = "[-+]?\\d?(?:\\.\\d)?" // precision like in "%1.2f"
do {
return try NSRegularExpression(pattern: "(?<!%)%\(position)\(precision)(@|\(pattern_int)|\(pattern_float)|[csp])", options: [.CaseInsensitive])
} catch {
fatalError("Error building the regular expression used to match string formats")
}
}()
// "I give %d apples to %@" --> [.Int, .String]
private func typesFromFormatString(formatString: String) -> [Type] {
let range = NSRange(location: 0, length: (formatString as NSString).length)
// Extract the list of chars (conversion specifiers) and their optional positional specifier
let chars = formatTypesRegEx.matchesInString(formatString, options: [], range: range).map { match -> (String, Int?) in
let range: NSRange
if match.rangeAtIndex(3).location != NSNotFound {
// [dioux] are in range #3 because in #2 there may be length modifiers (like in "lld")
range = match.rangeAtIndex(3)
} else {
// otherwise, no length modifier, the conversion specifier is in #2
range = match.rangeAtIndex(2)
}
let char = (formatString as NSString).substringWithRange(range)
let posRange = match.rangeAtIndex(1)
if posRange.location == NSNotFound {
// No positional specifier
return (char, nil)
} else {
// Remove the "$" at the end of the positional specifier, and convert to Int
let posRange1 = NSRange(location: posRange.location, length: posRange.length-1)
let pos = (formatString as NSString).substringWithRange(posRange1)
return (char, Int(pos))
}
}
// enumerate the conversion specifiers and their optionally forced position and build the array of Types accordingly
var list = [Type]()
var nextNonPositional = 1
for (str, pos) in chars {
if let char = str.characters.first, let p = Type(formatChar: char) {
let insertionPos: Int
if let pos = pos {
insertionPos = pos
}
else {
insertionPos = nextNonPositional
nextNonPositional += 1
}
if insertionPos > 0 {
while list.count <= insertionPos-1 {
list.append(Type._Any)
}
list[insertionPos-1] = p
}
}
}
return list
}

View File

@@ -13,14 +13,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setValue(["es"], forKey: "AppleLanguages")
defaults.setValue(["en"], forKey: "AppleLanguages")
print(R.string.settings.notTranslated())
print(R.string.settings.onlyDutch())
print(R.string.settings.formatSpecifiers1(11, 22, "str"))
print(R.string.settings.formatSpecifiers2(11, 22, "str"))
print(R.string.settings.formatSpecifiers3())
print(R.string.settings.formatSpecifiers4(11))
print(R.string.settings.formatSpecifiers5(11, "str"))
// Override point for customization after application launch.
return true

View File

@@ -10,4 +10,13 @@
"Not translated" = "Base language; Not translated";
"Multiline" = "ABC
DEF
GHI";
"Copy.Progress" = "%1$d of %2$i files copied, %3$f.2%% completed.";
"FormatSpecifiers1" = "number 1: %d, number 2: %i, string 3: %@";
"FormatSpecifiers2" = "string 3: %3$@, number 2: %2$d, number 1: %1$i";
"FormatSpecifiers3" = "Nothing";
"FormatSpecifiers4" = "number 1: %1$d";
"FormatSpecifiers5" = "number 1: %d, string 3: %@";

View File

@@ -6,6 +6,7 @@
// Copyright © 2016 Nolan Warner. All rights reserved.
//
one = Uno;
two = 2;

View File

@@ -6,6 +6,7 @@
// Copyright © 2016 Nolan Warner. All rights reserved.
//
one = "一つ";
two = 2;

View File

@@ -10,4 +10,13 @@
"Only Dutch" = "Alleen Nederlands. Doesn't apepar in Base translation";
"Multiline" = "ABC
DEF
GHI";
"Copy.Progress" = "Van de %2$d bestanden zijn er %1$d gekopieerd, %3$.2f%% compleet.";
"FormatSpecifiers1" = "number 1: %d, number 2: %i";
"FormatSpecifiers2" = "string 3: %3$@, number 1: %1$i";
"FormatSpecifiers3" = "number 2: %2$d, string 3: %3$@, number 1: %1$i";
"FormatSpecifiers4" = "number 1: %d, number 2: %i, string 3: %@";
"FormatSpecifiers5" = "number 1: %d, number 2: %i, string 3: %@";

View File

@@ -13,8 +13,14 @@ import XCTest
class StringsTests: XCTestCase {
func testNoNilStrings() {
XCTAssertNotNil(R.string.localizable.one())
XCTAssertNotNil(R.string.generic.loremipsum())
XCTAssertNotNil(R.string.localizable.japaneseOnly())
XCTAssertNotNil(R.string.localizable.one())
XCTAssertNotNil(R.string.localizable.quote(4))
XCTAssertNotNil(R.string.localizable.two())
XCTAssertNotNil(R.string.settings.copyProgress(2, 4, 50.0))
XCTAssertNotNil(R.string.settings.multiline())
XCTAssertNotNil(R.string.settings.notTranslated())
XCTAssertNotNil(R.string.settings.title())
}
}