Files
keyboard/Shared/KBLocalizationManager.m
2025-11-03 21:04:39 +08:00

181 lines
7.6 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// KBLocalizationManager.m
// 多语言管理实现
//
#import "KBLocalizationManager.h"
#import <Security/Security.h>
#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<NSString *> *)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<NSString *> *)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];
// 基于当前 TargetApp 或扩展)的主 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