File: Locale_Cache.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (453 lines) | stat: -rw-r--r-- 17,874 bytes parent folder | download
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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//===----------------------------------------------------------------------===//
//
// 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 {
    // MARK: - State
    
    struct State {
        private var cachedCurrentLocale: (any _LocaleProtocol)!
        private var cachedSystemLocale: (any _LocaleProtocol)!
        private var cachedFixedLocales: [String : any _LocaleProtocol] = [:]
        private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:]

#if FOUNDATION_FRAMEWORK
        private var cachedCurrentNSLocale: _NSSwiftLocale!
        private var cachedAutoupdatingNSLocale: _NSSwiftLocale!
        private var cachedSystemNSLocale: _NSSwiftLocale!
        private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:]
        
        struct IdentifierAndPrefs : Hashable {
            let identifier: String
            let prefs: LocalePreferences?
        }
        
        private var cachedFixedLocaleToNSLocales: [IdentifierAndPrefs : _NSSwiftLocale] = [:]
#endif

        private var cachedAutoupdatingLocale: _LocaleAutoupdating!
        
        private var noteCount = -1
        private var wasResetManually = false

        /// Clears the cached `Locale` values, if they need to be recalculated.
        mutating func resetCurrentIfNeeded() {
#if FOUNDATION_FRAMEWORK
            let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount())
#else
            let newNoteCount = 1
#endif

            if newNoteCount != noteCount || wasResetManually {
                cachedCurrentLocale = nil
                noteCount = newNoteCount
                wasResetManually = false

#if FOUNDATION_FRAMEWORK
                cachedCurrentNSLocale = nil
                // For Foundation.framework, we listen for system notifications about the system Locale changing from the Darwin notification center.
                _CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfLocaleCurrentLocaleDidChange!.rawValue)
#endif
            }
        }

        /// Get or create the current locale.
        /// `disableBundleMatching` should normally be disabled (`false`). The only reason to turn it on (`true`) is if we are attempting to create a testing scenario that does not use the main bundle's languages.
        mutating func current(preferences: LocalePreferences?, cache: Bool, disableBundleMatching: Bool) -> (any _LocaleProtocol)? {
            resetCurrentIfNeeded()

            if let cachedCurrentLocale {
                return cachedCurrentLocale
            }
            
            // At this point we know we need to create, or re-create, the Locale instance.
            // If we do not have a set of preferences to use, we have to return nil.
            guard let preferences else {
                return nil
            }

            let locale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: disableBundleMatching)
            if cache {
                // It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
                self.cachedCurrentLocale = locale
            }

            return locale
        }
        
        mutating func autoupdatingCurrent() -> _LocaleAutoupdating {
            if let cached = cachedAutoupdatingLocale {
                return cached
            } else {
                cachedAutoupdatingLocale = _LocaleAutoupdating()
                return cachedAutoupdatingLocale
            }
        }

        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

        mutating func currentNSLocale(preferences: LocalePreferences?, cache: Bool) -> _NSSwiftLocale? {
            resetCurrentIfNeeded()

            if let currentNSLocale = cachedCurrentNSLocale {
                return currentNSLocale
            } else if let current = cachedCurrentLocale {
                // We have a cached Swift Locale but not an NSLocale, yet
                let nsLocale = _NSSwiftLocale(Locale(inner: current))
                cachedCurrentNSLocale = nsLocale
                return nsLocale
            }
            
            // At this point we know we need to create, or re-create, the Locale instance.
            
            // If we do not have a set of preferences to use, we have to return nil.
            guard let preferences else {
                return nil
            }

#if canImport(_FoundationICU)
            // We have neither a Swift Locale nor an NSLocale. Recalculate and set both.
            let locale = _LocaleICU(name: nil, prefs: preferences, disableBundleMatching: false)
#else
            let locale = _LocaleUnlocalized(name: nil, prefs: preferences, disableBundleMatching: false)
#endif
            let nsLocale = _NSSwiftLocale(Locale(inner: locale))
            
            if cache {
                // It's possible this was an 'incomplete locale', in which case we will want to calculate it again later.
                self.cachedCurrentLocale = locale
                cachedCurrentNSLocale = nsLocale
            }

            return nsLocale
        }

        mutating func autoupdatingNSLocale() -> _NSSwiftLocale {
            if let result = cachedAutoupdatingNSLocale {
                return result
            }

            // Don't call Locale.autoupdatingCurrent directly to avoid a recursive lock
            cachedAutoupdatingNSLocale = _NSSwiftLocale(Locale(inner: autoupdatingCurrent()))
            return cachedAutoupdatingNSLocale
        }

        mutating func systemNSLocale() -> _NSSwiftLocale {
            if let result = cachedSystemNSLocale {
                return result
            }

            let inner = Locale(inner: system())
            cachedSystemNSLocale = _NSSwiftLocale(inner)
            return cachedSystemNSLocale
        }
