forked from swiftlang/swift-foundation
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLocale_Cache.swift
More file actions
359 lines (299 loc) · 13.3 KB
/
Locale_Cache.swift
File metadata and controls
359 lines (299 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if FOUNDATION_FRAMEWORK
internal import _ForSwiftFoundation
import CoreFoundation
internal import CoreFoundation_Private.CFNotificationCenter
internal import os
#endif
internal import _FoundationCShims
#if FOUNDATION_FRAMEWORK && canImport(_FoundationICU)
// Here, we always have access to _LocaleICU
internal func _localeICUClass() -> _LocaleProtocol.Type {
_LocaleICU.self
}
#else
dynamic package func _localeICUClass() -> _LocaleProtocol.Type {
// Return _LocaleUnlocalized if FoundationInternationalization isn't loaded. The `Locale` initializers are not failable, so we just fall back to the unlocalized type when needed without failure.
_LocaleUnlocalized.self
}
#endif
/// Singleton which listens for notifications about preference changes for Locale and holds cached singletons.
struct LocaleCache : Sendable, ~Copyable {
// MARK: - State
struct State {
init() {
#if FOUNDATION_FRAMEWORK
// For Foundation.framework, we listen for system notifications about the system Locale changing from the Darwin notification center.
_CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfLocaleCurrentLocaleDidChange!.rawValue)
#endif
}
private var cachedFixedLocales: [String : any _LocaleProtocol] = [:]
private var cachedFixedComponentsLocales: [String /*ICU identifier*/: any _LocaleProtocol] = [:]
#if FOUNDATION_FRAMEWORK
private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:]
struct IdentifierAndPrefs : Hashable {
let identifier: String
let prefs: LocalePreferences?
}
private var cachedFixedLocaleToNSLocales: [IdentifierAndPrefs : _NSSwiftLocale] = [:]
#endif
mutating func fixed(_ id: String) -> any _LocaleProtocol {
// Note: Even if the currentLocale's identifier is the same, currentLocale may have preference overrides which are not reflected in the identifier itself.
if let locale = cachedFixedLocales[id] {
return locale
} else {
let locale = _localeICUClass().init(identifier: id, prefs: nil)
cachedFixedLocales[id] = locale
return locale
}
}
#if FOUNDATION_FRAMEWORK
mutating func fixedNSLocale(identifier id: String) -> _NSSwiftLocale {
if let locale = cachedFixedIdentifierToNSLocales[id] {
return locale
} else {
let inner = Locale(inner: fixed(id))
let locale = _NSSwiftLocale(inner)
// We have found ObjC clients that rely upon an immortal lifetime for these `Locale`s, so we do not clear this cache.
cachedFixedIdentifierToNSLocales[id] = locale
return locale
}
}
#if canImport(_FoundationICU)
mutating func fixedNSLocale(_ locale: _LocaleICU) -> _NSSwiftLocale {
let id = IdentifierAndPrefs(identifier: locale.identifier, prefs: locale.prefs)
if let locale = cachedFixedLocaleToNSLocales[id] {
return locale
} else {
let inner = Locale(inner: locale)
let nsLocale = _NSSwiftLocale(inner)
// We have found ObjC clients that rely upon an immortal lifetime for these `Locale`s, so we do not clear this cache.
cachedFixedLocaleToNSLocales[id] = nsLocale
return nsLocale
}
}
#endif
#endif // FOUNDATION_FRAMEWORK
mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol {
let identifier = comps.icuIdentifier
if let l = cachedFixedComponentsLocales[identifier] {
return l
} else {
let new = _localeICUClass().init(components: comps)
cachedFixedComponentsLocales[identifier] = new
return new
}
}
}
let lock: LockedState<State>
static let cache = LocaleCache()
private let _currentCache = LockedState<(any _LocaleProtocol)?>(initialState: nil)
#if FOUNDATION_FRAMEWORK
private var _currentNSCache = LockedState<_NSSwiftLocale?>(initialState: nil)
#endif
fileprivate init() {
lock = LockedState(initialState: State())
}
/// For testing of `autoupdatingCurrent` only. If you want to test `current`, create a custom `Locale` with the appropriate settings using `localeAsIfCurrent(name:overrides:disableBundleMatching:)` and use that instead.
/// This mutates global state of the current locale, so it is not safe to use in concurrent testing.
func resetCurrent(to preferences: LocalePreferences) {
// Disable bundle matching so we can emulate a non-English main bundle during test
let newLocale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: true)
_currentCache.withLock {
$0 = newLocale
}
#if FOUNDATION_FRAMEWORK
_currentNSCache.withLock { $0 = nil }
#endif
}
func reset() {
_currentCache.withLock { $0 = nil }
#if FOUNDATION_FRAMEWORK
_currentNSCache.withLock { $0 = nil }
#endif
}
var current: any _LocaleProtocol {
return _currentAndCache.locale
}
fileprivate var _currentAndCache: (locale: any _LocaleProtocol, doCache: Bool) {
if let result = _currentCache.withLock({ $0 }) {
return (result, true)
}
// We need to fetch prefs and try again
let (preferences, doCache) = preferences()
let locale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: false)
// It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
if doCache {
return _currentCache.withLock {
if let current = $0 {
// Someone beat us to setting it - use existing one
return (current, true)
} else {
$0 = locale
return (locale, true)
}
}
} else {
return (locale, false)
}
}
// MARK: Singletons
// This value is immutable, so we can share one instance for the whole process.
static let unlocalized = _LocaleUnlocalized(identifier: "en_001")
// This value is immutable, so we can share one instance for the whole process.
static let autoupdatingCurrent = _LocaleAutoupdating()
static let system : any _LocaleProtocol = {
_localeICUClass().init(identifier: "", prefs: nil)
}()
#if FOUNDATION_FRAMEWORK
static let autoupdatingCurrentNSLocale : _NSSwiftLocale = {
_NSSwiftLocale(Locale(inner: autoupdatingCurrent))
}()
static let systemNSLocale : _NSSwiftLocale = {
_NSSwiftLocale(Locale(inner: system))
}()
#endif
// MARK: -
func fixed(_ id: String) -> any _LocaleProtocol {
lock.withLock {
$0.fixed(id)
}
}
#if FOUNDATION_FRAMEWORK
func fixedNSLocale(identifier id: String) -> _NSSwiftLocale {
lock.withLock { $0.fixedNSLocale(identifier: id) }
}
#if canImport(_FoundationICU)
func fixedNSLocale(_ locale: _LocaleICU) -> _NSSwiftLocale {
lock.withLock { $0.fixedNSLocale(locale) }
}
#endif
func currentNSLocale() -> _NSSwiftLocale {
if let result = _currentNSCache.withLock({ $0 }) {
return result
}
// Create the current _NSSwiftLocale, based on the current Swift Locale.
// n.b. do not call just `current` here; instead, use `_currentAndCache`
// so that the caching status is honored
let (current, doCache) = _currentAndCache
let nsLocale = _NSSwiftLocale(Locale(inner: current))
if doCache {
return _currentNSCache.withLock {
if let current = $0 {
// Someone beat us to setting it, use that one
return current
} else {
$0 = nsLocale
return nsLocale
}
}
} else {
return nsLocale
}
}
#endif // FOUNDATION_FRAMEWORK
func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol {
lock.withLock { $0.fixedComponentsWithCache(comps) }
}
#if FOUNDATION_FRAMEWORK && !NO_CFPREFERENCES
func preferences() -> (LocalePreferences, Bool) {
// On Darwin, we check the current user preferences for Locale values
var wouldDeadlock: DarwinBoolean = false
let cfPrefs = __CFXPreferencesCopyCurrentApplicationStateWithDeadlockAvoidance(&wouldDeadlock).takeRetainedValue()
var prefs = LocalePreferences()
prefs.apply(cfPrefs)
if wouldDeadlock.boolValue {
// Don't cache a locale built with incomplete prefs
return (prefs, false)
} else {
return (prefs, true)
}
}
func preferredLanguages(forCurrentUser: Bool) -> [String] {
var languages: [String] = []
if forCurrentUser {
languages = CFPreferencesCopyValue("AppleLanguages" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? [String] ?? []
} else {
languages = CFPreferencesCopyAppValue("AppleLanguages" as CFString, kCFPreferencesCurrentApplication) as? [String] ?? []
}
return languages.compactMap {
Locale.canonicalLanguageIdentifier(from: $0)
}
}
func preferredLocale() -> String? {
guard let preferredLocaleID = CFPreferencesCopyAppValue("AppleLocale" as CFString, kCFPreferencesCurrentApplication) as? String else {
return nil
}
return preferredLocaleID
}
#else
func preferences() -> (LocalePreferences, Bool) {
var prefs = LocalePreferences()
prefs.locale = "en_001"
prefs.languages = ["en-001"]
return (prefs, true)
}
func preferredLanguages(forCurrentUser: Bool) -> [String] {
[Locale.canonicalLanguageIdentifier(from: "en-001")]
}
func preferredLocale() -> String? {
"en_001"
}
#endif
#if FOUNDATION_FRAMEWORK && !NO_CFPREFERENCES
/// This returns an instance of `Locale` that's set up exactly like it would be if the user changed the current locale to that identifier, set the preferences keys in the overrides dictionary, then called `current`.
func localeAsIfCurrent(name: String?, cfOverrides: CFDictionary? = nil, disableBundleMatching: Bool = false) -> Locale {
var (prefs, _) = preferences()
if let cfOverrides { prefs.apply(cfOverrides) }
let inner = _LocaleICU(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
return Locale(inner: inner)
}
#endif
/// This returns an instance of `Locale` that's set up exactly like it would be if the user changed the current locale to that identifier, set the preferences keys in the overrides dictionary, then called `current`.
func localeAsIfCurrent(name: String?, overrides: LocalePreferences? = nil, disableBundleMatching: Bool = false) -> Locale {
var (prefs, _) = preferences()
if let overrides { prefs.apply(overrides) }
let inner = _localeICUClass().init(name: name, prefs: prefs, disableBundleMatching: disableBundleMatching)
return Locale(inner: inner)
}
func localeWithPreferences(identifier: String, prefs: LocalePreferences?) -> Locale {
if let prefs {
let inner = _localeICUClass().init(identifier: identifier, prefs: prefs)
return Locale(inner: inner)
} else {
return Locale(inner: LocaleCache.cache.fixed(identifier))
}
}
func localeAsIfCurrentWithBundleLocalizations(_ availableLocalizations: [String], allowsMixedLocalizations: Bool) -> Locale? {
#if FOUNDATION_FRAMEWORK && canImport(_FoundationICU)
guard !allowsMixedLocalizations else {
let (prefs, _) = preferences()
let inner = _LocaleICU(name: nil, prefs: prefs, disableBundleMatching: true)
return Locale(inner: inner)
}
let preferredLanguages = preferredLanguages(forCurrentUser: false)
guard let preferredLocaleID = preferredLocale() else { return nil }
let canonicalizedLocalizations = availableLocalizations.compactMap { Locale.canonicalLanguageIdentifier(from: $0) }
let identifier = Locale.localeIdentifierForCanonicalizedLocalizations(canonicalizedLocalizations, preferredLanguages: preferredLanguages, preferredLocaleID: preferredLocaleID)
guard let identifier else {
return nil
}
let (prefs, _) = preferences()
let inner = _LocaleICU(identifier: identifier, prefs: prefs)
return Locale(inner: inner)
#else
// No way to canonicalize on this platform
return nil
#endif
}
}