Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

103 lines
3.3 KiB
Swift

import Foundation
/// Resolves localization resources without depending on a process-wide language list.
///
/// The app, dynamic plugins, and Finder Sync are separate targets, so this helper is
/// compiled into each of them instead of introducing a target dependency.
enum LocalizedBundleResolver {
static func localizedBundles(
in bundle: Bundle,
preferredLanguages: [String],
baseLanguage: String
) -> [Bundle] {
let key = CacheKey(
bundleURL: bundle.bundleURL,
preferredLanguages: preferredLanguages,
baseLanguage: baseLanguage
)
return cache.bundles(for: key) {
var bundles: [Bundle] = []
for language in preferredLanguages + [baseLanguage] {
for candidate in candidateLanguageIdentifiers(for: language) {
guard let path = bundle.path(forResource: candidate, ofType: "lproj"),
let localizedBundle = Bundle(path: path)
else {
continue
}
if !bundles.contains(where: { $0.bundleURL == localizedBundle.bundleURL }) {
bundles.append(localizedBundle)
}
break
}
}
return bundles
}
}
static func candidateLanguageIdentifiers(for language: String) -> [String] {
let normalized = language.replacingOccurrences(of: "_", with: "-")
var candidates = [normalized]
let components = normalized.split(separator: "-").map(String.init)
if let languageCode = components.first {
if languageCode == "zh" {
candidates.append(
components.contains(where: { ["Hant", "HK", "MO", "TW"].contains($0) })
? "zh-Hant"
: "zh-Hans"
)
}
candidates.append(languageCode)
}
return candidates.reduce(into: []) { result, candidate in
if !result.contains(candidate) {
result.append(candidate)
}
}
}
/// Keeps the user's region, calendar, and format settings while changing
/// only the language used by formatters.
static func locale(for language: String?, currentLocale: Locale = .current) -> Locale {
guard let language, !language.isEmpty else {
return currentLocale
}
let suffix = currentLocale.identifier.drop {
$0 != "_" && $0 != "@"
}
return Locale(identifier: language + suffix)
}
fileprivate struct CacheKey: Hashable {
let bundleURL: URL
let preferredLanguages: [String]
let baseLanguage: String
}
private static let cache = LocalizedBundleCache()
}
private final class LocalizedBundleCache: @unchecked Sendable {
private let lock = NSLock()
private var values: [LocalizedBundleResolver.CacheKey: [Bundle]] = [:]
func bundles(
for key: LocalizedBundleResolver.CacheKey,
build: () -> [Bundle]
) -> [Bundle] {
lock.withLock {
if let value = values[key] {
return value
}
let value = build()
values[key] = value
return value
}
}
}