#endif // FOUNDATION_FRAMEWORK

        mutating func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol {
            if let l = cachedFixedComponentsLocales[comps] {
                return l
            } else {
                let new = _localeICUClass().init(components: comps)
                
                cachedFixedComponentsLocales[comps] = new
                return new
            }
        }

        mutating func system() -> any _LocaleProtocol {
            if let locale = cachedSystemLocale {
                return locale
            }

            let locale = _localeICUClass().init(identifier: "", prefs: nil)
            cachedSystemLocale = locale
            return locale
        }

        mutating func reset() {
            wasResetManually = true
        }
    }

    let lock: LockedState<State>

    static let cache = LocaleCache()

    fileprivate init() {
        lock = LockedState(initialState: State())
    }

    func reset() {
        lock.withLock { $0.reset() }
    }

    /// 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) {
        lock.withLock {
            $0.reset()
            // Disable bundle matching so we can emulate a non-English main bundle during test
            let _ = $0.current(preferences: preferences, cache: true, disableBundleMatching: true)
        }
    }

    var current: any _LocaleProtocol {
        var result = lock.withLock {
            $0.current(preferences: nil, cache: false, disableBundleMatching: false)
        }
        
        if let result { return result }
        
        // We need to fetch prefs and try again
        let (prefs, doCache) = preferences()
        
        result = lock.withLock {
            $0.current(preferences: prefs, cache: doCache, disableBundleMatching: false)
        }
        
        guard let result else {
            fatalError("Nil result getting current Locale with preferences")
        }
        
        return result
    }
    
    /// This value is immutable, so we can share one instance for the whole process.
    private static let _unlocalizedCache = _LocaleUnlocalized(identifier: "en_001")
    var unlocalized: _LocaleUnlocalized {
        Self._unlocalizedCache
    }
    
    var autoupdatingCurrent: _LocaleAutoupdating {
        lock.withLock { $0.autoupdatingCurrent() }
    }

    var system: any _LocaleProtocol {
        lock.withLock { $0.system() }
    }

    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 autoupdatingCurrentNSLocale() -> _NSSwiftLocale {
        lock.withLock { $0.autoupdatingNSLocale() }
    }

    func currentNSLocale() -> _NSSwiftLocale {
        var result = lock.withLock {
            $0.currentNSLocale(preferences: nil, cache: false)
        }
        
        if let result { return result }
        
        // We need to fetch prefs and try again. Don't do this inside a lock (106190030). On Darwin it is possible to get a KVO callout from fetching the preferences, which could ask for the current Locale, which could cause a reentrant lock.
        let (prefs, doCache) = preferences()
        
        result = lock.withLock {
            $0.currentNSLocale(preferences: prefs, cache: doCache)
        }
        
        guard let result else {
            fatalError("Nil result getting current NSLocale with preferences")
        }
        
        return result
    }

    func systemNSLocale() -> _NSSwiftLocale {
        lock.withLock { $0.systemNSLocale() }
    }
#endif // FOUNDATION_FRAMEWORK

    func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol {
        lock.withLock { $0.fixedComponents(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
    }
}