Skip to content

Commit 2e92b02

Browse files
connor-ricksstephencelismbrandonw
authored
Add support for LocalizedStringResource (#315)
* Add support for `LocalizedStringResource` * Update LocalizedStringResource usage to use a Any box. Updates LocalizedStringResource usage to use an Any box rather than using the string verbatim. This allows us to use the actual "correct" Text exposed by SwiftUI. - The Any box is required given the minimum version os swift-navigation. Initializing anything else besides a resource would be a programmer error, so encapsulating it in a box allows us to fail in one place rather than having pre-conditions everywhere. - Renames `.localized` to `.localizedStringKey` for clarity. * Cover all cases of enum storage. * clean up --------- Co-authored-by: Stephen Celis <[email protected]> Co-authored-by: Brandon Williams <[email protected]>
1 parent aa64e83 commit 2e92b02

File tree

2 files changed

+148
-32
lines changed

2 files changed

+148
-32
lines changed

Sources/SwiftNavigation/TextState.swift

Lines changed: 135 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,66 +119,128 @@ public struct TextState: Equatable, Hashable, Sendable {
119119
fileprivate enum Storage: Equatable, Hashable, @unchecked Sendable {
120120
indirect case concatenated(TextState, TextState)
121121
#if canImport(SwiftUI)
122-
case localized(
123-
LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?)
122+
case localizedStringKey(
123+
LocalizedStringKey,
124+
tableName: String?,
125+
bundle: Bundle?,
126+
comment: StaticString?
127+
)
124128
#endif
129+
case localizedStringResource(LocalizedStringResourceBox)
125130
case verbatim(String)
126131

127132
static func == (lhs: Self, rhs: Self) -> Bool {
128133
switch (lhs, rhs) {
129134
case (.concatenated(let l1, let l2), .concatenated(let r1, let r2)):
130135
return l1 == r1 && l2 == r2
136+
case (.concatenated, .localizedStringResource),
137+
(.localizedStringResource, .concatenated),
138+
(.concatenated, .verbatim),
139+
(.verbatim, .concatenated):
140+
// NB: We do not attempt to equate concatenated cases.
141+
return false
142+
case (.verbatim(let lhs), .verbatim(let rhs)):
143+
return lhs == rhs
144+
145+
case (.verbatim(let string), .localizedStringResource(let resource)),
146+
(.localizedStringResource(let resource), .verbatim(let string)):
147+
return string == resource.asString()
148+
149+
case (.localizedStringResource(let lhs), .localizedStringResource(let rhs)):
150+
return lhs.asString() == rhs.asString()
131151

132152
#if canImport(SwiftUI)
153+
case (.concatenated, .localizedStringKey),
154+
(.localizedStringKey, .concatenated):
155+
// NB: We do not attempt to equate concatenated cases.
156+
return false
157+
case (
158+
.verbatim(let string), .localizedStringKey(let key, let table, let bundle, let comment)
159+
),
160+
(.localizedStringKey(let key, let table, let bundle, let comment), .verbatim(let string)):
161+
return string == key.formatted(tableName: table, bundle: bundle, comment: comment)
162+
133163
case (
134-
.localized(let lk, let lt, let lb, let lc), .localized(let rk, let rt, let rb, let rc)
164+
.localizedStringKey(let lk, let lt, let lb, let lc),
165+
.localizedStringKey(let rk, let rt, let rb, let rc)
135166
):
136167
return lk.formatted(tableName: lt, bundle: lb, comment: lc)
137168
== rk.formatted(tableName: rt, bundle: rb, comment: rc)
138-
#endif
139169

140-
case (.verbatim(let lhs), .verbatim(let rhs)):
141-
return lhs == rhs
170+
case (
171+
.localizedStringKey(let key, let table, let bundle, let comment),
172+
.localizedStringResource(let resource)
173+
),
174+
(
175+
.localizedStringResource(let resource),
176+
.localizedStringKey(let key, let table, let bundle, let comment)
177+
):
178+
return key.formatted(tableName: table, bundle: bundle, comment: comment)
179+
== resource.asString()
142180

143-
#if canImport(SwiftUI)
144-
case (.localized(let key, let tableName, let bundle, let comment), .verbatim(let string)),
145-
(.verbatim(let string), .localized(let key, let tableName, let bundle, let comment)):
146-
return key.formatted(tableName: tableName, bundle: bundle, comment: comment) == string
147181
#endif
148-
149-
// NB: We do not attempt to equate concatenated cases.
150-
default:
151-
return false
152182
}
153183
}
154184

155185
func hash(into hasher: inout Hasher) {
156-
enum Key {
157-
case concatenated
158-
case localized
159-
case verbatim
160-
}
161-
162186
switch self {
163187
case (.concatenated(let first, let second)):
164-
hasher.combine(Key.concatenated)
165188
hasher.combine(first)
166189
hasher.combine(second)
167190

168191
#if canImport(SwiftUI)
169-
case .localized(let key, let tableName, let bundle, let comment):
170-
hasher.combine(Key.localized)
192+
case .localizedStringKey(let key, let tableName, let bundle, let comment):
171193
hasher.combine(key.formatted(tableName: tableName, bundle: bundle, comment: comment))
172194
#endif
173195

196+
case .localizedStringResource(let resource):
197+
hasher.combine(resource.asString())
198+
174199
case .verbatim(let string):
175-
hasher.combine(Key.verbatim)
176200
hasher.combine(string)
177201
}
178202
}
179203
}
180204
}
181205

206+
// MARK: - LocalizedStringResourceBox
207+
208+
private struct LocalizedStringResourceBox: @unchecked Sendable {
209+
// REVISIT: Make 'Any' into 'any Sendable' when minimum deployment target is iOS 18
210+
let value: Any
211+
212+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
213+
init(_ resource: LocalizedStringResource) {
214+
self.value = resource
215+
}
216+
217+
func asText() -> Text {
218+
guard
219+
#available(iOS 16, macOS 13, tvOS 16, watchOS 9, *),
220+
let resource = value as? LocalizedStringResource
221+
else {
222+
preconditionFailure(
223+
"LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available."
224+
)
225+
}
226+
227+
return Text(resource)
228+
}
229+
230+
func asString() -> String {
231+
guard
232+
#available(iOS 16, macOS 13, tvOS 16, watchOS 9, *),
233+
let resource = value as? LocalizedStringResource
234+
else {
235+
preconditionFailure(
236+
"LocalizedStringResourceBox should only be exposed where LocalizedStringResource is available."
237+
)
238+
}
239+
240+
return String(localized: resource)
241+
}
242+
}
243+
182244
// MARK: - API
183245

184246
extension TextState {
@@ -198,10 +260,24 @@ extension TextState {
198260
bundle: Bundle? = nil,
199261
comment: StaticString? = nil
200262
) {
201-
self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment)
263+
self.storage = .localizedStringKey(
264+
key,
265+
tableName: tableName,
266+
bundle: bundle,
267+
comment: comment
268+
)
202269
}
203270
#endif
204271

272+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
273+
public init(
274+
_ resource: LocalizedStringResource
275+
) {
276+
self.storage = .localizedStringResource(
277+
LocalizedStringResourceBox(resource)
278+
)
279+
}
280+
205281
public static func + (lhs: Self, rhs: Self) -> Self {
206282
.init(storage: .concatenated(lhs, rhs))
207283
}
@@ -391,13 +467,27 @@ extension TextState {
391467
return `self`
392468
}
393469

470+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
394471
public func accessibilityLabel(
395-
_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil,
472+
_ resource: LocalizedStringResource
473+
) -> Self {
474+
var `self` = self
475+
`self`.modifiers.append(
476+
.accessibilityLabel(.init(verbatim: String(localized: resource)))
477+
)
478+
return `self`
479+
}
480+
481+
public func accessibilityLabel(
482+
_ key: LocalizedStringKey,
483+
tableName: String? = nil,
484+
bundle: Bundle? = nil,
396485
comment: StaticString? = nil
397486
) -> Self {
398487
var `self` = self
399488
`self`.modifiers.append(
400-
.accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment)))
489+
.accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment))
490+
)
401491
return `self`
402492
}
403493

