diff --git a/Sources/SkipFoundation/PropertyListSerialization.swift b/Sources/SkipFoundation/PropertyListSerialization.swift index e098d21..2daf11d 100644 --- a/Sources/SkipFoundation/PropertyListSerialization.swift +++ b/Sources/SkipFoundation/PropertyListSerialization.swift @@ -106,14 +106,8 @@ public class PropertyListSerialization { let categories = ["zero", "one", "two", "few", "many", "other"] let rules = categories.compactMap { (category: String) -> String? in guard let pluralString = varDict[category] as? String else { return nil } - var cleaned = pluralString! - for index in 1...10 { - cleaned = cleaned - .replacingOccurrences(of: "%\(index)$@", with: "{\(index-1)}") - .replacingOccurrences(of: "%\(index)$d", with: "{\(index-1)}") - } let icuCategory = category == "zero" ? "=0" : category - return "\(icuCategory){\(cleaned)}" + return "\(icuCategory){\(icuMessage(fromFormat: pluralString!))}" }.joined(separator: " ") if !rules.isEmpty { @@ -140,6 +134,43 @@ public class PropertyListSerialization { return result } + /// Converts a printf-style plural form into ICU MessageFormat text: each specifier becomes an argument + /// (`%lld` and `%1$d` become `{0,number,#}`, without grouping like Darwin, and `%@` becomes `{0}`), + /// and ICU syntax characters in the literal text are quoted. + static func icuMessage(fromFormat format: String) -> String { + let specifier = java.util.regex.Pattern.compile("%(?:(\\d+)\\$)?[-+ 0#']*\\d*(?:\\.\\d+)?(?:hh|h|ll|l|q|z|t|j)?([@dDiuUxXoOfFeEgGaAcCsSp%])") + let matcher = specifier.matcher(format) + var result = "" + var literalStart = 0 + var nextArgument = 0 + while matcher.find() { + result += icuLiteral(format.substring(literalStart, matcher.start())) + literalStart = matcher.end() + let conversion = matcher.group(2)! + if conversion == "%" { + result += "%" + continue + } + var argument = nextArgument + if let position = matcher.group(1) { + argument = position.toInt() - 1 + } else { + nextArgument += 1 + } + result += "dDiuU".contains(conversion) ? "{\(argument),number,#}" : "{\(argument)}" + } + result += icuLiteral(format.substring(literalStart)) + return result + } + + private static func icuLiteral(_ text: String) -> String { + text.replace("'", "''") + .replace("{", "'{'") + .replace("}", "'}'") + .replace("#", "'#'") + .replace("|", "'|'") + } + private static func openStepPropertyList(from data: Data, options: PropertyListSerialization.ReadOptions = []) throws -> [String: String]? { var dict: Dictionary = [:] diff --git a/Tests/SkipFoundationTests/Foundation/LocaleTests.swift b/Tests/SkipFoundationTests/Foundation/LocaleTests.swift index 455c393..bba5622 100644 --- a/Tests/SkipFoundationTests/Foundation/LocaleTests.swift +++ b/Tests/SkipFoundationTests/Foundation/LocaleTests.swift @@ -178,6 +178,47 @@ final class LocaleTests: XCTestCase { XCTAssertNotNil(dict["repeats_every_day"]) } + func testLocalizableStringsDictPrintfSpecifiers() throws { + #if !SKIP + throw XCTSkip("Darwin formats .stringsdict plurals natively") + #else + let locstr = #""" + + + + + %lld files + + NSStringLocalizedFormatKey + %#@value@ + value + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + zero + No files + one + %lld file, it's #1 + other + %lld files {100%%} + + + + + """# + + let data = try XCTUnwrap(locstr.data(using: .utf8)) + let plist = try XCTUnwrap(PropertyListSerialization.propertyList(from: data, options: [], format: nil)) + let format = try XCTUnwrap(plist["%lld files"]) + + XCTAssertEqual(String.localizedStringWithFormat(format, 0), "No files") + XCTAssertEqual(String.localizedStringWithFormat(format, 1), "1 file, it's #1") + XCTAssertEqual(String.localizedStringWithFormat(format, 1234), "1234 files {100%}") + #endif + } + func testLocaleFormats() throws { #if !SKIP // TODO