This commit is contained in:
2026-03-02 09:19:06 +08:00
parent da4649101e
commit 781e557e80
34 changed files with 3926 additions and 87 deletions

View File

@@ -33,6 +33,11 @@
/// 键盘扩展聊天更新的 companionId键盘写入主 App 读取后刷新对应聊天记录)
#define AppGroup_ChatUpdatedCompanionId @"AppGroup_ChatUpdatedCompanionId"
/// 当前选中的输入配置(主 App 写入,键盘扩展读取)
#define AppGroup_SelectedKeyboardProfileId @"AppGroup_SelectedKeyboardProfileId"
#define AppGroup_SelectedKeyboardLanguageCode @"AppGroup_SelectedKeyboardLanguageCode"
#define AppGroup_SelectedKeyboardLayoutVariant @"AppGroup_SelectedKeyboardLayoutVariant"
/// Darwin 跨进程通知:键盘扩展发送聊天消息后通知主 App 刷新
#define kKBDarwinChatUpdated @"com.loveKey.nyx.chat.updated"

View File

@@ -0,0 +1,48 @@
//
// KBInputProfileManager.h
// KeyBoard
//
// 管理输入配置(语言 + 布局)的统一配置中心
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBInputProfileLayout : NSObject
@property (nonatomic, copy) NSString *variant;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *profileId;
@property (nonatomic, copy) NSString *layoutJsonId;
@property (nonatomic, copy) NSString *suggestionEngine;
@end
@interface KBInputProfile : NSObject
@property (nonatomic, copy) NSString *code;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *defaultSkinZip;
@property (nonatomic, strong) NSArray<KBInputProfileLayout *> *layouts;
@end
@interface KBInputProfileManager : NSObject
+ (instancetype)sharedManager;
/// 获取所有支持的语言配置
- (NSArray<KBInputProfile *> *)allProfiles;
/// 根据语言代码获取配置
- (nullable KBInputProfile *)profileForLanguageCode:(NSString *)languageCode;
/// 根据 profileId 获取布局配置
- (nullable KBInputProfileLayout *)layoutForProfileId:(NSString *)profileId;
/// 根据 profileId 获取对应的 layoutJsonId
- (nullable NSString *)layoutJsonIdForProfileId:(NSString *)profileId;
/// 根据 profileId 获取对应的联想引擎类型
- (nullable NSString *)suggestionEngineForProfileId:(NSString *)profileId;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,194 @@
//
// KBInputProfileManager.m
// KeyBoard
//
#import "KBInputProfileManager.h"
@implementation KBInputProfileLayout
@end
@implementation KBInputProfile
@end
@interface KBInputProfileManager ()
@property (nonatomic, strong) NSArray<KBInputProfile *> *profiles;
@end
@implementation KBInputProfileManager
+ (instancetype)sharedManager {
static KBInputProfileManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
[self loadProfiles];
}
return self;
}
- (void)loadProfiles {
NSString *path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json"];
if (path.length == 0) {
path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json" inDirectory:@"Resource"];
}
if (path.length == 0) {
path = [[NSBundle mainBundle] pathForResource:@"kb_input_profiles" ofType:@"json" inDirectory:@"Shared/Resource"];
}
if (!path) {
NSLog(@"[KBInputProfileManager] kb_input_profiles.json not found");
self.profiles = [self defaultProfiles];
return;
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
NSLog(@"[KBInputProfileManager] Failed to read kb_input_profiles.json");
self.profiles = [self defaultProfiles];
return;
}
NSError *error = nil;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (error || ![json isKindOfClass:NSDictionary.class]) {
NSLog(@"[KBInputProfileManager] Failed to parse JSON: %@", error);
self.profiles = [self defaultProfiles];
return;
}
NSArray *profilesArray = json[@"profiles"];
if (![profilesArray isKindOfClass:NSArray.class]) {
NSLog(@"[KBInputProfileManager] Invalid profiles array");
self.profiles = [self defaultProfiles];
return;
}
self.profiles = [self parseProfilesFromJSONArray:profilesArray];
if (self.profiles.count == 0) {
NSLog(@"[KBInputProfileManager] Parsed profiles is empty, fallback to default");
self.profiles = [self defaultProfiles];
}
NSLog(@"[KBInputProfileManager] Loaded %lu profiles", (unsigned long)self.profiles.count);
}
- (NSArray<KBInputProfile *> *)parseProfilesFromJSONArray:(NSArray *)profilesArray {
NSMutableArray<KBInputProfile *> *result = [NSMutableArray array];
for (NSDictionary *profileDict in profilesArray) {
if (![profileDict isKindOfClass:NSDictionary.class]) { continue; }
KBInputProfile *profile = [[KBInputProfile alloc] init];
profile.code = [profileDict[@"code"] isKindOfClass:NSString.class] ? profileDict[@"code"] : @"";
profile.name = [profileDict[@"name"] isKindOfClass:NSString.class] ? profileDict[@"name"] : @"";
profile.defaultSkinZip = [profileDict[@"defaultSkinZip"] isKindOfClass:NSString.class] ? profileDict[@"defaultSkinZip"] : @"";
NSArray *layoutsArray = profileDict[@"layouts"];
NSMutableArray<KBInputProfileLayout *> *layouts = [NSMutableArray array];
if ([layoutsArray isKindOfClass:NSArray.class]) {
for (NSDictionary *layoutDict in layoutsArray) {
if (![layoutDict isKindOfClass:NSDictionary.class]) { continue; }
KBInputProfileLayout *layout = [[KBInputProfileLayout alloc] init];
layout.variant = [layoutDict[@"variant"] isKindOfClass:NSString.class] ? layoutDict[@"variant"] : @"";
layout.title = [layoutDict[@"title"] isKindOfClass:NSString.class] ? layoutDict[@"title"] : @"";
layout.profileId = [layoutDict[@"profileId"] isKindOfClass:NSString.class] ? layoutDict[@"profileId"] : @"";
layout.layoutJsonId = [layoutDict[@"layoutJsonId"] isKindOfClass:NSString.class] ? layoutDict[@"layoutJsonId"] : @"";
layout.suggestionEngine = [layoutDict[@"suggestionEngine"] isKindOfClass:NSString.class] ? layoutDict[@"suggestionEngine"] : @"";
[layouts addObject:layout];
}
}
profile.layouts = [layouts copy];
[result addObject:profile];
}
return [result copy];
}
- (NSArray<KBInputProfile *> *)defaultProfiles {
NSArray *fallback = @[
@{
@"code": @"en",
@"name": @"English",
@"defaultSkinZip": @"normal_them.zip",
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"en_US_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"}]
},
@{
@"code": @"es",
@"name": @"Español",
@"defaultSkinZip": @"",
@"layouts": @[
@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"es_ES_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"},
@{@"variant": @"azerty", @"title": @"AZERTY", @"profileId": @"es_ES_azerty", @"layoutJsonId": @"letters_azerty", @"suggestionEngine": @"latin"},
@{@"variant": @"qwertz", @"title": @"QWERTZ", @"profileId": @"es_ES_qwertz", @"layoutJsonId": @"letters_qwertz", @"suggestionEngine": @"latin"}
]
},
@{
@"code": @"pt",
@"name": @"Português",
@"defaultSkinZip": @"",
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"pt_PT_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"}]
},
@{
@"code": @"zh-Hant",
@"name": @"繁體中文(台灣)",
@"defaultSkinZip": @"",
@"layouts": @[
@{@"variant": @"pinyin", @"title": @"拼音(繁體)", @"profileId": @"zh_Hant_TW_pinyin", @"layoutJsonId": @"letters", @"suggestionEngine": @"pinyin_traditional"},
@{@"variant": @"bopomofo_full", @"title": @"注音全鍵盤", @"profileId": @"zh_Hant_TW_bopomofo_full", @"layoutJsonId": @"letters_bopomofo_full", @"suggestionEngine": @"bopomofo"},
@{@"variant": @"bopomofo_standard", @"title": @"注音標準", @"profileId": @"zh_Hant_TW_bopomofo_standard", @"layoutJsonId": @"letters_bopomofo_standard", @"suggestionEngine": @"bopomofo"}
]
},
@{
@"code": @"id",
@"name": @"Bahasa Indonesia",
@"defaultSkinZip": @"",
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"id_ID_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"latin"}]
},
@{
@"code": @"zh-Hans",
@"name": @"简体中文",
@"defaultSkinZip": @"",
@"layouts": @[@{@"variant": @"qwerty", @"title": @"QWERTY", @"profileId": @"zh_Hans_CN_qwerty", @"layoutJsonId": @"letters", @"suggestionEngine": @"pinyin_simplified"}]
}
];
return [self parseProfilesFromJSONArray:fallback];
}
- (NSArray<KBInputProfile *> *)allProfiles {
return self.profiles;
}
- (nullable KBInputProfile *)profileForLanguageCode:(NSString *)languageCode {
for (KBInputProfile *profile in self.profiles) {
if ([profile.code isEqualToString:languageCode]) {
return profile;
}
}
return nil;
}
- (nullable KBInputProfileLayout *)layoutForProfileId:(NSString *)profileId {
for (KBInputProfile *profile in self.profiles) {
for (KBInputProfileLayout *layout in profile.layouts) {
if ([layout.profileId isEqualToString:profileId]) {
return layout;
}
}
}
return nil;
}
- (nullable NSString *)layoutJsonIdForProfileId:(NSString *)profileId {
KBInputProfileLayout *layout = [self layoutForProfileId:profileId];
return layout.layoutJsonId;
}
- (nullable NSString *)suggestionEngineForProfileId:(NSString *)profileId {
KBInputProfileLayout *layout = [self layoutForProfileId:profileId];
return layout.suggestionEngine;
}
@end