@@ -447,8 +537,10 @@ extension TextState {
447537
switch state.storage {
448538
case .concatenated(let first, let second):
449539
text = Text(first) + Text(second)
450-
case .localized(let content, let tableName, let bundle, let comment):
540+
case .localizedStringKey(let content, let tableName, let bundle, let comment):
451541
text = .init(content, tableName: tableName, bundle: bundle, comment: comment)
542+
case .localizedStringResource(let resourceBox):
543+
text = resourceBox.asText()
452544
case .verbatim(let content):
453545
text = .init(verbatim: content)
454546
}
@@ -465,9 +557,14 @@ extension TextState {
465557
switch value.storage {
466558
case .verbatim(let string):
467559
return text.accessibilityLabel(string)
468-
case .localized(let key, let tableName, let bundle, let comment):
560+
case .localizedStringKey(let key, let tableName, let bundle, let comment):
469561
return text.accessibilityLabel(
470-
Text(key, tableName: tableName, bundle: bundle, comment: comment))
562+
Text(key, tableName: tableName, bundle: bundle, comment: comment)
563+
)
564+
case .localizedStringResource(let resourceBox):
565+
return text.accessibilityLabel(
566+
resourceBox.asText()
567+
)
471568
case .concatenated(_, _):
472569
assertionFailure("`.accessibilityLabel` does not support concatenated `TextState`")
473570
return text
@@ -572,7 +669,7 @@ extension String {
572669
self = String(state: lhs, locale: locale) + String(state: rhs, locale: locale)
573670

574671
#if canImport(SwiftUI)
575-
case .localized(let key, let tableName, let bundle, let comment):
672+
case .localizedStringKey(let key, let tableName, let bundle, let comment):
576673
self = key.formatted(
577674
locale: locale,
578675
tableName: tableName,
@@ -581,6 +678,9 @@ extension String {
581678
)
582679
#endif
583680

681+
case .localizedStringResource(let resourceBox):
682+
self = resourceBox.asString()
683+
584684
case .verbatim(let string):
585685
self = string
586686
}
@@ -637,9 +737,12 @@ extension TextState: CustomDumpRepresentable {
637737
case .concatenated(let lhs, let rhs):
638738
output = dumpHelp(lhs) + dumpHelp(rhs)
639739
#if canImport(SwiftUI)
640-
case .localized(let key, let tableName, let bundle, let comment):
740+
case .localizedStringKey(let key, let tableName, let bundle, let comment):
641741
output = key.formatted(tableName: tableName, bundle: bundle, comment: comment)
642742
#endif
743+
case .localizedStringResource(let resourceBox):
744+
output = resourceBox.asString()
745+
643746
case .verbatim(let string):
644747
output = string
645748
}

Tests/SwiftNavigationTests/TextStateTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,18 @@ final class TextStateTests: XCTestCase {
7272
"""#
7373
)
7474
}
75+
76+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
77+
func testTextStateLocalizedStringResource() {
78+
var dump = ""
79+
let resource = LocalizedStringResource("hello.world", defaultValue: "Hello, world!")
80+
customDump(TextState(resource), to: &dump)
81+
XCTAssertEqual(
82+
dump,
83+
"""
84+
"Hello, world!"
85+
"""
86+
)
87+
}
7588
#endif
7689
}

0 commit comments

Comments
 (0)