Skip to content

Instantly share code, notes, and snippets.

@robnadin
Forked from beccadax/Example.swift
Last active February 11, 2020 07:02
Show Gist options
  • Save robnadin/84b30508f791397f439cdc7798d4013f to your computer and use it in GitHub Desktop.
Save robnadin/84b30508f791397f439cdc7798d4013f to your computer and use it in GitHub Desktop.
Elegant handling of localizable strings in Swift 5.
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.")
//
// 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