// // KBLocalizationManager.m // 多语言管理实现 // #import "KBLocalizationManager.h" #import #import "KBConfig.h" /// 语言变更通知名称 NSString * const KBLocalizationDidChangeNotification = @"KBLocalizationDidChangeNotification"; // 通过共享钥匙串跨 Target 持久化语言选择 static NSString * const kKBLocService = @"com.loveKey.nyx.loc"; static NSString * const kKBLocAccount = @"lang"; // 保存 UTF8 的语言代码 @interface KBLocalizationManager () @property (nonatomic, copy, readwrite) NSString *currentLanguageCode; // 当前语言代码 @property (nonatomic, strong) NSBundle *langBundle; // 对应语言的 .lproj 资源包 @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; } @implementation KBLocalizationManager + (instancetype)shared { static KBLocalizationManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ m = [KBLocalizationManager new]; // 默认支持语言;可在启动时由外部重置 m.supportedLanguageCodes = @[ @"en", @"zh-Hans" ]; // 启动读取:先取共享钥匙串,再按系统偏好回退 NSString *saved = [[self class] kc_read]; if (saved.length == 0) { saved = [m bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: @"en"; } [m applyLanguage:saved]; }); return m; } #pragma mark - 对外 API - (void)setSupportedLanguageCodes:(NSArray *)supportedLanguageCodes { // 归一化:去重、去空 NSMutableOrderedSet *set = [NSMutableOrderedSet orderedSet]; for (NSString *c in supportedLanguageCodes) { if (c.length) { [set addObject:c]; } } _supportedLanguageCodes = set.array.count ? set.array : @[ @"en" ]; // 若当前语言不再受支持,则按最佳匹配切回(不持久化,仅内存),并广播变更 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; // 忽略空值 if ([code isEqualToString:self.currentLanguageCode]) return; // 无变更 [self applyLanguage:code]; if (persist) { [[self class] kc_write:code]; } // 需同步到 App/扩展 [[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil]; } - (void)resetToSystemLanguage { NSString *best = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: @"en"; [self setCurrentLanguageCode:best persist:NO]; } - (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 *)preferred { if (self.supportedLanguageCodes.count == 0) return @"en"; // 1) 完全匹配 for (NSString *p in preferred) { NSString *pLC = p.lowercaseString; for (NSString *s in self.supportedLanguageCodes) { if ([pLC isEqualToString:s.lowercaseString]) { return s; } } } // 2) 前缀匹配:如 zh-Hans-CN -> zh-Hans, en-GB -> en for (NSString *p in preferred) { 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; } } } // 3) 特殊处理中文:将 zh-Hant/zh-TW/zh-HK 映射到 zh-Hant(若受支持) for (NSString *p in preferred) { 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; } } } if ([pLC hasPrefix:@"zh-hans"] || [pLC hasPrefix:@"zh-cn"]) { for (NSString *s in self.supportedLanguageCodes) { if ([s.lowercaseString isEqualToString:@"zh-hans"]) { return s; } } } } // 4) 兜底:取第一个受支持语言 return self.supportedLanguageCodes.firstObject ?: @"en"; } #pragma mark - 内部实现 - (void)applyLanguage:(NSString *)code { _currentLanguageCode = [code copy]; // 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源 NSString *path = [NSBundle.mainBundle pathForResource:code ofType:@"lproj"]; if (!path) { // 尝试去区域后缀:如 en-GB -> en NSString *shortCode = [[code 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)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; } @end