mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-22 21:40:04 +08:00
Currently, we do not cache the current Swift locale if we cannot fetch proper preferences. This allows us to update the cached value if subsequent access succeeds. We should be doing that for `currentNSLocale` too, but we are not. Fix the logic so that the behaviors match. Resolves rdar://142699797
363 lines
13 KiB
Swift
363 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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: [Locale.Components : 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
|
|
|
|
func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? {
|
|
cachedFixedComponentsLocales[comps]
|
|
}
|
|
|
|
mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol {
|
|
if let l = fixedComponents(comps) {
|
|
return l
|
|
} else {
|
|
let new = _localeICUClass().init(components: comps)
|
|
|
|
cachedFixedComponentsLocales[comps] = 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
|
|
}
|
|
}
|