View File

@@ -17,8 +17,12 @@ typedef NSString *KBLanguageCode NS_EXTENSIBLE_STRING_ENUM;
/// 项目内统一使用的语言常量
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeEnglish; // @"en"
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSimplifiedChinese; // @"zh-Hans"
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeTraditionalChinese; // @"zh-Hant"
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeSpanish; // @"es"
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodePortuguese; // @"pt"
FOUNDATION_EXPORT KBLanguageCode const KBLanguageCodeIndonesian; // @"id"
/// 默认支持的语言列表(目前为 @[KBLanguageCodeEnglish, KBLanguageCodeSimplifiedChinese]
/// 默认支持的语言列表(当前en / zh-Hans / zh-Hant / es / pt / id
FOUNDATION_EXPORT NSArray<KBLanguageCode> *KBDefaultSupportedLanguageCodes(void);
/// 当前语言变更通知(不附带 userInfo

View File

@@ -10,13 +10,24 @@
///
KBLanguageCode const KBLanguageCodeEnglish = @"en";
KBLanguageCode const KBLanguageCodeSimplifiedChinese = @"zh-Hans";
KBLanguageCode const KBLanguageCodeTraditionalChinese = @"zh-Hant";
KBLanguageCode const KBLanguageCodeSpanish = @"es";
KBLanguageCode const KBLanguageCodePortuguese = @"pt";
KBLanguageCode const KBLanguageCodeIndonesian = @"id";
///
NSArray<KBLanguageCode> *KBDefaultSupportedLanguageCodes(void) {
static NSArray<KBLanguageCode> *codes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
codes = @[KBLanguageCodeEnglish, KBLanguageCodeSimplifiedChinese];
codes = @[
KBLanguageCodeEnglish,
KBLanguageCodeSimplifiedChinese,
KBLanguageCodeTraditionalChinese,
KBLanguageCodeSpanish,
KBLanguageCodePortuguese,
KBLanguageCodeIndonesian
];
});
return codes;
}

