Skip to content

Instantly share code, notes, and snippets.

@Jeehut
Created July 19, 2020 17:00
Show Gist options
  • Save Jeehut/c8c9a8caf8dc7c02583a4a07dfbb37aa to your computer and use it in GitHub Desktop.
Save Jeehut/c8c9a8caf8dc7c02583a4a07dfbb37aa to your computer and use it in GitHub Desktop.
Exploring safer localization workflows in SwiftUI ...
// Copyright © 2020 Flinesoft. All rights reserved.
import Foundation
import SwiftUI
public struct SafeLocalizedStringKey :
ExpressibleByStringLiteral,
ExpressibleByStringInterpolation,
ExpressibleByExtendedGraphemeClusterLiteral,
ExpressibleByUnicodeScalarLiteral,
Equatable
{
static var validationBundle: Bundle = Bundle.main
let unsafeKey: LocalizedStringKey
let stringsKey: String
public init(_ value: String) {
unsafeKey = LocalizedStringKey(value)
stringsKey = value
validateAvailabilityInSupportedLocales()
}
public init(stringLiteral value: String) {
unsafeKey = LocalizedStringKey(stringLiteral: value)
stringsKey = value
validateAvailabilityInSupportedLocales()
}
public init(stringInterpolation: StringInterpolation) {
unsafeKey = LocalizedStringKey(stringInterpolation: stringInterpolation.unsafeInterpolation)
stringsKey = stringInterpolation.interpolatedStringsKey
validateAvailabilityInSupportedLocales()
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.unsafeKey == rhs.unsafeKey
}
private func validateAvailabilityInSupportedLocales() {
#if DEBUG
let missingLocales: [String] = Self.validationBundle.localizations.filter { locale in
let localeBundle = Bundle(path: Self.validationBundle.path(forResource: locale, ofType: "lproj")!)!
let localizedValue = NSLocalizedString(stringsKey, bundle: localeBundle, comment: "")
return localizedValue == stringsKey || localizedValue.isEmpty
}
guard missingLocales.isEmpty else {
assertionFailure("Missing locales \(missingLocales) for localized string key '\(stringsKey)'.")
return
}
#endif
}
}
extension SafeLocalizedStringKey {
public struct StringInterpolation : StringInterpolationProtocol {
var unsafeInterpolation: LocalizedStringKey.StringInterpolation
var interpolatedStringsKey: String
public init(literalCapacity: Int, interpolationCount: Int) {
unsafeInterpolation = LocalizedStringKey.StringInterpolation(literalCapacity: literalCapacity, interpolationCount: interpolationCount)
interpolatedStringsKey = ""
}
public mutating func appendLiteral(_ literal: String) {
unsafeInterpolation.appendLiteral(literal)
interpolatedStringsKey.append(literal)
}
public mutating func appendInterpolation(_ string: String) {
unsafeInterpolation.appendInterpolation(string)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation<Subject>(_ subject: Subject, formatter: Formatter? = nil) where Subject : ReferenceConvertible {
unsafeInterpolation.appendInterpolation(subject, formatter: formatter)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation<Subject>(_ subject: Subject, formatter: Formatter? = nil) where Subject : NSObject {
unsafeInterpolation.appendInterpolation(subject, formatter: formatter)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation<T>(_ value: T) where T : _FormatSpecifiable {
unsafeInterpolation.appendInterpolation(value)
switch value {
case is Int, is Double:
interpolatedStringsKey.append("%lld")
default:
interpolatedStringsKey.append("%@@")
}
}
public mutating func appendInterpolation<T>(_ value: T, specifier: String) where T : _FormatSpecifiable {
unsafeInterpolation.appendInterpolation(value, specifier: specifier)
interpolatedStringsKey.append(specifier)
}
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
public mutating func appendInterpolation(_ text: Text) {
unsafeInterpolation.appendInterpolation(text)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation(_ image: Image) {
unsafeInterpolation.appendInterpolation(image)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation(_ date: Date, style: Text.DateStyle) {
unsafeInterpolation.appendInterpolation(date, style: style)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation(_ dates: ClosedRange<Date>) {
unsafeInterpolation.appendInterpolation(dates)
interpolatedStringsKey.append("%@")
}
public mutating func appendInterpolation(_ interval: DateInterval) {
unsafeInterpolation.appendInterpolation(interval)
interpolatedStringsKey.append("%@")
}
}
}
extension Button where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, action: @escaping () -> Void) {
self.init(titleKey.unsafeKey, action: action)
}
}
extension ColorPicker where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, selection: Binding<Color>, supportsOpacity: Bool = true) {
self.init(titleKey.unsafeKey, selection: selection, supportsOpacity: supportsOpacity)
}
}
extension CommandMenu {
public init(safe nameKey: SafeLocalizedStringKey, @ViewBuilder content: () -> Content) {
self.init(nameKey.unsafeKey, content: content)
}
}
extension DatePicker where Label == Text {
public init(
safe titleKey: SafeLocalizedStringKey,
selection: Binding<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date]
) {
self.init(titleKey.unsafeKey, selection: selection, displayedComponents: displayedComponents)
}
public init(
safe titleKey: SafeLocalizedStringKey,
selection: Binding<Date>,
in range: ClosedRange<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date]
) {
self.init(titleKey.unsafeKey, selection: selection, in: range, displayedComponents: displayedComponents)
}
public init(
safe titleKey: SafeLocalizedStringKey,
selection: Binding<Date>,
in range: PartialRangeFrom<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date]
) {
self.init(titleKey.unsafeKey, selection: selection, in: range, displayedComponents: displayedComponents)
}
public init(
safe titleKey: SafeLocalizedStringKey,
selection: Binding<Date>,
in range: PartialRangeThrough<Date>,
displayedComponents: DatePicker<Label>.Components = [.hourAndMinute, .date]
) {
self.init(titleKey.unsafeKey, selection: selection, in: range, displayedComponents: displayedComponents)
}
}
extension DisclosureGroup where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, @ViewBuilder content: @escaping () -> Content) {
self.init(titleKey.unsafeKey, content: content)
}
public init(safe titleKey: SafeLocalizedStringKey, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) {
self.init(titleKey.unsafeKey, isExpanded: isExpanded, content: content)
}
}
extension Label where Title == Text, Icon == Image {
public init(safe titleKey: SafeLocalizedStringKey, image name: String) {
self.init(titleKey.unsafeKey, image: name)
}
public init(safe titleKey: SafeLocalizedStringKey, systemImage name: String) {
self.init(titleKey.unsafeKey, systemImage: name)
}
}
extension Link where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, destination: URL) {
self.init(titleKey.unsafeKey, destination: destination)
}
}
extension NavigationLink where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, destination: Destination) {
self.init(titleKey.unsafeKey, destination: destination)
}
public init(safe titleKey: SafeLocalizedStringKey, destination: Destination, isActive: Binding<Bool>) {
self.init(titleKey.unsafeKey, destination: destination, isActive: isActive)
}
public init<V>(_ titleKey: SafeLocalizedStringKey, destination: Destination, tag: V, selection: Binding<V?>) where V : Hashable {
self.init(titleKey.unsafeKey, destination: destination, tag: tag, selection: selection)
}
}
extension Picker where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, selection: Binding<SelectionValue>, @ViewBuilder content: () -> Content) {
self.init(titleKey.unsafeKey, selection: selection, content: content)
}
}
extension ProgressView where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey) {
self.init(titleKey.unsafeKey)
}
public init<V>(_ titleKey: SafeLocalizedStringKey, value: V?, total: V = 1.0) where V : BinaryFloatingPoint {
self.init(titleKey.unsafeKey, value: value, total: total)
}
}
extension SecureField where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, text: Binding<String>, onCommit: @escaping () -> Void = {}) {
self.init(titleKey.unsafeKey, text: text, onCommit: onCommit)
}
}
extension Stepper where Label == Text {
public init(
safe titleKey: SafeLocalizedStringKey,
onIncrement: (() -> Void)?,
onDecrement: (() -> Void)?,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) {
self.init(titleKey.unsafeKey, onIncrement: onIncrement, onDecrement: onDecrement, onEditingChanged: onEditingChanged)
}
public init<V>(
safe titleKey: SafeLocalizedStringKey,
value: Binding<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) where V : Strideable {
self.init(titleKey.unsafeKey, value: value, step: step, onEditingChanged: onEditingChanged)
}
public init<V>(
safe titleKey: SafeLocalizedStringKey,
value: Binding<V>,
in bounds: ClosedRange<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) where V : Strideable {
self.init(titleKey.unsafeKey, value: value, in: bounds, step: step, onEditingChanged: onEditingChanged)
}
}
extension Text {
public init(safe key: SafeLocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) {
self.init(key.unsafeKey, tableName: tableName, bundle: bundle, comment: comment)
}
}
extension TextField where Label == Text {
public init(
safe titleKey: SafeLocalizedStringKey,
text: Binding<String>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
) {
self.init(titleKey.unsafeKey, text: text, onEditingChanged: onEditingChanged, onCommit: onCommit)
}
public init<T>(
safe titleKey: SafeLocalizedStringKey,
value: Binding<T>,
formatter: Formatter,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
) {
self.init(titleKey.unsafeKey, value: value, formatter: formatter, onEditingChanged: onEditingChanged)
}
}
extension Toggle where Label == Text {
public init(safe titleKey: SafeLocalizedStringKey, isOn: Binding<Bool>) {
self.init(titleKey.unsafeKey, isOn: isOn)
}
}
extension View {
public func navigationBarTitle(safe titleKey: SafeLocalizedStringKey) -> some View {
self.navigationBarTitle(titleKey.unsafeKey)
}
public func navigationBarTitle(safe titleKey: SafeLocalizedStringKey, displayMode: NavigationBarItem.TitleDisplayMode) -> some View {
self.navigationBarTitle(titleKey.unsafeKey, displayMode: displayMode)
}
public func navigationTitle(safe titleKey: SafeLocalizedStringKey) -> some View {
self.navigationBarTitle(titleKey.unsafeKey)
}
public func help(safe textKey: SafeLocalizedStringKey) -> some View {
self.help(textKey.unsafeKey)
}
}
extension WindowGroup {
public init(safe titleKey: SafeLocalizedStringKey, id: String, @ViewBuilder content: () -> Content) {
self.init(titleKey.unsafeKey, id: id, content: content)
}
public init(safe titleKey: SafeLocalizedStringKey, @ViewBuilder content: () -> Content) {
self.init(titleKey.unsafeKey, content: content)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment