我的项目里有5个国家的语言,如果用户在app里手动切换了国家语言,只要不卸载app,用户在手机设置切换语言,app的语言不要变;如果app被删 除,重新安装,app语言要跟随手机设置的语言(如果语言对不上,app就显示英语) 用户没有在app里手动设置过国家语言,用户在手机设置界面切换国家,app要跟随手机设置的语言(如果语言对不上,app就显示英语)。
475 lines
22 KiB
Objective-C
475 lines
22 KiB
Objective-C
//
|
||
// KBLocalizationManager.m
|
||
// 多语言管理实现
|
||
//
|
||
|
||
#import "KBLocalizationManager.h"
|
||
#import <Security/Security.h>
|
||
#import "KBConfig.h"
|
||
|
||
// Darwin 跨进程通知:语言变更(App <-> 扩展)
|
||
static NSString * const kKBDarwinLocalizationChanged = @"com.loveKey.nyx.loc.changed";
|
||
|
||
/// 语言常量定义
|
||
KBLanguageCode const KBLanguageCodeEnglish = @"en";
|
||
KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant";
|
||
KBLanguageCode const KBLanguageCodeSpanish = @"es";
|
||
KBLanguageCode const KBLanguageCodePortuguese = @"pt-PT";
|
||
KBLanguageCode const KBLanguageCodeIndonesian = @"id";
|
||
|
||
/// 默认支持语言列表(集中配置)
|
||
NSArray<KBLanguageCode> *KBDefaultSupportedLanguageCodes(void) {
|
||
static NSArray<KBLanguageCode> *codes;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
codes = @[
|
||
KBLanguageCodeEnglish,
|
||
KBLanguageCodeSpanish,
|
||
KBLanguageCodeIndonesian,
|
||
KBLanguageCodePortuguese,
|
||
KBLanguageCodeTraditionalChinese
|
||
];
|
||
});
|
||
return codes;
|
||
}
|
||
|
||
/// 语言变更通知名称
|
||
NSString * const KBLocalizationDidChangeNotification = @"KBLocalizationDidChangeNotification";
|
||
|
||
// 通过共享钥匙串跨 Target 持久化语言选择
|
||
static NSString * const kKBLocService = @"com.loveKey.nyx.loc";
|
||
static NSString * const kKBLocAccount = @"lang"; // 保存 UTF8 的语言代码
|
||
|
||
// 首次安装/首次启动标记(仅主 App 侧使用标准 UserDefaults;卸载后会被清除,用于抵消 Keychain 在卸载重装后仍保留的问题)
|
||
static NSString * const kKBLocDidLaunchKey = @"com.loveKey.nyx.loc.did_launch";
|
||
// 主 App 启动标记(存 App Group,键盘扩展可读):用于“扩展先启动”场景下避免读取卸载重装遗留的 Keychain 语言。
|
||
static NSString * const kKBLocAppBootstrappedKey = @"com.loveKey.nyx.loc.app_bootstrapped";
|
||
// 用户是否在 App 内手动选择过语言(存 App Group;卸载重装会清理,满足“重装后跟随系统”)
|
||
static NSString * const kKBLocUserSelectedLanguageKey = @"com.loveKey.nyx.loc.user_selected_language";
|
||
|
||
@interface KBLocalizationManager ()
|
||
@property (nonatomic, copy, readwrite) KBLanguageCode currentLanguageCode; // 当前语言代码
|
||
@property (nonatomic, strong) NSBundle *langBundle; // 对应语言的 .lproj 资源包
|
||
|
||
- (NSString *)normalizeLanguageCode:(NSString *)code;
|
||
- (NSString *)supportedLanguageCodeForCandidate:(nullable NSString *)code;
|
||
- (void)kb_onSystemLocaleMaybeChanged:(NSNotification *)note;
|
||
@end
|
||
|
||
// 避免 +shared 初始化阶段递归触发自身:
|
||
// 这里提供一个 C 级别的工具函数,构建钥匙串查询,不依赖实例或 +shared。
|
||
static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
||
NSMutableDictionary *q = [@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||
(__bridge id)kSecAttrService: kKBLocService,
|
||
(__bridge id)kSecAttrAccount: kKBLocAccount } mutableCopy];
|
||
if (KB_KEYCHAIN_ACCESS_GROUP.length > 0) {
|
||
q[(__bridge id)kSecAttrAccessGroup] = KB_KEYCHAIN_ACCESS_GROUP;
|
||
}
|
||
return q;
|
||
}
|
||
|
||
static void KBLocDarwinCallback(CFNotificationCenterRef center,
|
||
void *observer,
|
||
CFStringRef name,
|
||
const void *object,
|
||
CFDictionaryRef userInfo) {
|
||
// 语言更新后,扩展侧需要主动 reload;App 侧收到也无害(会被 resolved==current short-circuit)。
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
KBLocalizationManager *m = [KBLocalizationManager shared];
|
||
[m reloadFromSharedStorageIfNeeded];
|
||
});
|
||
}
|
||
|
||
@implementation KBLocalizationManager
|
||
|
||
/// 是否运行在 App Extension(如键盘扩展)里。
|
||
/// 说明:扩展与主 App 共享 Keychain,但不应参与“首次启动”判定,避免扩展抢先写入标记导致主 App 不再清理历史 Keychain。
|
||
+ (BOOL)kb_isRunningInAppExtension {
|
||
return ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSExtension"] != nil);
|
||
}
|
||
|
||
+ (NSUserDefaults *)kb_appGroupUserDefaults {
|
||
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||
return ud ?: [NSUserDefaults standardUserDefaults];
|
||
}
|
||
|
||
+ (BOOL)kb_isAppBootstrapped {
|
||
// App Group 在卸载重装后会被清理;扩展通过该标记判断“主 App 是否已在本次安装中启动过”。
|
||
return [[self kb_appGroupUserDefaults] boolForKey:kKBLocAppBootstrappedKey];
|
||
}
|
||
|
||
+ (void)kb_markAppBootstrappedIfNeeded {
|
||
NSUserDefaults *ud = [self kb_appGroupUserDefaults];
|
||
if ([ud boolForKey:kKBLocAppBootstrappedKey]) { return; }
|
||
[ud setBool:YES forKey:kKBLocAppBootstrappedKey];
|
||
[ud synchronize];
|
||
}
|
||
|
||
+ (BOOL)kb_userDidSelectLanguage {
|
||
NSUserDefaults *ud = [self kb_appGroupUserDefaults];
|
||
return [ud boolForKey:kKBLocUserSelectedLanguageKey];
|
||
}
|
||
|
||
+ (void)kb_setUserDidSelectLanguage:(BOOL)selected {
|
||
NSUserDefaults *ud = [self kb_appGroupUserDefaults];
|
||
[ud setBool:selected forKey:kKBLocUserSelectedLanguageKey];
|
||
[ud synchronize];
|
||
}
|
||
|
||
+ (instancetype)shared {
|
||
static KBLocalizationManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
|
||
m = [KBLocalizationManager new];
|
||
// 默认支持语言;可在启动时由外部重置
|
||
m.supportedLanguageCodes = KBDefaultSupportedLanguageCodes();
|
||
|
||
BOOL isAppExtension = [[self class] kb_isRunningInAppExtension];
|
||
BOOL wasAppBootstrapped = [[self class] kb_isAppBootstrapped];
|
||
BOOL isAppBootstrapped = wasAppBootstrapped;
|
||
if (!isAppExtension) {
|
||
// 仅主 App 写入该标记;扩展只读取
|
||
[[self class] kb_markAppBootstrappedIfNeeded];
|
||
isAppBootstrapped = YES;
|
||
}
|
||
// 新安装判定(优先使用 App Group 标记):卸载重装后 App Group 会被清理,但 Keychain 可能仍残留。
|
||
// 仅主 App 在检测到“本次安装从未启动过”时清理 Keychain,并写入系统择优结果,供扩展读取。
|
||
BOOL isFreshInstall = (!isAppExtension && !wasAppBootstrapped);
|
||
if (isFreshInstall) {
|
||
[[self class] kc_write:nil];
|
||
// 新安装:确保“未手动选择”状态(App Group 卸载重装会清理,这里再兜底一次)
|
||
[[self class] kb_setUserDidSelectLanguage:NO];
|
||
}
|
||
|
||
// 兼容旧逻辑:仍保留标准 UserDefaults 的首次启动标记(仅主 App),但不再作为关键判定条件。
|
||
BOOL isFirstLaunch = (!isAppExtension) ? [[self class] kb_markLaunchedAndReturnIsFirstLaunch] : NO;
|
||
|
||
// 语言选择策略(满足产品规则):
|
||
// 1) 用户在 App 内手动选过语言 -> 锁定(不跟随系统);通过 Keychain 跨 Target 同步
|
||
// 2) 用户未手动选择过 -> 跟随系统当前语言(首选列表第一个),不读取/不写入 Keychain
|
||
BOOL userSelected = [[self class] kb_userDidSelectLanguage];
|
||
NSString *saved = nil;
|
||
if (userSelected) {
|
||
// 扩展在“主 App 尚未启动过”的场景下,忽略 Keychain(防止卸载重装遗留旧值在扩展先启动时生效)
|
||
if (!(isAppExtension && !isAppBootstrapped)) {
|
||
saved = (isFreshInstall ? nil : [[self class] kc_read]);
|
||
}
|
||
} else {
|
||
// 未手动选择:清理可能遗留的 Keychain(例如旧版本曾写入“系统默认”导致后续不再跟随系统)
|
||
if (!isAppExtension) {
|
||
NSString *legacy = [[self class] kc_read];
|
||
if (legacy.length > 0) {
|
||
[[self class] kc_write:nil];
|
||
}
|
||
}
|
||
}
|
||
#if DEBUG
|
||
NSLog(@"[KBLoc] init bundle=%@ isExt=%d appBoot=%d wasBoot=%d fresh=%d firstLaunch=%d userSelected=%d preferred=%@ keychainSaved=%@ supported=%@",
|
||
NSBundle.mainBundle.bundleIdentifier ?: @"",
|
||
isAppExtension, isAppBootstrapped, wasAppBootstrapped, isFreshInstall, isFirstLaunch,
|
||
userSelected,
|
||
[NSLocale preferredLanguages],
|
||
saved ?: @"(nil)",
|
||
m.supportedLanguageCodes ?: @[]);
|
||
#endif
|
||
// 计算最终语言:优先手动选择;否则跟随系统当前语言(不在 5 种内则英文)
|
||
NSString *resolved = [m supportedLanguageCodeForCandidate:saved];
|
||
if (saved.length > 0 && resolved.length == 0) {
|
||
// 若保存了不支持的语言码(历史遗留),视为“未手动选择”,清理并回退系统
|
||
[[self class] kc_write:nil];
|
||
[[self class] kb_setUserDidSelectLanguage:NO];
|
||
saved = nil;
|
||
resolved = @"";
|
||
}
|
||
if (resolved.length == 0) {
|
||
resolved = [m bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
||
}
|
||
[m applyLanguage:resolved];
|
||
#if DEBUG
|
||
NSLog(@"[KBLoc] resolved=%@ current=%@ bundle=%@",
|
||
resolved ?: @"",
|
||
m.currentLanguageCode ?: @"",
|
||
m.langBundle.bundlePath ?: @"");
|
||
#endif
|
||
|
||
// 监听 Darwin 跨进程通知(语言变更),用于“App 内切语言 -> 立即刷新键盘”。
|
||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
|
||
(__bridge const void *)(m),
|
||
KBLocDarwinCallback,
|
||
(__bridge CFStringRef)kKBDarwinLocalizationChanged,
|
||
NULL,
|
||
CFNotificationSuspensionBehaviorDeliverImmediately);
|
||
|
||
// 系统语言/地区变化:如果未手动锁定语言,则跟随系统刷新
|
||
[[NSNotificationCenter defaultCenter] addObserver:m
|
||
selector:@selector(kb_onSystemLocaleMaybeChanged:)
|
||
name:NSCurrentLocaleDidChangeNotification
|
||
object:nil];
|
||
});
|
||
return m;
|
||
}
|
||
|
||
#pragma mark - 对外 API
|
||
|
||
- (void)setSupportedLanguageCodes:(NSArray<NSString *> *)supportedLanguageCodes {
|
||
// 归一化:去重、去空
|
||
NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet];
|
||
for (NSString *c in supportedLanguageCodes) {
|
||
if (c.length) { [set addObject:c]; }
|
||
}
|
||
_supportedLanguageCodes = set.array.count ? (NSArray<KBLanguageCode> *)set.array : @[ KBLanguageCodeEnglish ];
|
||
// 若当前语言不再受支持,则按最佳匹配切回(不持久化,仅内存),并广播变更
|
||
if (self.currentLanguageCode.length && ![set containsObject:self.currentLanguageCode]) {
|
||
NSString *best = [self bestSupportedLanguageForPreferred:@[self.currentLanguageCode]];
|
||
[self applyLanguage:best ?: _supportedLanguageCodes.firstObject];
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||
}
|
||
}
|
||
|
||
- (void)setCurrentLanguageCode:(NSString *)code persist:(BOOL)persist {
|
||
if (code.length == 0) return; // 忽略空值
|
||
// 仅允许切到“受支持语言”;不支持(如 zh-Hans)时按最佳匹配回退(默认 en)
|
||
NSString *resolved = [self supportedLanguageCodeForCandidate:code];
|
||
if (resolved.length == 0) {
|
||
resolved = [self bestSupportedLanguageForPreferred:@[code]] ?: (self.supportedLanguageCodes.firstObject ?: KBLanguageCodeEnglish);
|
||
}
|
||
if ([resolved isEqualToString:self.currentLanguageCode]) return; // 无变更
|
||
[self applyLanguage:resolved];
|
||
if (persist) {
|
||
[[self class] kb_setUserDidSelectLanguage:YES];
|
||
[[self class] kc_write:self.currentLanguageCode]; // 需同步到 App/扩展
|
||
// 通知对端进程刷新语言(特别是键盘扩展正在显示时)
|
||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
|
||
(__bridge CFStringRef)kKBDarwinLocalizationChanged,
|
||
NULL, NULL, true);
|
||
}
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||
}
|
||
|
||
- (void)resetToSystemLanguage {
|
||
[[self class] kb_setUserDidSelectLanguage:NO];
|
||
[[self class] kc_write:nil];
|
||
NSString *best = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
||
if ([best isEqualToString:self.currentLanguageCode]) {
|
||
return;
|
||
}
|
||
[self applyLanguage:best];
|
||
// 通知对端进程刷新语言(特别是键盘扩展正在显示时)
|
||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
|
||
(__bridge CFStringRef)kKBDarwinLocalizationChanged,
|
||
NULL, NULL, true);
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||
}
|
||
|
||
- (NSString *)localizedStringForKey:(NSString *)key {
|
||
return [self localizedStringForKey:key table:nil value:key];
|
||
}
|
||
|
||
- (NSString *)localizedStringForKey:(NSString *)key table:(NSString *)table value:(NSString *)value {
|
||
if (key.length == 0) return @"";
|
||
NSBundle *bundle = self.langBundle ?: NSBundle.mainBundle;
|
||
NSString *tbl = table ?: @"Localizable";
|
||
// 使用 bundle API,避免 NSLocalizedString 被系统语言钉死
|
||
NSString *str = [bundle localizedStringForKey:key value:value table:tbl];
|
||
return str ?: (value ?: key);
|
||
}
|
||
|
||
- (NSString *)bestSupportedLanguageForPreferred:(NSArray<NSString *> *)preferred {
|
||
if (self.supportedLanguageCodes.count == 0) return KBLanguageCodeEnglish;
|
||
// 需求:只使用“系统当前语言”(首选语言列表的第一个)。
|
||
// 若当前语言不在项目支持的 5 种语言内,则回退英文;不要从后续备选语言里再挑一个支持语言。
|
||
NSString *primary = preferred.firstObject ?: @"";
|
||
if (primary.length == 0) {
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([s.lowercaseString isEqualToString:@"en"]) { return s; }
|
||
}
|
||
return self.supportedLanguageCodes.firstObject ?: KBLanguageCodeEnglish;
|
||
}
|
||
NSArray<NSString *> *primaryOnly = @[ primary ];
|
||
// 1) 完全匹配
|
||
for (NSString *p in primaryOnly) {
|
||
NSString *pLC = p.lowercaseString;
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([pLC isEqualToString:s.lowercaseString]) { return s; }
|
||
}
|
||
}
|
||
// 2) 前缀匹配:如 en-GB -> en, es-MX -> es
|
||
for (NSString *p in primaryOnly) {
|
||
NSString *pLC = p.lowercaseString;
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
NSString *sLC = s.lowercaseString;
|
||
if ([pLC hasPrefix:[sLC stringByAppendingString:@"-"]] || [pLC hasPrefix:[sLC stringByAppendingString:@"_"]]) {
|
||
return s;
|
||
}
|
||
// also allow reverse: when supported is regional (rare)
|
||
if ([sLC hasPrefix:[pLC stringByAppendingString:@"-"]] || [sLC hasPrefix:[pLC stringByAppendingString:@"_"]]) {
|
||
return s;
|
||
}
|
||
}
|
||
}
|
||
// 2.5) 特殊处理葡语:pt-BR / pt-PT / pt -> pt-PT(若受支持)
|
||
for (NSString *p in primaryOnly) {
|
||
NSString *pLC = p.lowercaseString;
|
||
if ([pLC isEqualToString:@"pt"] || [pLC hasPrefix:@"pt-"] || [pLC hasPrefix:@"pt_"]) {
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([s.lowercaseString isEqualToString:@"pt-pt"]) { return s; }
|
||
}
|
||
}
|
||
}
|
||
// 3) 特殊处理中文:将 zh-Hant/zh-TW/zh-HK 映射到 zh-Hant(若受支持)
|
||
for (NSString *p in primaryOnly) {
|
||
NSString *pLC = p.lowercaseString;
|
||
if ([pLC hasPrefix:@"zh-hant"] || [pLC hasPrefix:@"zh-tw"] || [pLC hasPrefix:@"zh-hk"]) {
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([s.lowercaseString isEqualToString:@"zh-hant"]) { return s; }
|
||
}
|
||
}
|
||
}
|
||
// 4) 兜底:若英文受支持则固定回退英文,否则取第一个
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([s.lowercaseString isEqualToString:@"en"]) { return s; }
|
||
}
|
||
return self.supportedLanguageCodes.firstObject ?: KBLanguageCodeEnglish;
|
||
}
|
||
|
||
- (NSString *)currentLanguageHeaderValue {
|
||
return self.currentLanguageCode ?: KBLanguageCodeEnglish;
|
||
}
|
||
|
||
#pragma mark - 内部实现
|
||
|
||
- (NSString *)normalizeLanguageCode:(NSString *)code {
|
||
if (code.length == 0) return @"";
|
||
NSString *normalizedCode = code;
|
||
NSString *lower = normalizedCode.lowercaseString;
|
||
if ([lower isEqualToString:@"pt"] || [lower hasPrefix:@"pt-"] || [lower hasPrefix:@"pt_"]) {
|
||
normalizedCode = @"pt-PT";
|
||
}
|
||
lower = normalizedCode.lowercaseString;
|
||
if ([lower hasPrefix:@"zh-hant"] || [lower hasPrefix:@"zh_hant"] || [lower hasPrefix:@"zh-tw"] || [lower hasPrefix:@"zh_hk"]) {
|
||
normalizedCode = KBLanguageCodeTraditionalChinese;
|
||
}
|
||
return normalizedCode;
|
||
}
|
||
|
||
- (NSString *)supportedLanguageCodeForCandidate:(NSString *)code {
|
||
NSString *normalized = [self normalizeLanguageCode:code];
|
||
if (normalized.length == 0) return @"";
|
||
for (NSString *s in self.supportedLanguageCodes) {
|
||
if ([s.lowercaseString isEqualToString:normalized.lowercaseString]) {
|
||
return s; // 返回“工程内规范码”(保留大小写/连字符风格)
|
||
}
|
||
}
|
||
return @"";
|
||
}
|
||
|
||
- (void)applyLanguage:(NSString *)code {
|
||
NSString *normalizedCode = [self normalizeLanguageCode:code];
|
||
_currentLanguageCode = [normalizedCode copy];
|
||
// 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源
|
||
NSString *path = [NSBundle.mainBundle pathForResource:normalizedCode ofType:@"lproj"];
|
||
if (!path) {
|
||
// 尝试去区域后缀:如 en-GB -> en
|
||
NSString *shortCode = [[normalizedCode componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"-_"]] firstObject];
|
||
if (shortCode.length > 0) {
|
||
path = [NSBundle.mainBundle pathForResource:shortCode ofType:@"lproj"];
|
||
}
|
||
}
|
||
if (path) {
|
||
self.langBundle = [NSBundle bundleWithPath:path];
|
||
} else {
|
||
self.langBundle = NSBundle.mainBundle; // 兜底
|
||
}
|
||
}
|
||
|
||
#pragma mark - 钥匙串读写(App/扩展共享)
|
||
|
||
+ (BOOL)kb_markLaunchedAndReturnIsFirstLaunch {
|
||
// 注意:这里只能用主 App 自己的标准 UserDefaults。
|
||
// 不要用 App Group,否则键盘扩展可能会抢先写入“已启动”标记,导致主 App 无法在首次启动时清理历史 Keychain。
|
||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||
if ([ud boolForKey:kKBLocDidLaunchKey]) { return NO; }
|
||
[ud setBool:YES forKey:kKBLocDidLaunchKey];
|
||
[ud synchronize];
|
||
return YES;
|
||
}
|
||
|
||
+ (BOOL)kc_write:(NSString *)lang {
|
||
NSMutableDictionary *q = KBLocBaseKCQuery();
|
||
SecItemDelete((__bridge CFDictionaryRef)q);
|
||
if (lang.length == 0) return YES; // 等价于删除
|
||
NSData *data = [lang dataUsingEncoding:NSUTF8StringEncoding];
|
||
q[(__bridge id)kSecValueData] = data ?: [NSData data];
|
||
q[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||
OSStatus st = SecItemAdd((__bridge CFDictionaryRef)q, NULL);
|
||
return (st == errSecSuccess);
|
||
}
|
||
|
||
+ (NSString *)kc_read {
|
||
NSMutableDictionary *q = KBLocBaseKCQuery();
|
||
q[(__bridge id)kSecReturnData] = @YES;
|
||
q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||
CFTypeRef dataRef = NULL; OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &dataRef);
|
||
if (st != errSecSuccess || !dataRef) return nil;
|
||
NSData *data = (__bridge_transfer NSData *)dataRef;
|
||
if (data.length == 0) return nil;
|
||
NSString *lang = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||
return lang;
|
||
}
|
||
|
||
- (void)kb_onSystemLocaleMaybeChanged:(NSNotification *)note {
|
||
// 只有未手动选择语言时才需要跟随系统变化;手动选择情况下 reload 会优先读取保存值,不会被系统变化影响。
|
||
[self reloadFromSharedStorageIfNeeded];
|
||
}
|
||
|
||
- (void)reloadFromSharedStorageIfNeeded {
|
||
BOOL userSelected = [[self class] kb_userDidSelectLanguage];
|
||
BOOL isAppExtension = [[self class] kb_isRunningInAppExtension];
|
||
BOOL isAppBootstrapped = [[self class] kb_isAppBootstrapped];
|
||
|
||
NSString *target = @"";
|
||
if (userSelected) {
|
||
// 扩展在主 App 未启动前:不读取 Keychain(可能遗留旧值),保持当前按系统择优的结果。
|
||
if (isAppExtension && !isAppBootstrapped) {
|
||
#if DEBUG
|
||
NSLog(@"[KBLoc] reload skip (app not bootstrapped yet) bundle=%@ current=%@",
|
||
NSBundle.mainBundle.bundleIdentifier ?: @"",
|
||
self.currentLanguageCode ?: @"");
|
||
#endif
|
||
return;
|
||
}
|
||
NSString *saved = [[self class] kc_read];
|
||
if (saved.length == 0) {
|
||
// 标记为手动选择但无保存值:降级为“跟随系统”
|
||
[[self class] kb_setUserDidSelectLanguage:NO];
|
||
} else {
|
||
NSString *resolved = [self supportedLanguageCodeForCandidate:saved];
|
||
if (resolved.length == 0) {
|
||
// 保存了不支持的语言码(历史遗留):清理并降级为“跟随系统”
|
||
[[self class] kc_write:nil];
|
||
[[self class] kb_setUserDidSelectLanguage:NO];
|
||
} else {
|
||
target = resolved;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (target.length == 0) {
|
||
// 未手动选择:始终按系统当前语言(首选列表第一个)择优
|
||
target = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
||
}
|
||
|
||
if ([target isEqualToString:self.currentLanguageCode]) {
|
||
return;
|
||
}
|
||
|
||
[self applyLanguage:target];
|
||
// 系统跟随模式:主 App 语言变化时需要通知键盘扩展即时刷新(避免键盘仍停留旧语言)。
|
||
if (!userSelected && !isAppExtension) {
|
||
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
|
||
(__bridge CFStringRef)kKBDarwinLocalizationChanged,
|
||
NULL, NULL, true);
|
||
}
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||
}
|
||
|
||
@end
|