View File

@@ -140,6 +140,12 @@
"Commit" = "Commit";
"Nickname" = "Nickname";
"Gender" = "Gender";
"Input Language" = "Input Language";
"Choose Layout" = "Choose Layout";
"Multiple Keyboard Layouts" = "Multiple Keyboard Layouts";
"This language has a default skin configured. It won't be auto-applied when switching language." = "This language has a default skin configured. It won't be auto-applied when switching language.";
"Please configure a default skin for this language before switching." = "Please configure a default skin for this language before switching.";
"Default skin install failed. Please check skin resource configuration." = "Default skin install failed. Please check skin resource configuration.";
"User ID" = "User ID";
"Modify Gender" = "Modify Gender";
"Male" = "Male";

View File

@@ -141,6 +141,12 @@
"Commit" = "提交";
"Nickname" = "用户名";
"Gender" = "性别";
"Input Language" = "输入语言";
"Choose Layout" = "选择键盘布局";
"Multiple Keyboard Layouts" = "多种键盘布局";
"This language has a default skin configured. It won't be auto-applied when switching language." = "该语言已配置默认皮肤,切换语言时不会自动应用。";
"Please configure a default skin for this language before switching." = "请先为该语言配置默认皮肤";
"Default skin install failed. Please check skin resource configuration." = "默认皮肤安装失败,请检查皮肤资源配置";
"User ID" = "用户ID";
"Modify Gender" = "修改性别";
"Male" = "男";

View File

@@ -0,0 +1,117 @@
{
"__comment": "输入配置文件:定义所有支持的语言和布局",
"profiles": [
{
"code": "en",
"name": "English",
"defaultSkinZip": "normal_them.zip",
"layouts": [
{
"variant": "qwerty",
"title": "QWERTY",
"profileId": "en_US_qwerty",
"layoutJsonId": "letters",
"suggestionEngine": "latin"
}
]
},
{
"code": "es",
"name": "Español",
"defaultSkinZip": "",
"layouts": [
{
"variant": "qwerty",
"title": "QWERTY",
"profileId": "es_ES_qwerty",
"layoutJsonId": "letters",
"suggestionEngine": "latin"
},
{
"variant": "azerty",
"title": "AZERTY",
"profileId": "es_ES_azerty",
"layoutJsonId": "letters_azerty",
"suggestionEngine": "latin"
},
{
"variant": "qwertz",
"title": "QWERTZ",
"profileId": "es_ES_qwertz",
"layoutJsonId": "letters_qwertz",
"suggestionEngine": "latin"
}
]
},
{
"code": "pt",
"name": "Português",
"defaultSkinZip": "",
"layouts": [
{
"variant": "qwerty",
"title": "QWERTY",
"profileId": "pt_PT_qwerty",
"layoutJsonId": "letters",
"suggestionEngine": "latin"
}
]
},
{
"code": "zh-Hant",
"name": "繁體中文(台灣)",
"defaultSkinZip": "",
"layouts": [
{
"variant": "pinyin",
"title": "拼音(繁體)",
"profileId": "zh_Hant_TW_pinyin",
"layoutJsonId": "letters",
"suggestionEngine": "pinyin_traditional"
},
{
"variant": "bopomofo_full",
"title": "注音全鍵盤",
"profileId": "zh_Hant_TW_bopomofo_full",
"layoutJsonId": "letters_bopomofo_full",
"suggestionEngine": "bopomofo"
},
{
"variant": "bopomofo_standard",
"title": "注音標準",
"profileId": "zh_Hant_TW_bopomofo_standard",
"layoutJsonId": "letters_bopomofo_standard",
"suggestionEngine": "bopomofo"
}
]
},
{
"code": "id",
"name": "Bahasa Indonesia",
"defaultSkinZip": "",
"layouts": [
{
"variant": "qwerty",
"title": "QWERTY",
"profileId": "id_ID_qwerty",
"layoutJsonId": "letters",
"suggestionEngine": "latin"
}
]
},
{
"code": "zh-Hans",
"name": "简体中文",
"defaultSkinZip": "",
"layouts": [
{
"variant": "qwerty",
"title": "QWERTY",
"profileId": "zh_Hans_CN_qwerty",
"layoutJsonId": "letters",
"suggestionEngine": "pinyin_simplified"
}
]
}
]
}