语言逻辑处理
我的项目里有5个国家的语言,如果用户在app里手动切换了国家语言,只要不卸载app,用户在手机设置切换语言,app的语言不要变;如果app被删 除,重新安装,app语言要跟随手机设置的语言(如果语言对不上,app就显示英语) 用户没有在app里手动设置过国家语言,用户在手机设置界面切换国家,app要跟随手机设置的语言(如果语言对不上,app就显示英语)。
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
#import "KBKeyBoardMainView.h"
|
#import "KBKeyBoardMainView.h"
|
||||||
#import "KBKeyboardSubscriptionView.h"
|
#import "KBKeyboardSubscriptionView.h"
|
||||||
#import "KBLocalizationManager.h"
|
#import "KBLocalizationManager.h"
|
||||||
|
#import "KBSettingView.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
#import "KBSkinInstallBridge.h"
|
#import "KBSkinInstallBridge.h"
|
||||||
#import "KBSuggestionEngine.h"
|
#import "KBSuggestionEngine.h"
|
||||||
@@ -96,6 +97,19 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
}
|
}
|
||||||
[self kb_applyTheme];
|
[self kb_applyTheme];
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
// 语言变化时,重建键盘 UI(保证“App 语言=键盘语言”,并支持 App 内切换语言后键盘即时刷新)
|
||||||
|
self.kb_localizationObserverToken = [[NSNotificationCenter defaultCenter]
|
||||||
|
addObserverForName:KBLocalizationDidChangeNotification
|
||||||
|
object:nil
|
||||||
|
queue:[NSOperationQueue mainQueue]
|
||||||
|
usingBlock:^(__unused NSNotification *_Nonnull note) {
|
||||||
|
__strong typeof(weakSelf) self = weakSelf;
|
||||||
|
if (!self) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self kb_reloadUIForLocalizationChange];
|
||||||
|
}];
|
||||||
[self kb_applyTheme];
|
[self kb_applyTheme];
|
||||||
[self kb_registerDarwinSkinInstallObserver];
|
[self kb_registerDarwinSkinInstallObserver];
|
||||||
[self kb_consumePendingShopSkin];
|
[self kb_consumePendingShopSkin];
|
||||||
@@ -239,6 +253,11 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
removeObserver:self.kb_skinObserverToken];
|
removeObserver:self.kb_skinObserverToken];
|
||||||
self.kb_skinObserverToken = nil;
|
self.kb_skinObserverToken = nil;
|
||||||
}
|
}
|
||||||
|
if (self.kb_localizationObserverToken) {
|
||||||
|
[[NSNotificationCenter defaultCenter]
|
||||||
|
removeObserver:self.kb_localizationObserverToken];
|
||||||
|
self.kb_localizationObserverToken = nil;
|
||||||
|
}
|
||||||
[self kb_stopObservingAppGroupChanges];
|
[self kb_stopObservingAppGroupChanges];
|
||||||
[self kb_unregisterDarwinSkinInstallObserver];
|
[self kb_unregisterDarwinSkinInstallObserver];
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -250,6 +269,57 @@ static NSString *KBFormatMB(uint64_t bytes) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Localization
|
||||||
|
|
||||||
|
- (void)kb_reloadUIForLocalizationChange {
|
||||||
|
if (![NSThread isMainThread]) {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf kb_reloadUIForLocalizationChange];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录当前面板状态,重建后尽量恢复。
|
||||||
|
KBKeyboardPanelMode targetMode = self.kb_panelMode;
|
||||||
|
// 强制下次布局刷新:即使 profileId 未变,也需要让新建的主视图应用一次当前 profile。
|
||||||
|
_kb_lastLoadedProfileId = nil;
|
||||||
|
|
||||||
|
// 主键盘/面板里有大量静态文案(init 时设置),语言变化后需要重建才能刷新。
|
||||||
|
if (_keyBoardMainView) {
|
||||||
|
[_keyBoardMainView removeFromSuperview];
|
||||||
|
_keyBoardMainView = nil;
|
||||||
|
}
|
||||||
|
self.keyBoardMainHeightConstraint = nil;
|
||||||
|
|
||||||
|
if (_functionView) {
|
||||||
|
[_functionView removeFromSuperview];
|
||||||
|
_functionView = nil;
|
||||||
|
}
|
||||||
|
if (_settingView) {
|
||||||
|
[_settingView removeFromSuperview];
|
||||||
|
_settingView = nil;
|
||||||
|
}
|
||||||
|
if (_subscriptionView) {
|
||||||
|
[_subscriptionView removeFromSuperview];
|
||||||
|
_subscriptionView = nil;
|
||||||
|
}
|
||||||
|
if (_chatPanelView) {
|
||||||
|
[_chatPanelView removeFromSuperview];
|
||||||
|
_chatPanelView = nil;
|
||||||
|
}
|
||||||
|
self.chatPanelVisible = NO;
|
||||||
|
self.chatPanelHeightConstraint = nil;
|
||||||
|
|
||||||
|
// 强制触发面板刷新:先回到 Main,再切回目标面板(避免 kb_setPanelMode 直接 return)。
|
||||||
|
self.kb_panelMode = KBKeyboardPanelModeMain;
|
||||||
|
[self kb_setPanelMode:targetMode animated:NO];
|
||||||
|
// 语言变化后,键盘布局/profile 也可能需要同步更新(未手动选择键盘配置时会随 App 语言变化)
|
||||||
|
[self kb_checkAndApplyLayoutIfNeeded];
|
||||||
|
[KBHUD setContainerView:self.view];
|
||||||
|
[self kb_applyTheme];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Layout Switching
|
#pragma mark - Layout Switching
|
||||||
|
|
||||||
- (void)kb_checkAndApplyLayoutIfNeeded {
|
- (void)kb_checkAndApplyLayoutIfNeeded {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
NSString *_chatPanelBaselineText;
|
NSString *_chatPanelBaselineText;
|
||||||
id _kb_fullAccessObserverToken;
|
id _kb_fullAccessObserverToken;
|
||||||
id _kb_skinObserverToken;
|
id _kb_skinObserverToken;
|
||||||
|
id _kb_localizationObserverToken;
|
||||||
KBKeyboardPanelMode _kb_panelMode;
|
KBKeyboardPanelMode _kb_panelMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ typedef NS_ENUM(NSInteger, KBKeyboardPanelMode) {
|
|||||||
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
|
@property(nonatomic, copy) NSString *chatPanelBaselineText; // 打开聊天面板时宿主输入框已有的文本
|
||||||
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
|
@property(nonatomic, strong, nullable) id kb_fullAccessObserverToken;
|
||||||
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
||||||
|
@property(nonatomic, strong, nullable) id kb_localizationObserverToken;
|
||||||
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
|
@property(nonatomic, assign) KBKeyboardPanelMode kb_panelMode;
|
||||||
@property(nonatomic, strong, nullable) id kb_appGroupObserverToken;
|
@property(nonatomic, strong, nullable) id kb_appGroupObserverToken;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#import "KBKeyboardLayoutResolver.h"
|
#import "KBKeyboardLayoutResolver.h"
|
||||||
#import "KBInputProfileManager.h"
|
#import "KBInputProfileManager.h"
|
||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
|
|
||||||
@implementation KBKeyboardLayoutResolver
|
@implementation KBKeyboardLayoutResolver
|
||||||
|
|
||||||
@@ -18,6 +19,30 @@
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 未手动选择键盘输入配置时,根据当前 App 语言推导默认键盘语言码(对应 kb_input_profiles.json 的 code)。
|
||||||
|
- (NSString *)kb_defaultKeyboardLanguageCodeForAppLanguageCode:(NSString *)appLanguageCode {
|
||||||
|
NSString *lc = (appLanguageCode ?: @"").lowercaseString;
|
||||||
|
if ([lc hasPrefix:@"es"]) { return @"es"; }
|
||||||
|
if ([lc hasPrefix:@"pt"]) { return @"pt"; }
|
||||||
|
if ([lc hasPrefix:@"id"]) { return @"id"; }
|
||||||
|
if ([lc hasPrefix:@"zh-hant"]) { return @"zh-Hant-Pinyin"; }
|
||||||
|
return @"en";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)kb_didUserSelectKeyboardProfileInAppGroup:(NSUserDefaults *)appGroup {
|
||||||
|
return [appGroup boolForKey:AppGroup_DidUserSelectKeyboardProfile];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable KBInputProfileLayout *)kb_defaultLayoutForCurrentAppLanguage {
|
||||||
|
NSString *appLang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
|
NSString *kbLang = [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLang];
|
||||||
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:kbLang];
|
||||||
|
if (!profile) {
|
||||||
|
profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:@"en"];
|
||||||
|
}
|
||||||
|
return profile.layouts.firstObject;
|
||||||
|
}
|
||||||
|
|
||||||
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId {
|
- (NSString *)layoutJsonIdForProfileId:(NSString *)profileId {
|
||||||
if (profileId.length == 0) {
|
if (profileId.length == 0) {
|
||||||
return @"letters";
|
return @"letters";
|
||||||
@@ -51,19 +76,31 @@
|
|||||||
- (nullable NSString *)currentProfileId {
|
- (nullable NSString *)currentProfileId {
|
||||||
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
NSString *profileId = [appGroup stringForKey:AppGroup_SelectedKeyboardProfileId];
|
NSString *profileId = [appGroup stringForKey:AppGroup_SelectedKeyboardProfileId];
|
||||||
return profileId;
|
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
|
||||||
|
return profileId;
|
||||||
|
}
|
||||||
|
KBInputProfileLayout *layout = [self kb_defaultLayoutForCurrentAppLanguage];
|
||||||
|
return layout.profileId.length > 0 ? layout.profileId : profileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable NSString *)currentLanguageCode {
|
- (nullable NSString *)currentLanguageCode {
|
||||||
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
NSString *languageCode = [appGroup stringForKey:AppGroup_SelectedKeyboardLanguageCode];
|
NSString *languageCode = [appGroup stringForKey:AppGroup_SelectedKeyboardLanguageCode];
|
||||||
return languageCode;
|
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
|
||||||
|
return languageCode;
|
||||||
|
}
|
||||||
|
NSString *appLang = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
|
return [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLang];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable NSString *)currentLayoutVariant {
|
- (nullable NSString *)currentLayoutVariant {
|
||||||
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
NSUserDefaults *appGroup = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
NSString *layoutVariant = [appGroup stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
|
NSString *layoutVariant = [appGroup stringForKey:AppGroup_SelectedKeyboardLayoutVariant];
|
||||||
return layoutVariant;
|
if ([self kb_didUserSelectKeyboardProfileInAppGroup:appGroup]) {
|
||||||
|
return layoutVariant;
|
||||||
|
}
|
||||||
|
KBInputProfileLayout *layout = [self kb_defaultLayoutForCurrentAppLanguage];
|
||||||
|
return layout.variant.length > 0 ? layout.variant : layoutVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -843,7 +843,7 @@
|
|||||||
"align": "left",
|
"align": "left",
|
||||||
"insetLeft": 4,
|
"insetLeft": 4,
|
||||||
"insetRight": 4,
|
"insetRight": 4,
|
||||||
"gap": 5,
|
"gap": 6,
|
||||||
"items": [
|
"items": [
|
||||||
"letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ",
|
"letter:ㄅ", "letter:ㄉ", "letter:ˇ", "letter:ˋ", "letter:ㄓ",
|
||||||
"letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"
|
"letter:ˊ", "letter:˙", "letter:ㄚ", "letter:ㄞ", "letter:ㄢ", "letter:ㄦ"
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
#define AppGroup_SelectedKeyboardLanguageCode @"AppGroup_SelectedKeyboardLanguageCode"
|
#define AppGroup_SelectedKeyboardLanguageCode @"AppGroup_SelectedKeyboardLanguageCode"
|
||||||
#define AppGroup_SelectedKeyboardLayoutVariant @"AppGroup_SelectedKeyboardLayoutVariant"
|
#define AppGroup_SelectedKeyboardLayoutVariant @"AppGroup_SelectedKeyboardLayoutVariant"
|
||||||
|
|
||||||
|
/// 是否用户手动选择过键盘输入配置(语言/布局/profile)。
|
||||||
|
/// YES:主 App 不再自动把键盘配置跟随 App 语言变更(避免覆盖用户手选)。
|
||||||
|
/// NO :主 App 会在“App 语言变化且未手动选择键盘配置”时同步更新键盘配置,使键盘语言/布局与 App 一致。
|
||||||
|
#define AppGroup_DidUserSelectKeyboardProfile @"AppGroup_DidUserSelectKeyboardProfile"
|
||||||
|
|
||||||
/// Darwin 跨进程通知:键盘扩展发送聊天消息后通知主 App 刷新
|
/// Darwin 跨进程通知:键盘扩展发送聊天消息后通知主 App 刷新
|
||||||
#define kKBDarwinChatUpdated @"com.loveKey.nyx.chat.updated"
|
#define kKBDarwinChatUpdated @"com.loveKey.nyx.chat.updated"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
#import <Security/Security.h>
|
#import <Security/Security.h>
|
||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
|
|
||||||
|
// Darwin 跨进程通知:语言变更(App <-> 扩展)
|
||||||
|
static NSString * const kKBDarwinLocalizationChanged = @"com.loveKey.nyx.loc.changed";
|
||||||
|
|
||||||
/// 语言常量定义
|
/// 语言常量定义
|
||||||
KBLanguageCode const KBLanguageCodeEnglish = @"en";
|
KBLanguageCode const KBLanguageCodeEnglish = @"en";
|
||||||
KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant";
|
KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant";
|
||||||
@@ -37,9 +40,20 @@ NSString * const KBLocalizationDidChangeNotification = @"KBLocalizationDidChange
|
|||||||
static NSString * const kKBLocService = @"com.loveKey.nyx.loc";
|
static NSString * const kKBLocService = @"com.loveKey.nyx.loc";
|
||||||
static NSString * const kKBLocAccount = @"lang"; // 保存 UTF8 的语言代码
|
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 ()
|
@interface KBLocalizationManager ()
|
||||||
@property (nonatomic, copy, readwrite) KBLanguageCode currentLanguageCode; // 当前语言代码
|
@property (nonatomic, copy, readwrite) KBLanguageCode currentLanguageCode; // 当前语言代码
|
||||||
@property (nonatomic, strong) NSBundle *langBundle; // 对应语言的 .lproj 资源包
|
@property (nonatomic, strong) NSBundle *langBundle; // 对应语言的 .lproj 资源包
|
||||||
|
|
||||||
|
- (NSString *)normalizeLanguageCode:(NSString *)code;
|
||||||
|
- (NSString *)supportedLanguageCodeForCandidate:(nullable NSString *)code;
|
||||||
|
- (void)kb_onSystemLocaleMaybeChanged:(NSNotification *)note;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// 避免 +shared 初始化阶段递归触发自身:
|
// 避免 +shared 初始化阶段递归触发自身:
|
||||||
@@ -54,19 +68,141 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
return q;
|
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
|
@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 {
|
+ (instancetype)shared {
|
||||||
static KBLocalizationManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
|
static KBLocalizationManager *m; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
|
||||||
m = [KBLocalizationManager new];
|
m = [KBLocalizationManager new];
|
||||||
// 默认支持语言;可在启动时由外部重置
|
// 默认支持语言;可在启动时由外部重置
|
||||||
m.supportedLanguageCodes = KBDefaultSupportedLanguageCodes();
|
m.supportedLanguageCodes = KBDefaultSupportedLanguageCodes();
|
||||||
// 启动读取:先取共享钥匙串,再按系统偏好回退
|
|
||||||
NSString *saved = [[self class] kc_read];
|
BOOL isAppExtension = [[self class] kb_isRunningInAppExtension];
|
||||||
if (saved.length == 0) {
|
BOOL wasAppBootstrapped = [[self class] kb_isAppBootstrapped];
|
||||||
saved = [m bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
BOOL isAppBootstrapped = wasAppBootstrapped;
|
||||||
|
if (!isAppExtension) {
|
||||||
|
// 仅主 App 写入该标记;扩展只读取
|
||||||
|
[[self class] kb_markAppBootstrappedIfNeeded];
|
||||||
|
isAppBootstrapped = YES;
|
||||||
}
|
}
|
||||||
[m applyLanguage:saved];
|
// 新安装判定(优先使用 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;
|
return m;
|
||||||
}
|
}
|
||||||
@@ -90,15 +226,37 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
|
|
||||||
- (void)setCurrentLanguageCode:(NSString *)code persist:(BOOL)persist {
|
- (void)setCurrentLanguageCode:(NSString *)code persist:(BOOL)persist {
|
||||||
if (code.length == 0) return; // 忽略空值
|
if (code.length == 0) return; // 忽略空值
|
||||||
if ([code isEqualToString:self.currentLanguageCode]) return; // 无变更
|
// 仅允许切到“受支持语言”;不支持(如 zh-Hans)时按最佳匹配回退(默认 en)
|
||||||
[self applyLanguage:code];
|
NSString *resolved = [self supportedLanguageCodeForCandidate:code];
|
||||||
if (persist) { [[self class] kc_write:self.currentLanguageCode]; } // 需同步到 App/扩展
|
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];
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)resetToSystemLanguage {
|
- (void)resetToSystemLanguage {
|
||||||
|
[[self class] kb_setUserDidSelectLanguage:NO];
|
||||||
|
[[self class] kc_write:nil];
|
||||||
NSString *best = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
NSString *best = [self bestSupportedLanguageForPreferred:[NSLocale preferredLanguages]] ?: KBLanguageCodeEnglish;
|
||||||
[self setCurrentLanguageCode:best persist:NO];
|
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 {
|
- (NSString *)localizedStringForKey:(NSString *)key {
|
||||||
@@ -116,15 +274,25 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
|
|
||||||
- (NSString *)bestSupportedLanguageForPreferred:(NSArray<NSString *> *)preferred {
|
- (NSString *)bestSupportedLanguageForPreferred:(NSArray<NSString *> *)preferred {
|
||||||
if (self.supportedLanguageCodes.count == 0) return KBLanguageCodeEnglish;
|
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) 完全匹配
|
// 1) 完全匹配
|
||||||
for (NSString *p in preferred) {
|
for (NSString *p in primaryOnly) {
|
||||||
NSString *pLC = p.lowercaseString;
|
NSString *pLC = p.lowercaseString;
|
||||||
for (NSString *s in self.supportedLanguageCodes) {
|
for (NSString *s in self.supportedLanguageCodes) {
|
||||||
if ([pLC isEqualToString:s.lowercaseString]) { return s; }
|
if ([pLC isEqualToString:s.lowercaseString]) { return s; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2) 前缀匹配:如 en-GB -> en, es-MX -> es
|
// 2) 前缀匹配:如 en-GB -> en, es-MX -> es
|
||||||
for (NSString *p in preferred) {
|
for (NSString *p in primaryOnly) {
|
||||||
NSString *pLC = p.lowercaseString;
|
NSString *pLC = p.lowercaseString;
|
||||||
for (NSString *s in self.supportedLanguageCodes) {
|
for (NSString *s in self.supportedLanguageCodes) {
|
||||||
NSString *sLC = s.lowercaseString;
|
NSString *sLC = s.lowercaseString;
|
||||||
@@ -138,7 +306,7 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2.5) 特殊处理葡语:pt-BR / pt-PT / pt -> pt-PT(若受支持)
|
// 2.5) 特殊处理葡语:pt-BR / pt-PT / pt -> pt-PT(若受支持)
|
||||||
for (NSString *p in preferred) {
|
for (NSString *p in primaryOnly) {
|
||||||
NSString *pLC = p.lowercaseString;
|
NSString *pLC = p.lowercaseString;
|
||||||
if ([pLC isEqualToString:@"pt"] || [pLC hasPrefix:@"pt-"] || [pLC hasPrefix:@"pt_"]) {
|
if ([pLC isEqualToString:@"pt"] || [pLC hasPrefix:@"pt-"] || [pLC hasPrefix:@"pt_"]) {
|
||||||
for (NSString *s in self.supportedLanguageCodes) {
|
for (NSString *s in self.supportedLanguageCodes) {
|
||||||
@@ -147,7 +315,7 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 3) 特殊处理中文:将 zh-Hant/zh-TW/zh-HK 映射到 zh-Hant(若受支持)
|
// 3) 特殊处理中文:将 zh-Hant/zh-TW/zh-HK 映射到 zh-Hant(若受支持)
|
||||||
for (NSString *p in preferred) {
|
for (NSString *p in primaryOnly) {
|
||||||
NSString *pLC = p.lowercaseString;
|
NSString *pLC = p.lowercaseString;
|
||||||
if ([pLC hasPrefix:@"zh-hant"] || [pLC hasPrefix:@"zh-tw"] || [pLC hasPrefix:@"zh-hk"]) {
|
if ([pLC hasPrefix:@"zh-hant"] || [pLC hasPrefix:@"zh-tw"] || [pLC hasPrefix:@"zh-hk"]) {
|
||||||
for (NSString *s in self.supportedLanguageCodes) {
|
for (NSString *s in self.supportedLanguageCodes) {
|
||||||
@@ -155,7 +323,10 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 4) 兜底:取第一个受支持语言
|
// 4) 兜底:若英文受支持则固定回退英文,否则取第一个
|
||||||
|
for (NSString *s in self.supportedLanguageCodes) {
|
||||||
|
if ([s.lowercaseString isEqualToString:@"en"]) { return s; }
|
||||||
|
}
|
||||||
return self.supportedLanguageCodes.firstObject ?: KBLanguageCodeEnglish;
|
return self.supportedLanguageCodes.firstObject ?: KBLanguageCodeEnglish;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +336,8 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
|
|
||||||
#pragma mark - 内部实现
|
#pragma mark - 内部实现
|
||||||
|
|
||||||
- (void)applyLanguage:(NSString *)code {
|
- (NSString *)normalizeLanguageCode:(NSString *)code {
|
||||||
|
if (code.length == 0) return @"";
|
||||||
NSString *normalizedCode = code;
|
NSString *normalizedCode = code;
|
||||||
NSString *lower = normalizedCode.lowercaseString;
|
NSString *lower = normalizedCode.lowercaseString;
|
||||||
if ([lower isEqualToString:@"pt"] || [lower hasPrefix:@"pt-"] || [lower hasPrefix:@"pt_"]) {
|
if ([lower isEqualToString:@"pt"] || [lower hasPrefix:@"pt-"] || [lower hasPrefix:@"pt_"]) {
|
||||||
@@ -175,6 +347,22 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
if ([lower hasPrefix:@"zh-hant"] || [lower hasPrefix:@"zh_hant"] || [lower hasPrefix:@"zh-tw"] || [lower hasPrefix:@"zh_hk"]) {
|
if ([lower hasPrefix:@"zh-hant"] || [lower hasPrefix:@"zh_hant"] || [lower hasPrefix:@"zh-tw"] || [lower hasPrefix:@"zh_hk"]) {
|
||||||
normalizedCode = KBLanguageCodeTraditionalChinese;
|
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];
|
_currentLanguageCode = [normalizedCode copy];
|
||||||
// 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源
|
// 基于当前 Target(App 或扩展)的主 bundle 加载 .lproj 资源
|
||||||
NSString *path = [NSBundle.mainBundle pathForResource:normalizedCode ofType:@"lproj"];
|
NSString *path = [NSBundle.mainBundle pathForResource:normalizedCode ofType:@"lproj"];
|
||||||
@@ -194,6 +382,16 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
|
|
||||||
#pragma mark - 钥匙串读写(App/扩展共享)
|
#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 {
|
+ (BOOL)kc_write:(NSString *)lang {
|
||||||
NSMutableDictionary *q = KBLocBaseKCQuery();
|
NSMutableDictionary *q = KBLocBaseKCQuery();
|
||||||
SecItemDelete((__bridge CFDictionaryRef)q);
|
SecItemDelete((__bridge CFDictionaryRef)q);
|
||||||
@@ -217,14 +415,60 @@ static inline NSMutableDictionary *KBLocBaseKCQuery(void) {
|
|||||||
return lang;
|
return lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)reloadFromSharedStorageIfNeeded {
|
- (void)kb_onSystemLocaleMaybeChanged:(NSNotification *)note {
|
||||||
NSString *saved = [[self class] kc_read];
|
// 只有未手动选择语言时才需要跟随系统变化;手动选择情况下 reload 会优先读取保存值,不会被系统变化影响。
|
||||||
if (saved.length == 0) return;
|
[self reloadFromSharedStorageIfNeeded];
|
||||||
if ([saved isEqualToString:self.currentLanguageCode]) return;
|
}
|
||||||
|
|
||||||
[self applyLanguage:saved];
|
- (void)reloadFromSharedStorageIfNeeded {
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBLocalizationDidChangeNotification
|
BOOL userSelected = [[self class] kb_userDidSelectLanguage];
|
||||||
object:nil];
|
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
|
@end
|
||||||
|
|||||||
@@ -1022,6 +1022,18 @@ static NSString * const kKBSkinMetadataThemeKey = @"theme_json";
|
|||||||
return themeOK;
|
return themeOK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目标默认皮肤不存在:如果当前仍是“默认类皮肤”,为避免 key_icons 仍是旧语言导致看起来“还是繁体”,回退到无图标的默认主题。
|
||||||
|
// 等主 App 后续安装/应用对应语言的默认皮肤后,会通过 KBSkinManager 的 Darwin 通知同步到扩展。
|
||||||
|
if (!hasTargetSkin && isDefaultLike &&
|
||||||
|
currentSkinId.length > 0 &&
|
||||||
|
![currentSkinId isEqualToString:@"default"] &&
|
||||||
|
[currentSkinId hasPrefix:@"bundle_skin_default_"] &&
|
||||||
|
![currentSkinId isEqualToString:targetSkinId]) {
|
||||||
|
NSLog(@"[SkinBridge] reloadCurrentSkinIconMap: missing target skin, fallback to default theme (clear old icon map)");
|
||||||
|
[[KBSkinManager shared] resetToDefault];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果目标皮肤不存在,尝试更新当前皮肤的图标映射
|
// 如果目标皮肤不存在,尝试更新当前皮肤的图标映射
|
||||||
if (currentSkinId.length == 0 || [currentSkinId isEqualToString:@"default"]) {
|
if (currentSkinId.length == 0 || [currentSkinId isEqualToString:@"default"]) {
|
||||||
NSLog(@"[SkinBridge] reloadCurrentSkinIconMap: no custom skin applied, skip");
|
NSLog(@"[SkinBridge] reloadCurrentSkinIconMap: no custom skin applied, skip");
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
#import "KBUserSessionManager.h"
|
#import "KBUserSessionManager.h"
|
||||||
#import "KBLoginVC.h"
|
#import "KBLoginVC.h"
|
||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
|
#import "KBLocalizationManager.h"
|
||||||
#import "KBSkinInstallBridge.h"
|
#import "KBSkinInstallBridge.h"
|
||||||
|
#import "KBSkinManager.h"
|
||||||
#import "KBAppUpdateView.h"
|
#import "KBAppUpdateView.h"
|
||||||
|
#import "KBInputProfileManager.h"
|
||||||
|
|
||||||
static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
||||||
|
|
||||||
@@ -46,6 +49,10 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
|||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||||
/// 1:配置国际化(统一使用集中管理的语言列表)
|
/// 1:配置国际化(统一使用集中管理的语言列表)
|
||||||
[KBLocalizationManager shared].supportedLanguageCodes = KBDefaultSupportedLanguageCodes();
|
[KBLocalizationManager shared].supportedLanguageCodes = KBDefaultSupportedLanguageCodes();
|
||||||
|
/// 1.1:首次启动为键盘写入默认输入配置(语言/布局/profile),保证“App 语言=键盘语言”
|
||||||
|
[self kb_bootstrapDefaultKeyboardProfileIfNeeded];
|
||||||
|
/// 1.2:未手动选择键盘输入配置时,让键盘配置随 App 语言变化
|
||||||
|
[self kb_syncKeyboardProfileToCurrentAppLanguageIfNeeded];
|
||||||
/// 2 : 处理token问题(包括卸载重装场景下的 token 清理)
|
/// 2 : 处理token问题(包括卸载重装场景下的 token 清理)
|
||||||
[[KBUserSessionManager shared] bootstrapIfNeeded];
|
[[KBUserSessionManager shared] bootstrapIfNeeded];
|
||||||
|
|
||||||
@@ -74,6 +81,12 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
|||||||
// 安装默认皮肤(首次安装时)
|
// 安装默认皮肤(首次安装时)
|
||||||
[self kb_installDefaultSkinIfNeeded];
|
[self kb_installDefaultSkinIfNeeded];
|
||||||
|
|
||||||
|
// App 语言变化(系统跟随或手动切换)时,同步键盘配置并让键盘即时刷新
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(kb_onLocalizationChanged:)
|
||||||
|
name:KBLocalizationDidChangeNotification
|
||||||
|
object:nil];
|
||||||
|
|
||||||
[self kb_requestAppUpdateAndPresentIfNeeded];
|
[self kb_requestAppUpdateAndPresentIfNeeded];
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
@@ -87,6 +100,119 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
|||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)kb_defaultKeyboardLanguageCodeForAppLanguageCode:(NSString *)appLanguageCode {
|
||||||
|
NSString *lc = appLanguageCode.lowercaseString ?: @"";
|
||||||
|
if ([lc hasPrefix:@"es"]) { return @"es"; }
|
||||||
|
if ([lc hasPrefix:@"pt"]) { return @"pt"; }
|
||||||
|
if ([lc hasPrefix:@"id"]) { return @"id"; }
|
||||||
|
if ([lc hasPrefix:@"zh-hant"]) { return @"zh-Hant-Pinyin"; }
|
||||||
|
return @"en";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_bootstrapDefaultKeyboardProfileIfNeeded {
|
||||||
|
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
NSString *savedProfileId = [shared stringForKey:AppGroup_SelectedKeyboardProfileId];
|
||||||
|
if (savedProfileId.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *appLanguageCode = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
|
NSString *kbLanguageCode = [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLanguageCode];
|
||||||
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:kbLanguageCode];
|
||||||
|
if (!profile) {
|
||||||
|
profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:@"en"];
|
||||||
|
}
|
||||||
|
KBInputProfileLayout *layout = profile.layouts.firstObject;
|
||||||
|
if (!layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[shared setObject:(profile.code ?: @"en") forKey:AppGroup_SelectedKeyboardLanguageCode];
|
||||||
|
[shared setObject:(layout.variant ?: @"qwerty") forKey:AppGroup_SelectedKeyboardLayoutVariant];
|
||||||
|
[shared setObject:(layout.profileId ?: @"en_US_qwerty") forKey:AppGroup_SelectedKeyboardProfileId];
|
||||||
|
[shared synchronize];
|
||||||
|
|
||||||
|
NSLog(@"[AppDelegate] Bootstrap keyboard profile: appLang=%@ kbLang=%@ profileId=%@ variant=%@",
|
||||||
|
appLanguageCode, profile.code, layout.profileId, layout.variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_syncKeyboardProfileToCurrentAppLanguageIfNeeded {
|
||||||
|
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
if ([shared boolForKey:AppGroup_DidUserSelectKeyboardProfile]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSString *appLanguageCode = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
|
NSString *kbLanguageCode = [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLanguageCode];
|
||||||
|
|
||||||
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:kbLanguageCode];
|
||||||
|
if (!profile) {
|
||||||
|
profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:@"en"];
|
||||||
|
}
|
||||||
|
KBInputProfileLayout *layout = profile.layouts.firstObject;
|
||||||
|
if (!layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *curProfileId = [shared stringForKey:AppGroup_SelectedKeyboardProfileId] ?: @"";
|
||||||
|
NSString *curLang = [shared stringForKey:AppGroup_SelectedKeyboardLanguageCode] ?: @"";
|
||||||
|
NSString *curVariant = [shared stringForKey:AppGroup_SelectedKeyboardLayoutVariant] ?: @"";
|
||||||
|
if ([curProfileId isEqualToString:layout.profileId ?: @""] &&
|
||||||
|
[curLang isEqualToString:profile.code ?: @""] &&
|
||||||
|
[curVariant isEqualToString:layout.variant ?: @""]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[shared setObject:(profile.code ?: @"en") forKey:AppGroup_SelectedKeyboardLanguageCode];
|
||||||
|
[shared setObject:(layout.variant ?: @"qwerty") forKey:AppGroup_SelectedKeyboardLayoutVariant];
|
||||||
|
[shared setObject:(layout.profileId ?: @"en_US_qwerty") forKey:AppGroup_SelectedKeyboardProfileId];
|
||||||
|
[shared synchronize];
|
||||||
|
|
||||||
|
NSLog(@"[AppDelegate] Sync keyboard profile to appLang=%@ kbLang=%@ profileId=%@ variant=%@",
|
||||||
|
appLanguageCode, profile.code, layout.profileId, layout.variant);
|
||||||
|
|
||||||
|
// 同步完键盘语言后,如当前是默认皮肤,则让皮肤也跟随(避免 key_icons 仍是旧语言导致看起来“还是繁体”)
|
||||||
|
[self kb_applyDefaultSkinForKeyboardLanguageIfNeeded:profile.code ?: @"en"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_onLocalizationChanged:(NSNotification *)note {
|
||||||
|
[self kb_syncKeyboardProfileToCurrentAppLanguageIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)kb_applyDefaultSkinForKeyboardLanguageIfNeeded:(NSString *)languageCode {
|
||||||
|
NSString *lc = languageCode.length > 0 ? languageCode : @"en";
|
||||||
|
NSString *targetSkinId = [NSString stringWithFormat:@"bundle_skin_default_%@", lc];
|
||||||
|
NSString *currentSkinId = [KBSkinManager shared].current.skinId ?: @"";
|
||||||
|
BOOL isDefaultLike = (currentSkinId.length == 0 ||
|
||||||
|
[currentSkinId isEqualToString:@"default"] ||
|
||||||
|
[currentSkinId hasPrefix:@"bundle_skin_default_"]);
|
||||||
|
if (!isDefaultLike) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ([currentSkinId isEqualToString:targetSkinId] && [KBSkinManager kb_hasAssetsForSkinId:targetSkinId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KBInputProfile *profile = [[KBInputProfileManager sharedManager] profileForLanguageCode:lc];
|
||||||
|
NSString *zipName = profile.defaultSkinZip.length > 0 ? profile.defaultSkinZip : @"normal_them.zip";
|
||||||
|
NSDictionary<NSString *, NSString *> *iconShortNames = [KBSkinInstallBridge iconShortNamesForLanguageCode:lc];
|
||||||
|
|
||||||
|
NSLog(@"[AppDelegate] Applying default skin for keyboard language=%@ currentSkin=%@ targetSkin=%@ zip=%@",
|
||||||
|
lc, currentSkinId, targetSkinId, zipName);
|
||||||
|
|
||||||
|
[KBSkinInstallBridge publishBundleSkinRequestWithId:targetSkinId
|
||||||
|
name:targetSkinId
|
||||||
|
zipName:zipName
|
||||||
|
iconShortNames:iconShortNames];
|
||||||
|
[KBSkinInstallBridge consumePendingRequestFromBundle:[NSBundle mainBundle]
|
||||||
|
completion:^(BOOL success, NSError * _Nullable error) {
|
||||||
|
if (!success) {
|
||||||
|
NSLog(@"[AppDelegate] Apply default skin failed: %@", error);
|
||||||
|
} else {
|
||||||
|
NSLog(@"[AppDelegate] Apply default skin success: %@", targetSkinId);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)kb_requestAppUpdateAndPresentIfNeeded {
|
- (void)kb_requestAppUpdateAndPresentIfNeeded {
|
||||||
__weak typeof(self) weakSelf = self;
|
__weak typeof(self) weakSelf = self;
|
||||||
[[KBLoginVM shared] checkAppUpdateWithCompletion:^(KBAppUpdateInfo * _Nullable info, NSError * _Nullable error) {
|
[[KBLoginVM shared] checkAppUpdateWithCompletion:^(KBAppUpdateInfo * _Nullable info, NSError * _Nullable error) {
|
||||||
@@ -545,36 +671,14 @@ static NSTimeInterval const kKBSubscriptionPrefillTTL = 10 * 60.0;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)kb_installDefaultSkinIfNeeded {
|
- (void)kb_installDefaultSkinIfNeeded {
|
||||||
static NSString *const kKBDefaultSkinInstalledKey = @"KBDefaultSkinInstalled";
|
NSLog(@"[AppDelegate] Installing/applying default skin if needed...");
|
||||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:AppGroup];
|
||||||
|
NSString *languageCode = [shared stringForKey:AppGroup_SelectedKeyboardLanguageCode];
|
||||||
if ([ud boolForKey:kKBDefaultSkinInstalledKey]) {
|
if (languageCode.length == 0) {
|
||||||
NSLog(@"[AppDelegate] Default skin already installed, skip");
|
NSString *appLanguageCode = [KBLocalizationManager shared].currentLanguageCode ?: KBLanguageCodeEnglish;
|
||||||
return;
|
languageCode = [self kb_defaultKeyboardLanguageCodeForAppLanguageCode:appLanguageCode];
|
||||||
}
|
}
|
||||||
|
[self kb_applyDefaultSkinForKeyboardLanguageIfNeeded:languageCode ?: @"en"];
|
||||||
NSLog(@"[AppDelegate] Installing default skin for first launch...");
|
|
||||||
|
|
||||||
NSString *skinId = @"bundle_skin_default_en";
|
|
||||||
NSString *zipName = @"normal_them.zip";
|
|
||||||
|
|
||||||
NSDictionary<NSString *, NSString *> *iconShortNames = [KBSkinInstallBridge iconShortNamesForLanguageCode:@"en"];
|
|
||||||
|
|
||||||
[KBSkinInstallBridge publishBundleSkinRequestWithId:skinId
|
|
||||||
name:skinId
|
|
||||||
zipName:zipName
|
|
||||||
iconShortNames:iconShortNames];
|
|
||||||
|
|
||||||
[KBSkinInstallBridge consumePendingRequestFromBundle:[NSBundle mainBundle]
|
|
||||||
completion:^(BOOL success, NSError * _Nullable error) {
|
|
||||||
if (success) {
|
|
||||||
NSLog(@"[AppDelegate] Default skin installed successfully");
|
|
||||||
[ud setBool:YES forKey:kKBDefaultSkinInstalledKey];
|
|
||||||
[ud synchronize];
|
|
||||||
} else {
|
|
||||||
NSLog(@"[AppDelegate] Default skin install failed: %@", error);
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -630,6 +630,7 @@ typedef void(^KBInputProfileSelectHandler)(NSString *languageCode, NSString *lay
|
|||||||
[shared setObject:languageCode forKey:AppGroup_SelectedKeyboardLanguageCode];
|
[shared setObject:languageCode forKey:AppGroup_SelectedKeyboardLanguageCode];
|
||||||
[shared setObject:layoutVariant forKey:AppGroup_SelectedKeyboardLayoutVariant];
|
[shared setObject:layoutVariant forKey:AppGroup_SelectedKeyboardLayoutVariant];
|
||||||
[shared setObject:profileId forKey:AppGroup_SelectedKeyboardProfileId];
|
[shared setObject:profileId forKey:AppGroup_SelectedKeyboardProfileId];
|
||||||
|
[shared setBool:YES forKey:AppGroup_DidUserSelectKeyboardProfile];
|
||||||
[shared synchronize];
|
[shared synchronize];
|
||||||
|
|
||||||
[[KBLocalizationManager shared] setCurrentLanguageCode:languageCode persist:YES];
|
[[KBLocalizationManager shared] setCurrentLanguageCode:languageCode persist:YES];
|
||||||
|
|||||||
Reference in New Issue
Block a user