Created
July 19, 2020 17:00
-
-
Save Jeehut/c8c9a8caf8dc7c02583a4a07dfbb37aa to your computer and use it in GitHub Desktop.
Exploring safer localization workflows in SwiftUI ...
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
// 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