Skip to content

Commit 74dacc0

Browse files
authored
Merge branch 'swiftlang:main' into implementation/progress-reporter
2 parents 4adf585 + f405ad1 commit 74dacc0

File tree

5 files changed

+277
-1
lines changed

5 files changed

+277
-1
lines changed

Proposals/0024-CurrentBundle.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Introduce `#bundle`
2+
3+
4+
* Proposal: [SF-0024](0024-filename.md)
5+
* Authors:[Matt Seaman](https://github.com/matthewseaman), [Andreas Neusuess](https://github.com/Tantalum73)
6+
* Review Manager: [Tina L](https://github.com/itingliu)
7+
* Status: **Accepted**
8+
9+
10+
## Revision history
11+
12+
* **v1** Initial version
13+
* **v1.1** Remove `#bundleDescription` and add 2 initializers to `LocalizedStringResource`
14+
15+
## Introduction
16+
17+
API which loads localized strings assumes `Bundle.main` by default. This works for apps, but code that runs in a framework, or was defined in a Swift package, needs to specify a different bundle. The ultimate goal is to remove this requirement in the future. One step towards that goal is to provide an easy accessor to the bundle that stores localized resources: `#bundle`.
18+
19+
## Motivation
20+
21+
Developers writing code in a framework or a Swift package need to repeat the `bundle` parameter for every localized string.
22+
Without any shortcuts, loading a localized string from a framework looks like this:
23+
24+
```swift
25+
label.text = String(
26+
localized: "She didn't clean the camera!",
27+
bundle: Bundle(for: MyViewController.self),
28+
comment: "Comment of astonished bystander"
29+
)
30+
```
31+
32+
Because of its impracticalities, developers often write accessors to the framework's bundle:
33+
34+
```swift
35+
private class LookupClass {}
36+
extension Bundle {
37+
static let framework = Bundle(for: LookupClass.self)
38+
39+
// Or worse yet, they lookup the bundle using its bundle identifier, which while tempting is actually rather inefficient.
40+
}
41+
42+
label.text = String(
43+
localized: "She didn't clean the camera!",
44+
bundle: .framework,
45+
comment: "Comment of astonished bystander"
46+
)
47+
```
48+
49+
While this solution requires less boilerplate, each framework target has to write some boilerplate still.
50+
51+
In the context of a localized Swift package, the build system takes care of creating an extension on `Bundle` called `Bundle.module` at build time. While this reduces the need for boilerplate already, it makes it complicated to move code from a framework or app target into a Swift package. Each call to a localization API needs to be audited and changed to `bundle: .module`.
52+
53+
54+
## Proposed solution and example
55+
56+
We propose a macro that handles locating the right bundle with localized resources. It will work in all contexts: apps, framework targets, and Swift packages.
57+
58+
```swift
59+
label.text = String(
60+
localized: "She didn't clean the camera!",
61+
bundle: #bundle,
62+
comment: "Comment of astonished bystander"
63+
)
64+
```
65+
66+
## Detailed design
67+
68+
We propose introducing a `#bundle` macro as follows:
69+
70+
```swift
71+
/// Returns the bundle most likely to contain resources for the calling code.
72+
///
73+
/// Code in an app, app extension, framework, etc. will return the bundle associated with that target.
74+
/// Code in a Swift Package target will return the resource bundle associated with that target.
75+
@available(macOS 10.0, iOS 2.0, tvOS 9.0, watchOS 2.0, *)
76+
@freestanding(expression)
77+
public macro bundle() -> Bundle = #externalMacro(module: "FoundationMacros", type: "CurrentBundleMacro")
78+
```
79+
80+
`#bundle` would expand to:
81+
82+
```swift
83+
{
84+
#if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE
85+
return Bundle.module
86+
#elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE
87+
#error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.")
88+
#else
89+
return Bundle(_dsoHandle: #dsohandle) ?? .main
90+
#endif
91+
}()
92+
```
93+
94+
This macro relies on `SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE `, a new `-D`-defined conditional that will be passed by SwiftBuild, SwiftPM, and potential 3rd party build systems under the same conditions where `Bundle.module` would be generated.
95+
96+
The preprocessor macro `SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE` should be set by build systems when `Bundle.module` is not generated and the fallback `#dsohandle` approach would not retrieve the correct bundle for resources. A Swift Package without any resource files would be an example of this. Under this scenario, usage of `#bundle` presents an error.
97+
98+
99+
It calls into new API on `Bundle`, which will be back-deployed so that using the macro isn't overly limited by the project's deployment target.
100+
101+
```swift
102+
extension Bundle {
103+
/// Creates an instance of `Bundle` from the current value for `#dsohandle`.
104+
///
105+
/// - warning: Don't call this method directly, and use `#bundle` instead.
106+
///
107+
/// In the context of a Swift Package or other static library,
108+
/// the result is the bundle that contains the produced binary, which may be
109+
/// different from where resources are stored.
110+
///
111+
/// - Parameter dsoHandle: `dsohandle` of the current binary.
112+
@available(FoundationPreview 6.2, *)
113+
@_alwaysEmitIntoClient
114+
public convenience init?(_dsoHandle: UnsafeRawPointer)
115+
```
116+
117+
The type `LocalizedStringResource` (LSR) doesn't operate on instances of `Bundle`, but `LocalizedStringResource.BundleDescription`. They can easily be converted into each other.
118+
To make the new macro work well with LSR, we suggest adding two new initializers. We mark them as `@_alwaysEmitIntoClient` and `@_disfavoredOverload`, to avoid ambiguity over the initializers accepting a `BundleDescription` parameter:
119+
120+
```swift
121+
@available(FoundationPreview 6.2, *)
122+
extension LocalizedStringResource {
123+
@_alwaysEmitIntoClient
124+
@_disfavoredOverload
125+
public init(_ keyAndValue: String.LocalizationValue, table: String? = nil, locale: Locale = .current, bundle: Bundle, comment: StaticString? = nil)
126+
127+
@_alwaysEmitIntoClient
128+
@_disfavoredOverload
129+
public init(_ key: StaticString, defaultValue: String.LocalizationValue, table: String? = nil, locale: Locale = .current, bundle: Bundle, comment: StaticString? = nil)
130+
}
131+
```
132+
133+
## Impact on existing code
134+
135+
This change is purely additive.
136+
137+
## Alternatives considered
138+
139+
### Not using a macro
140+
141+
We chose a macro because it gives us the most flexibility to update the implementation later.
142+
This will allow us to eventually use `#bundle` (or a wrapping macro) as the default argument for the bundle parameter, which (since [SE-0422](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0422-caller-side-default-argument-macro-expression.md)) will get expanded in the caller.
143+
144+
Also, only a macro lets us properly implement this for Swift Package targets since we need to either call `Bundle.module` (which only exists as a code-gen'd, internal symbol in clients) or access build-time information such as the name of the target.
145+
146+
### Not doing this change
147+
148+
Without this macro, developers will continue to have to write extensions on `Bundle` or repeat calling `Bundle(for: )` in their code.
149+
150+
151+
### Using the name `#currentResourceBundle`
152+
153+
Previously we discussed using the name `#currentResourceBundle` for the proposed new macro. It has been determined that `ResourceBundle` and `Bundle` describe the same thing in terms of loading resources. This macro will be used to load resources from the current bundle, repeating the fact that the current "resource bundle" is not necessary.
154+
155+
### Using the name `#currentBundle`
156+
157+
Previously we discussed using the name `#currentBundle` for the proposed new macro. It was pointed out that Swift already uses macros like `#filePath` or `#line`, which also imply "current".
158+
159+
While `#filePath` and `#line` are unambiguous, `#bundle` could be perceived as another way to spell `Bundle.main`. Calling it `#currentBundle` would help differentiate it from `Bundle.main`.
160+
161+
However, in the context of loading resources, `#bundle` is more accurate than `Bundle.main`, as it's correct in the majority of scenarios. Developers specifying `Bundle.main` when loading resources often want what `#bundle` offers, and calling the macro `#bundle` makes it easier to discover.
162+
163+
We think that consistency with existing Swift macros overweighs, and that the similarity to `Bundle.main` is an advantage for discoverability.
164+
165+
### Using a separate macro for `LocalizedStringResource.BundleDescription`
166+
An earlier version of this proposal suggested to add `#bundle` and `#bundleDescription`, to work with `String(localized: ... bundle: Bundle)` and `LocalizedStringResource(... bundle: LocalizedStringResource.BundleDescription)`.
167+
168+
Upon closer inspection, we can make LSR work with an instance of `Bundle` and have the proposed initializer convert it to a `LocalizedStringResource.BundleDescription` internally. This way, we only have to provide one macro, which makes it easier to discover for developers.
169+
170+
171+
## Future Directions
172+
173+
## Infer `currentBundle` by default
174+
175+
This change is the first step towards not having to specify a bundle at all. Ideally, localizing a string should not require more work than using a type or method call that expresses localizability (i.e. `String.LocalizationValue`, `LocalizedStringResource`, or `String(localized: )`).
176+
177+
178+
## Compute Package resource bundles without Bundle.module
179+
180+
If we enhance `MacroExpansionContext` to include some additional information from the build system (such as target name and type), we can change the implementation of `#bundle` to compute the bundle on its own.
181+
182+
This would be desirable so that the build system can inform Foundation about the bundle it creates on disk. Foundation's `#bundle` macro can ingest that information at build time, to produce code that loads the bundle in the current context.
183+
184+
`Bundle.module` can't be fully removed without breaking existing code, though it could be generated as deprecated and/or gated behind a build setting.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
import SwiftSyntaxMacros
15+
16+
public struct BundleMacro: SwiftSyntaxMacros.ExpressionMacro, Sendable {
17+
public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
18+
"""
19+
{
20+
#if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE
21+
return Bundle.module
22+
#elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE
23+
#error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.")
24+
#else
25+
return Bundle(_dsoHandle: #dsohandle) ?? .main
26+
#endif
27+
}()
28+
"""
29+
}
30+
}

Sources/FoundationMacros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ target_compile_options(FoundationMacros PRIVATE -parse-as-library)
6363

6464
target_sources(FoundationMacros PRIVATE
6565
FoundationMacros.swift
66+
BundleMacro.swift
6667
PredicateMacro.swift)
6768

6869
target_compile_options(FoundationMacros PRIVATE

Sources/FoundationMacros/FoundationMacros.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import SwiftCompilerPlugin
1717

1818
@main
1919
struct FoundationMacros: CompilerPlugin {
20-
var providingMacros: [Macro.Type] = [PredicateMacro.self, ExpressionMacro.self]
20+
var providingMacros: [Macro.Type] = [
21+
PredicateMacro.self,
22+
ExpressionMacro.self,
23+
BundleMacro.self
24+
]
2125
}
2226

2327
#endif
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2022-2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import XCTest
14+
import FoundationMacros
15+
16+
final class BundleMacroTests: XCTestCase {
17+
18+
func testSimple() {
19+
AssertMacroExpansion(
20+
macros: ["bundle": BundleMacro.self],
21+
"""
22+
#bundle
23+
""",
24+
"""
25+
{
26+
#if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE
27+
return Bundle.module
28+
#elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE
29+
#error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.")
30+
#else
31+
return Bundle(_dsoHandle: #dsohandle) ?? .main
32+
#endif
33+
}()
34+
"""
35+
)
36+
}
37+
38+
func testUsingParenthesis() {
39+
AssertMacroExpansion(
40+
macros: ["bundle": BundleMacro.self],
41+
"""
42+
#bundle()
43+
""",
44+
"""
45+
{
46+
#if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE
47+
return Bundle.module
48+
#elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE
49+
#error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.")
50+
#else
51+
return Bundle(_dsoHandle: #dsohandle) ?? .main
52+
#endif
53+
}()
54+
"""
55+
)
56+
}
57+
}

0 commit comments

Comments
 (0)