-
-
Save robnadin/84b30508f791397f439cdc7798d4013f to your computer and use it in GitHub Desktop.
Elegant handling of localizable strings in Swift 5.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let color = "blue" | |
let num = 42 | |
localized("Colorless green ideas sleep furiously.") | |
localized("Colorless \(color) ideas sleep furiously.") | |
localized("\(num.formatted("%05d")) colorless green ideas sleep furiously.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LocalizableString.swift | |
// Scoreboard | |
// | |
// Created by Brent Royal-Gordon on 10/5/15. | |
// Copyright © 2015 Architechies. All rights reserved. | |
// | |
import Foundation | |
public func localized(_ str: LocalizableString, table: String? = nil, in bundle: Bundle = .main) -> String { | |
let format = str.formatString | |
let localizedFormat = bundle.localizedString(forKey: format, value: format, table: table) | |
return String(format: localizedFormat, arguments: str.formatArguments) | |
} | |
public extension CVarArg { | |
func formatted(_ string: String) -> LocalizableString { | |
return LocalizableString(segments: [.format(format: string, argument: self)]) | |
} | |
} | |
public struct LocalizableString { | |
fileprivate enum Segment { | |
case literal(String) | |
case format(format: String, argument: CVarArg) | |
init(object: NSObject) { | |
self = .format(format: "%@" , argument: object) | |
} | |
var format: String { | |
switch self { | |
case let .literal(str): | |
return str.replacingOccurrences(of: "%", with: "%%") | |
case let .format(fmt, _): | |
return fmt | |
} | |
} | |
var argument: CVarArg? { | |
if case let .format(_, arg) = self { | |
return arg | |
} | |
else { | |
return nil | |
} | |
} | |
} | |
fileprivate var segments: [Segment] | |
public init() { | |
segments = [] | |
} | |
fileprivate init(segments: [Segment]) { | |
self.segments = segments | |
} | |
public init(_ string: String) { | |
self.init(segments: [.literal(string)]) | |
} | |
public var formatString: String { | |
return segments.map { $0.format }.reduce("", +) | |
} | |
public var formatArguments: [CVarArg] { | |
return segments.map { $0.argument }.compactMap { $0 } | |
} | |
} | |
extension LocalizableString: ExpressibleByStringLiteral { | |
public init(stringLiteral value: String) { | |
self.init(value) | |
} | |
public init(unicodeScalarLiteral value: String) { | |
self.init(value) | |
} | |
public init(extendedGraphemeClusterLiteral value: String) { | |
self.init(value) | |
} | |
} | |
extension LocalizableString: ExpressibleByStringInterpolation { | |
public init(stringInterpolation: LocalizableString) { | |
self.init(segments: stringInterpolation.segments) | |
} | |
} | |
extension LocalizableString: StringInterpolationProtocol { | |
public init(literalCapacity: Int, interpolationCount: Int) { | |
self.init() | |
segments.reserveCapacity(2 * interpolationCount + 1) | |
} | |
public mutating func appendLiteral(_ literal: String) { | |
segments.append(.literal(literal)) | |
} | |
public mutating func appendInterpolation(_ input: NSObject) { | |
segments.append(.init(object: input)) | |
} | |
public mutating func appendInterpolation(_ input: String) { | |
segments.append(.init(object: input as NSObject)) | |
} | |
public mutating func appendInterpolation(_ input: LocalizableString) { | |
segments.append(contentsOf: input.segments) | |
} | |
public mutating func appendInterpolation<T>(_ expr: T) { | |
segments.append(.init(object: String(describing: expr) as NSObject)) | |
} | |
} | |
extension LocalizableString: CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { | |
public var description: String { | |
return String(format: formatString, arguments: formatArguments) | |
} | |
public var debugDescription: String { | |
return "localized(\"" + segments.map { $0.debugDescription }.reduce("", +) + "\")" | |
} | |
public var customMirror: Mirror { | |
return Mirror(self, children: ["formatString": formatString, "formatArguments": formatArguments]) | |
} | |
} | |
extension LocalizableString.Segment: CustomDebugStringConvertible { | |
fileprivate var debugDescription: String { | |
switch self { | |
case let .literal(string): | |
return string.replacingOccurrences(of: "\\", with: "\\\\") | |
case let .format(format, argument): | |
if format == "%@" { | |
return "\\(" + String(reflecting: argument) + ")" | |
} | |
else { | |
return "\\(" + String(reflecting: argument) + ".formatted(\"\(format)\"))" | |
} | |
} | |
} | |
} | |
extension LocalizableString: Equatable {} | |
public func == (lhs: LocalizableString, rhs: LocalizableString) -> Bool { | |
return lhs.formatString == rhs.formatString | |
} | |
public func += (lhs: inout LocalizableString, rhs: LocalizableString) { | |
lhs.segments += rhs.segments | |
} | |
public func + (lhs: LocalizableString, rhs: LocalizableString) -> LocalizableString { | |
var lhs = lhs | |
lhs += rhs | |
return lhs | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment