diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index b7c6812c4..f9c4d287c 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -210,6 +210,8 @@ 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F639942AA6CFBB009B9914 /* EZBingConfig.m */; }; 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; + 03FB3EDD2B1B405B004C3238 /* sign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FB3EDC2B1B405B004C3238 /* sign.swift */; }; + 03FB3EDD2B1B405B004C3238 /* TencentSigning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FB3EDC2B1B405B004C3238 /* TencentSigning.swift */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF62B0DFF9000A7D372 /* EZNiuTransTranslate.m */; }; 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2721E4CF2AFE920700A059AC /* Alamofire */; }; @@ -225,6 +227,9 @@ 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 62ED29A12B15F1F500901F51 /* EZWrapView.m */; }; A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; }; B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; }; + C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01E82B12B3C80025EE8E /* TencentService.swift */; }; + C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */; }; + C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */; }; C4DE3D6D2AC00EB500C2B85D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = C4DE3D6C2AC00EB500C2B85D /* Localizable.xcstrings */; }; C98CAE75239F4619005F7DCA /* EasydictHelper.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = C90BE309239F38EB00ADE88B /* EasydictHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -641,6 +646,8 @@ 03F639932AA6CFBB009B9914 /* EZBingConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZBingConfig.h; sourceTree = ""; }; 03F639942AA6CFBB009B9914 /* EZBingConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZBingConfig.m; sourceTree = ""; }; 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; + 03FB3EDC2B1B405B004C3238 /* sign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sign.swift; sourceTree = ""; }; + 03FB3EDC2B1B405B004C3238 /* TencentSigning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentSigning.swift; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; 17BCAEF42B0DFF9000A7D372 /* EZNiuTransTranslate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslate.h; sourceTree = ""; }; @@ -667,6 +674,9 @@ 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Easydict.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91E3E579C6DB88658B4BB102 /* Pods-Easydict.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.release.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.release.xcconfig"; sourceTree = ""; }; A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = ""; }; + C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = ""; }; + C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentResponse.swift; sourceTree = ""; }; + C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentTranslateType.swift; sourceTree = ""; }; C4DE3D6C2AC00EB500C2B85D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Easydict/App/Localizable.xcstrings; sourceTree = SOURCE_ROOT; }; C4DE3D6E2AC00EB500C2B85D /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/Main.xcstrings; sourceTree = ""; }; C90BE309239F38EB00ADE88B /* EasydictHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EasydictHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1172,6 +1182,7 @@ children = ( 17BCAEF22B0DFF9000A7D372 /* Niutrans */, 2746AEBF2AF95040005FE0A1 /* Caiyun */, + C4DD01E72B12B3B00025EE8E /* Tencent */, 6220AD582A8280E800BBFB52 /* Bing */, 0399C6A929A8608000B4AFCC /* OpenAI */, 03F14A382956011400CB7379 /* Volcano */, @@ -1941,6 +1952,17 @@ name = "Recovered References"; sourceTree = ""; }; + C4DD01E72B12B3B00025EE8E /* Tencent */ = { + isa = PBXGroup; + children = ( + C4DD01E82B12B3C80025EE8E /* TencentService.swift */, + C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */, + C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */, + 03FB3EDC2B1B405B004C3238 /* TencentSigning.swift */, + ); + path = Tencent; + sourceTree = ""; + }; C99EEB0F2385796700FEE666 = { isa = PBXGroup; children = ( @@ -2250,6 +2272,7 @@ 03B0232E29231FA6001C7E63 /* MMCrashSignalExceptionHandler.m in Sources */, 03BDA7C42A26DA280079D04F /* NSDictionary+RubyDescription.m in Sources */, 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */, + C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */, 036A0DB82AD8403A006E6D4F /* NSString+EZHandleInputText.m in Sources */, 03BDA7C12A26DA280079D04F /* XPMArgumentParser.m in Sources */, 03B0231329231FA6001C7E63 /* NSView+HiddenDebug.m in Sources */, @@ -2350,6 +2373,7 @@ 039B694F2A9D9F370063709D /* EZWebViewManager.m in Sources */, 03D747432A07FB150006CD77 /* EZError.m in Sources */, 03B0231629231FA6001C7E63 /* SnipFocusView.m in Sources */, + 03FB3EDD2B1B405B004C3238 /* TencentSigning.swift in Sources */, 03B0230329231FA6001C7E63 /* EZResultView.m in Sources */, 03CAB9552ADBF0FF00DA94A3 /* EZSystemUtility.m in Sources */, 03BDA7C32A26DA280079D04F /* NSArray+XPMArgumentsNormalizer.m in Sources */, @@ -2376,6 +2400,7 @@ 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, + C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, 036A0DBB2AD941F9006E6D4F /* EZReplaceTextButton.m in Sources */, 03DC7C662A3CA465000BF7C9 /* HWSegmentedControl.m in Sources */, 03B022E929231FA6001C7E63 /* AppDelegate.m in Sources */, @@ -2383,6 +2408,7 @@ 03B0233529231FA6001C7E63 /* MMFileLogFormatter.m in Sources */, 03DC38C1292CC97900922CB2 /* EZServiceInfo.m in Sources */, 03B0232A29231FA6001C7E63 /* NSColor+MyColors.m in Sources */, + C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */, 03D043562928940500E7559E /* EZBaseQueryWindow.m in Sources */, 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index d5db1b405..bae71fa53 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -2175,6 +2175,23 @@ } } }, + "tencent_translate" : { + "comment" : "The name of Tencent Translate", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tencent Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "腾讯翻译君" + } + } + } + }, "toggle_languages" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Service/Model/EZConstKey.h b/Easydict/Feature/Service/Model/EZConstKey.h index d698c17d4..c5fe96a54 100644 --- a/Easydict/Feature/Service/Model/EZConstKey.h +++ b/Easydict/Feature/Service/Model/EZConstKey.h @@ -32,7 +32,8 @@ static NSString *const EZDeepLAuthKey = @"EZDeepLAuthKey"; static NSString *const EZBingCookieKey = @"EZBingCookieKey"; static NSString *const EZNiuTransAPIKey = @"EZNiuTransAPIKey"; static NSString *const EZCaiyunToken = @"EZCaiyunToken"; - +static NSString *const EZTencentSecretId = @"EZTencentSecretId"; +static NSString *const EZTencentSecretKey = @"EZTencentSecretKey"; @interface EZConstKey : NSObject diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.h b/Easydict/Feature/Service/Model/EZEnumTypes.h index db285ade9..75ee92999 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.h +++ b/Easydict/Feature/Service/Model/EZEnumTypes.h @@ -41,6 +41,7 @@ FOUNDATION_EXPORT EZServiceType const EZServiceTypeAppleDictionary; FOUNDATION_EXPORT EZServiceType const EZServiceTypeBing; FOUNDATION_EXPORT EZServiceType const EZServiceTypeNiuTrans; FOUNDATION_EXPORT EZServiceType const EZServiceTypeCaiyun; +FOUNDATION_EXPORT EZServiceType const EZServiceTypeTencent; FOUNDATION_EXPORT NSString *const EZQueryTextTypeKey; FOUNDATION_EXPORT NSString *const EZIntelligentQueryTextTypeKey; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.m b/Easydict/Feature/Service/Model/EZEnumTypes.m index 22133cb5e..9ca759786 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.m +++ b/Easydict/Feature/Service/Model/EZEnumTypes.m @@ -20,6 +20,7 @@ NSString *const EZServiceTypeBing = @"Bing"; NSString *const EZServiceTypeNiuTrans = @"NiuTrans"; NSString *const EZServiceTypeCaiyun = @"Caiyun"; +NSString *const EZServiceTypeTencent = @"Tencent"; NSString *const EZServiceTypeAppleDictionary = @"AppleDictionary"; diff --git a/Easydict/Feature/Service/Model/EZServiceTypes.m b/Easydict/Feature/Service/Model/EZServiceTypes.m index 9deb9c25b..a1cc9a103 100644 --- a/Easydict/Feature/Service/Model/EZServiceTypes.m +++ b/Easydict/Feature/Service/Model/EZServiceTypes.m @@ -61,6 +61,7 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { EZServiceTypeVolcano, [EZVolcanoTranslate class], EZServiceTypeNiuTrans, [EZNiuTransTranslate class], EZServiceTypeCaiyun, [EZCaiyunService class], + EZServiceTypeTencent, [EZTencentService class], nil]; return allServiceDict; } diff --git a/Easydict/Feature/Service/Tencent/TencentResponse.swift b/Easydict/Feature/Service/Tencent/TencentResponse.swift new file mode 100644 index 000000000..61b1e7329 --- /dev/null +++ b/Easydict/Feature/Service/Tencent/TencentResponse.swift @@ -0,0 +1,60 @@ +// +// TencentResponse.swift +// Easydict +// +// Created by Jerry on 2023-11-25. +// Copyright © 2023 izual. All rights reserved. +// + +import Foundation + +struct TencentResponse: Codable { + struct Response: Codable { + var RequestId: String + var Source: String + var Target: String + var TargetText: String + } + + var Response: Response +} + +/** + { + "Response": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "不支持的语种:hi_to_zh" + }, + "RequestId": "eb6d17f2-6771-4653-af6f-6b2edbf07294" + } + } + */ +struct TencentErrorResponse: Codable { + struct Response: Codable { + var error: Error + var requestId: String + + // CodingKeys 枚举用于映射字段名 + private enum CodingKeys: String, CodingKey { + case error = "Error" // error --> Error + case requestId = "RequestId" // requestId --> RequestId + } + } + + struct Error: Codable { + var code: String + var message: String + + private enum CodingKeys: String, CodingKey { + case code = "Code" // code --> Code + case message = "Message" // message --> Message + } + } + + var response: Response + + private enum CodingKeys: String, CodingKey { + case response = "Response" // response --> Response + } +} diff --git a/Easydict/Feature/Service/Tencent/TencentService.swift b/Easydict/Feature/Service/Tencent/TencentService.swift new file mode 100644 index 000000000..f9ff8632b --- /dev/null +++ b/Easydict/Feature/Service/Tencent/TencentService.swift @@ -0,0 +1,131 @@ +// +// TencentService.swift +// Easydict +// +// Created by Jerry on 2023-11-25. +// Copyright © 2023 izual. All rights reserved. +// + +import Alamofire +import Foundation + +@objc(EZTencentService) +public final class TencentService: QueryService { + override public func serviceType() -> ServiceType { + .tencent + } + + override public func link() -> String? { + "https://fanyi.qq.com" + } + + override public func name() -> String { + NSLocalizedString("tencent_translate", comment: "The name of Tencent Translate") + } + + override public func supportLanguagesDictionary() -> MMOrderedDictionary { + // TODO: Replace MMOrderedDictionary in the API + let orderedDict = MMOrderedDictionary() + TencentTranslateType.supportLanguagesDictionary.forEach { key, value in + orderedDict.setObject(value as NSString, forKey: key.rawValue as NSString) + } + return orderedDict + } + + override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + NSLog("Tencent Translate currently does not support OCR") + throw QueryServiceError.notSupported + } + + // MARK: API Request + private static let defaultSecretId = "" + private static let defaultSecretKey = "" + + + // easydict://writeKeyValue?EZTencentSecretId=xxx + private var secretId: String { + let secretId = UserDefaults.standard.string(forKey: EZTencentSecretId) + if let secretId, !secretId.isEmpty { + return secretId + } else { + return TencentService.defaultSecretId + } + } + + // easydict://writeKeyValue?EZTencentSecretKey=xxx + private var secretKey: String { + let secretKey = UserDefaults.standard.string(forKey: EZTencentSecretKey) + if let secretKey, !secretKey.isEmpty { + return secretKey + } else { + return TencentService.defaultSecretKey + } + } + + public override func translate(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) { + if prehandleQueryTextLanguage(text, from: from, to: to, completion: completion) { + return + } + + translateText(text, from: from, to: to, completion: completion) + } + + func translateText(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) { + let transType = TencentTranslateType.transType(from: from, to: to) + guard transType != .unsupported else { + result.errorType = .unsupportedLanguage + let unsupportedType = NSLocalizedString("unsupported_translation_type", comment: "") + result.errorMessage = "\(unsupportedType): \(from.rawValue) --> \(to.rawValue)" + completion(result, nil) + return + } + + let parameters: [String: Any] = [ + "SourceText": text, + "Source": transType.sourceLanguage, + "Target": transType.targetLanguage, + "ProjectId": 0 + ] + + let endpoint = "https://tmt.tencentcloudapi.com" + + let service = "tmt" + let action = "TextTranslate" + let version = "2018-03-21" + + let headers = tencentSignHeader(service: service, action: action, version: version, parameters: parameters, secretId: secretId, secretKey: secretKey) + + let request = AF.request(endpoint, + method: .post, + parameters: parameters, + encoding: JSONEncoding.default, + headers: headers) + .validate() + .responseDecodable(of: TencentResponse.self) { [weak self] response in + guard let self else { return } + let result = self.result + switch response.result { + case let .success(value): + result.from = from + result.to = to + result.queryText = text + result.translatedResults = value.Response.TargetText.components(separatedBy: "\n") + completion(result, nil) + case let .failure(error): + NSLog("Tencent lookup error \(error)") + if let data = response.data { + do { + let errorResponse = try JSONDecoder().decode(TencentErrorResponse.self, from: data) + result.errorMessage = errorResponse.response.error.message + } catch { + NSLog("Failed to decode error response: \(error)") + } + } + completion(result, error) + } + } + queryModel.setStop({ + request.cancel() + }, serviceType: serviceType().rawValue) + } +} diff --git a/Easydict/Feature/Service/Tencent/TencentSigning.swift b/Easydict/Feature/Service/Tencent/TencentSigning.swift new file mode 100644 index 000000000..071b0191f --- /dev/null +++ b/Easydict/Feature/Service/Tencent/TencentSigning.swift @@ -0,0 +1,95 @@ +// +// TencentSigning.swift +// Easydict +// +// Created by tisfeng on 2023/12/2. +// Copyright © 2023 izual. All rights reserved. +// + +import Foundation +import Alamofire +import CryptoKit + +// Tencent sigh header, Ref: https://github.com/TencentCloud/signature-process-demo/blob/main/signature-v3/swift/signv3.swift +func tencentSignHeader(service: String, action: String, version: String, parameters: [String: Any], secretId: String, secretKey: String) -> HTTPHeaders { + let service = service + let host = "\(service).tencentcloudapi.com" + let region = "ap-guangzhou" + let action = action + let version = version + let algorithm = "TC3-HMAC-SHA256" + let timestamp = Int(Date().timeIntervalSince1970) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + dateFormatter.timeZone = TimeZone(identifier: "UTC") + let date = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp))) + + // ************* 步骤 1:拼接规范请求串 ************* + let httpRequestMethod = "POST" + let canonicalUri = "/" + let canonicalQuerystring = "" + let ct = "application/json; charset=utf-8" + let payload = try! JSONSerialization.data(withJSONObject: parameters) + let payloadString = String(data: payload, encoding: .utf8)! + let canonicalHeaders = "content-type:\(ct)\nhost:\(host)\nx-tc-action:\(action.lowercased())\n" + let signedHeaders = "content-type;host;x-tc-action" + let hashedRequestPayload = payloadString.sha256() + let canonicalRequest = """ +\(httpRequestMethod) +\(canonicalUri) +\(canonicalQuerystring) +\(canonicalHeaders) +\(signedHeaders) +\(hashedRequestPayload) +""" + + // ************* 步骤 2:拼接待签名字符串 ************* + let credentialScope = "\(date)/\(service)/tc3_request" + let hashedCanonicalRequest = canonicalRequest.sha256() + let stringToSign = """ +\(algorithm) +\(timestamp) +\(credentialScope) +\(hashedCanonicalRequest) +""" + + // ************* 步骤 3:计算签名 ************* + let secretDate = date.hmac(key: Data("TC3\(secretKey)".utf8)) + let secretService = service.hmac(key: secretDate) + let secretSigning = "tc3_request".hmac(key: secretService) + let signature = stringToSign.hmac(key: secretSigning).map {String(format: "%02hhx", $0)}.joined() + + // ************* 步骤 4:拼接 Authorization ************* + let authorization = """ +\(algorithm) Credential=\(secretId)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature) +""" + + let headers: HTTPHeaders = [ + "Authorization": authorization, + "Content-Type": ct, + "Host": host, + "X-TC-Action": action, + "X-TC-Timestamp": "\(timestamp)", + "X-TC-Version": version, + "X-TC-Region": region + ] + + return headers +} + +extension String { + // sha256 + func sha256() -> String { + let data = Data(self.utf8) + let digest = SHA256.hash(data: data) + return digest.compactMap {String(format: "%02x", $0)}.joined() + } + + // hmac + func hmac(key: Data) -> Data { + let data = Data(self.utf8) + let symmetricKey = SymmetricKey(data: key) + let hmac = HMAC.authenticationCode(for: data, using: symmetricKey) + return Data(hmac) + } +} diff --git a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift new file mode 100644 index 000000000..1c52f6922 --- /dev/null +++ b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift @@ -0,0 +1,91 @@ +// +// TencentTranslateType.swift +// Easydict +// +// Created by Jerry on 2023-11-25. +// Copyright © 2023 izual. All rights reserved. +// + +import Foundation + +struct TencentTranslateType: Equatable { + + var sourceLanguage: String + var targetLanguage: String + + static let unsupported = TencentTranslateType(sourceLanguage: "unsupported", targetLanguage: "unsupported") + + // This docs missed traditionalChinese as target language if target languages contains simplifiedChinese. https://cloud.tencent.com/document/api/551/15619 + static let supportedTypes: [Language: [Language]] = [ + .simplifiedChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], + .traditionalChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], + .english: [.simplifiedChinese, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay, .arabic, .hindi], + .japanese: [.simplifiedChinese, .english, .korean], + .korean: [.simplifiedChinese, .english, .japanese], + .french: [.simplifiedChinese, .english, .spanish, .italian, .german, .turkish, .russian, .portuguese], + .spanish: [.simplifiedChinese, .english, .french, .italian, .german, .turkish, .russian, .portuguese], + .italian: [.simplifiedChinese, .english, .french, .spanish, .german, .turkish, .russian, .portuguese], + .german: [.simplifiedChinese, .english, .french, .spanish, .italian, .turkish, .russian, .portuguese], + .turkish: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .russian, .portuguese], + .russian: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .portuguese], + .portuguese: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .russian], + .vietnamese: [.simplifiedChinese, .english], + .indonesian: [.simplifiedChinese, .english], + .thai: [.simplifiedChinese, .english], + .malay: [.simplifiedChinese, .english], + .arabic: [.english], + .hindi: [.english] + ] + + static let supportLanguagesDictionary: [Language: String] = [ + .auto: "auto", + .simplifiedChinese: "zh", + .traditionalChinese: "zh-TW", + .english: "en", + .japanese: "ja", + .korean: "ko", + .french: "fr", + .spanish: "es", + .italian: "it", + .german: "de", + .turkish: "tr", + .russian: "ru", + .portuguese: "pt", + .vietnamese: "vi", + .indonesian: "id", + .thai: "th", + .malay: "ms", + .arabic: "ar", + .hindi: "hi" + ] + + static func transType(from: Language, to: Language) -> TencentTranslateType { + // !!!: Tencent translate support traditionalChinese as target language if target languages contain simplifiedChinese. + guard let targetLanguages = supportedTypes[from], + (targetLanguages.containsChinese() || targetLanguages.contains(to) || from == to || from.isKindOfChinese()) else { + return .unsupported + } + + guard let fromLanguage = supportLanguagesDictionary[from], + let toLanguage = supportLanguagesDictionary[to] else { + return .unsupported + } + + return TencentTranslateType(sourceLanguage: fromLanguage, targetLanguage: toLanguage) + } +} + + +extension Array where Element == Language { + // Contains Chinese language + func containsChinese() -> Bool { + contains { $0.isKindOfChinese() } + } +} + +extension Language { + // Is kind of Chinese language + func isKindOfChinese() -> Bool { + self == .simplifiedChinese || self == .traditionalChinese + } +} diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index 238d098e8..2098cdbe6 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -212,10 +212,11 @@ - (NSArray *)allowedReadWriteKeys { EZDeepLTranslationAPIKey, EZNiuTransAPIKey, EZCaiyunToken, + EZTencentSecretId, + EZTencentSecretKey, + EZBingCookieKey, EZIntelligentQueryModeKey, - - EZBingCookieKey, ]; return readWriteKeys;