├── .cursor └── rules │ └── swiftui-swift-simple-developer-cursor-rules.mdc ├── .gitignore ├── LICENSE ├── LanguageTool.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── LanguageTool ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024@2x.png │ │ ├── 128@2x.png │ │ ├── 256@2x 1.png │ │ ├── 256@2x.png │ │ ├── 32@2x.png │ │ ├── 512@2x 1.png │ │ ├── 512@2x.png │ │ ├── 64@2x.png │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Extensions │ ├── View+Extensions.swift │ └── View+Localization.swift ├── Info.plist ├── LanguageTool.entitlements ├── LanguageToolApp.swift ├── Models │ ├── AIError.swift │ ├── AIServiceType.swift │ ├── AppSettings.swift │ ├── Language.swift │ ├── LocalizationFormat.swift │ ├── PlatformType.swift │ └── TranslationManager.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Utilities │ ├── AIService.swift │ ├── ARBFileHandler.swift │ ├── AliyunService.swift │ ├── DeepSeekService.swift │ ├── ElectronLocalizationHandler.swift │ ├── GeminiService.swift │ ├── JsonUtils.swift │ ├── LocalizationJSONGenerator.swift │ ├── LocalizationManager.swift │ └── StringsFileParser.swift ├── ViewModels │ └── TransferViewModel.swift └── Views │ ├── Components │ ├── DragDropButton.swift │ └── LanguageToggle.swift │ ├── DeepseekDemo.swift │ ├── LocalizationMasterView.swift │ ├── SettingsView.swift │ └── TransferView.swift ├── Localizable.xcstrings ├── README-zh.md └── README.md /.cursor/rules/swiftui-swift-simple-developer-cursor-rules.mdc: -------------------------------------------------------------------------------- 1 | 2 | You are an expert iOS developer using Swift and SwiftUI. Follow these guidelines: 3 | 4 | 5 | # Code Structure 6 | 7 | - Use Swift's latest features and protocol-oriented programming 8 | - Prefer value types (structs) over classes 9 | - Use MVVM architecture with SwiftUI 10 | - Structure: Features/, Core/, UI/, Resources/ 11 | - Follow Apple's Human Interface Guidelines 12 | 13 | 14 | # Naming 15 | - camelCase for vars/funcs, PascalCase for types 16 | - Verbs for methods (fetchData) 17 | - Boolean: use is/has/should prefixes 18 | - Clear, descriptive names following Apple style 19 | 20 | 21 | # Swift Best Practices 22 | 23 | - Strong type system, proper optionals 24 | - async/await for concurrency 25 | - Result type for errors 26 | - @Published, @StateObject for state 27 | - Prefer let over var 28 | - Protocol extensions for shared code 29 | 30 | 31 | # UI Development 32 | 33 | - SwiftUI first, UIKit when needed 34 | - SF Symbols for icons 35 | - Support dark mode, dynamic type 36 | - SafeArea and GeometryReader for layout 37 | - Handle all screen sizes and orientations 38 | - Implement proper keyboard handling 39 | 40 | 41 | # Performance 42 | 43 | - Profile with Instruments 44 | - Lazy load views and images 45 | - Optimize network requests 46 | - Background task handling 47 | - Proper state management 48 | - Memory management 49 | 50 | 51 | # Data & State 52 | 53 | - CoreData for complex models 54 | - UserDefaults for preferences 55 | - Combine for reactive code 56 | - Clean data flow architecture 57 | - Proper dependency injection 58 | - Handle state restoration 59 | 60 | 61 | # Security 62 | 63 | - Encrypt sensitive data 64 | - Use Keychain securely 65 | - Certificate pinning 66 | - Biometric auth when needed 67 | - App Transport Security 68 | - Input validation 69 | 70 | 71 | # Testing & Quality 72 | 73 | - XCTest for unit tests 74 | - XCUITest for UI tests 75 | - Test common user flows 76 | - Performance testing 77 | - Error scenarios 78 | - Accessibility testing 79 | 80 | 81 | # Essential Features 82 | 83 | - Deep linking support 84 | - Push notifications 85 | - Background tasks 86 | - Localization 87 | - Error handling 88 | - Analytics/logging 89 | 90 | 91 | # Development Process 92 | 93 | - Use SwiftUI previews 94 | - Git branching strategy 95 | - Code review process 96 | - CI/CD pipeline 97 | - Documentation 98 | - Unit test coverage 99 | 100 | 101 | # App Store Guidelines 102 | 103 | - Privacy descriptions 104 | - App capabilities 105 | - In-app purchases 106 | - Review guidelines 107 | - App thinning 108 | - Proper signing 109 | 110 | 111 | Follow Apple's documentation for detailed implementation guidance. 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LanguageTool.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 894EABC82D6D6BBA00F1BE09 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 894EABC72D6D6BBA00F1BE09 /* Localizable.xcstrings */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 894EABC72D6D6BBA00F1BE09 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 15 | 89D94CC12D5CC564006DE9FB /* LanguageTool.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LanguageTool.app; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | /* End PBXFileReference section */ 17 | 18 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 19 | 89D94CE62D5CCDDA006DE9FB /* Exceptions for "LanguageTool" folder in "LanguageTool" target */ = { 20 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 21 | membershipExceptions = ( 22 | Info.plist, 23 | ); 24 | target = 89D94CC02D5CC564006DE9FB /* LanguageTool */; 25 | }; 26 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 27 | 28 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 29 | 89D94CC32D5CC564006DE9FB /* LanguageTool */ = { 30 | isa = PBXFileSystemSynchronizedRootGroup; 31 | exceptions = ( 32 | 89D94CE62D5CCDDA006DE9FB /* Exceptions for "LanguageTool" folder in "LanguageTool" target */, 33 | ); 34 | path = LanguageTool; 35 | sourceTree = ""; 36 | }; 37 | /* End PBXFileSystemSynchronizedRootGroup section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 89D94CBE2D5CC564006DE9FB /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | ); 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXFrameworksBuildPhase section */ 48 | 49 | /* Begin PBXGroup section */ 50 | 89D94CB82D5CC564006DE9FB = { 51 | isa = PBXGroup; 52 | children = ( 53 | 894EABC72D6D6BBA00F1BE09 /* Localizable.xcstrings */, 54 | 89D94CC32D5CC564006DE9FB /* LanguageTool */, 55 | 89D94CC22D5CC564006DE9FB /* Products */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 89D94CC22D5CC564006DE9FB /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 89D94CC12D5CC564006DE9FB /* LanguageTool.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXGroup section */ 68 | 69 | /* Begin PBXNativeTarget section */ 70 | 89D94CC02D5CC564006DE9FB /* LanguageTool */ = { 71 | isa = PBXNativeTarget; 72 | buildConfigurationList = 89D94CD22D5CC566006DE9FB /* Build configuration list for PBXNativeTarget "LanguageTool" */; 73 | buildPhases = ( 74 | 89D94CBD2D5CC564006DE9FB /* Sources */, 75 | 89D94CBE2D5CC564006DE9FB /* Frameworks */, 76 | 89D94CBF2D5CC564006DE9FB /* Resources */, 77 | ); 78 | buildRules = ( 79 | ); 80 | dependencies = ( 81 | ); 82 | fileSystemSynchronizedGroups = ( 83 | 89D94CC32D5CC564006DE9FB /* LanguageTool */, 84 | ); 85 | name = LanguageTool; 86 | packageProductDependencies = ( 87 | ); 88 | productName = LanguageTool; 89 | productReference = 89D94CC12D5CC564006DE9FB /* LanguageTool.app */; 90 | productType = "com.apple.product-type.application"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | 89D94CB92D5CC564006DE9FB /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | BuildIndependentTargetsInParallel = 1; 99 | LastSwiftUpdateCheck = 1610; 100 | LastUpgradeCheck = 1610; 101 | TargetAttributes = { 102 | 89D94CC02D5CC564006DE9FB = { 103 | CreatedOnToolsVersion = 16.1; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = 89D94CBC2D5CC564006DE9FB /* Build configuration list for PBXProject "LanguageTool" */; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | "zh-Hans", 114 | "zh-Hant", 115 | "zh-HK", 116 | ja, 117 | ko, 118 | de, 119 | fr, 120 | ru, 121 | es, 122 | it, 123 | ar, 124 | ca, 125 | hr, 126 | cs, 127 | da, 128 | nl, 129 | "en-AU", 130 | "en-IN", 131 | "en-GB", 132 | fi, 133 | "fr-CA", 134 | el, 135 | he, 136 | hi, 137 | hu, 138 | id, 139 | ms, 140 | nb, 141 | pl, 142 | "pt-BR", 143 | "pt-PT", 144 | ro, 145 | sk, 146 | sl, 147 | "es-419", 148 | "es-US", 149 | sv, 150 | th, 151 | tr, 152 | uk, 153 | vi, 154 | ); 155 | mainGroup = 89D94CB82D5CC564006DE9FB; 156 | minimizedProjectReferenceProxies = 1; 157 | preferredProjectObjectVersion = 77; 158 | productRefGroup = 89D94CC22D5CC564006DE9FB /* Products */; 159 | projectDirPath = ""; 160 | projectRoot = ""; 161 | targets = ( 162 | 89D94CC02D5CC564006DE9FB /* LanguageTool */, 163 | ); 164 | }; 165 | /* End PBXProject section */ 166 | 167 | /* Begin PBXResourcesBuildPhase section */ 168 | 89D94CBF2D5CC564006DE9FB /* Resources */ = { 169 | isa = PBXResourcesBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | 894EABC82D6D6BBA00F1BE09 /* Localizable.xcstrings in Resources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXResourcesBuildPhase section */ 177 | 178 | /* Begin PBXSourcesBuildPhase section */ 179 | 89D94CBD2D5CC564006DE9FB /* Sources */ = { 180 | isa = PBXSourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin XCBuildConfiguration section */ 189 | 89D94CD02D5CC566006DE9FB /* Debug */ = { 190 | isa = XCBuildConfiguration; 191 | buildSettings = { 192 | ALWAYS_SEARCH_USER_PATHS = NO; 193 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 194 | CLANG_ANALYZER_NONNULL = YES; 195 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 196 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 197 | CLANG_ENABLE_MODULES = YES; 198 | CLANG_ENABLE_OBJC_ARC = YES; 199 | CLANG_ENABLE_OBJC_WEAK = YES; 200 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_COMMA = YES; 203 | CLANG_WARN_CONSTANT_CONVERSION = YES; 204 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 205 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 206 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 207 | CLANG_WARN_EMPTY_BODY = YES; 208 | CLANG_WARN_ENUM_CONVERSION = YES; 209 | CLANG_WARN_INFINITE_RECURSION = YES; 210 | CLANG_WARN_INT_CONVERSION = YES; 211 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 212 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 213 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 215 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 216 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 217 | CLANG_WARN_STRICT_PROTOTYPES = YES; 218 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 219 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 220 | CLANG_WARN_UNREACHABLE_CODE = YES; 221 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 222 | COPY_PHASE_STRIP = NO; 223 | DEBUG_INFORMATION_FORMAT = dwarf; 224 | ENABLE_STRICT_OBJC_MSGSEND = YES; 225 | ENABLE_TESTABILITY = YES; 226 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 227 | GCC_C_LANGUAGE_STANDARD = gnu17; 228 | GCC_DYNAMIC_NO_PIC = NO; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_OPTIMIZATION_LEVEL = 0; 231 | GCC_PREPROCESSOR_DEFINITIONS = ( 232 | "DEBUG=1", 233 | "$(inherited)", 234 | ); 235 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 236 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 237 | GCC_WARN_UNDECLARED_SELECTOR = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 239 | GCC_WARN_UNUSED_FUNCTION = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 242 | MACOSX_DEPLOYMENT_TARGET = 15.1; 243 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 244 | MTL_FAST_MATH = YES; 245 | ONLY_ACTIVE_ARCH = YES; 246 | SDKROOT = macosx; 247 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 248 | SWIFT_EMIT_LOC_STRINGS = YES; 249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 250 | }; 251 | name = Debug; 252 | }; 253 | 89D94CD12D5CC566006DE9FB /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 258 | CLANG_ANALYZER_NONNULL = YES; 259 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_ENABLE_OBJC_WEAK = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 271 | CLANG_WARN_EMPTY_BODY = YES; 272 | CLANG_WARN_ENUM_CONVERSION = YES; 273 | CLANG_WARN_INFINITE_RECURSION = YES; 274 | CLANG_WARN_INT_CONVERSION = YES; 275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 281 | CLANG_WARN_STRICT_PROTOTYPES = YES; 282 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 283 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 284 | CLANG_WARN_UNREACHABLE_CODE = YES; 285 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 286 | COPY_PHASE_STRIP = NO; 287 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 288 | ENABLE_NS_ASSERTIONS = NO; 289 | ENABLE_STRICT_OBJC_MSGSEND = YES; 290 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 291 | GCC_C_LANGUAGE_STANDARD = gnu17; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 295 | GCC_WARN_UNDECLARED_SELECTOR = YES; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 300 | MACOSX_DEPLOYMENT_TARGET = 15.1; 301 | MTL_ENABLE_DEBUG_INFO = NO; 302 | MTL_FAST_MATH = YES; 303 | SDKROOT = macosx; 304 | SWIFT_COMPILATION_MODE = wholemodule; 305 | SWIFT_EMIT_LOC_STRINGS = YES; 306 | }; 307 | name = Release; 308 | }; 309 | 89D94CD32D5CC566006DE9FB /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 314 | CODE_SIGN_ENTITLEMENTS = LanguageTool/LanguageTool.entitlements; 315 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 316 | CODE_SIGN_STYLE = Automatic; 317 | COMBINE_HIDPI_IMAGES = YES; 318 | CURRENT_PROJECT_VERSION = 2025040201; 319 | DEVELOPMENT_ASSET_PATHS = "\"LanguageTool/Preview Content\""; 320 | DEVELOPMENT_TEAM = R9TC286V25; 321 | ENABLE_HARDENED_RUNTIME = YES; 322 | ENABLE_PREVIEWS = YES; 323 | GENERATE_INFOPLIST_FILE = YES; 324 | INFOPLIST_FILE = LanguageTool/Info.plist; 325 | INFOPLIST_KEY_CFBundleDisplayName = LanguageTool; 326 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 327 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "需要访问系统权限来保存本地化文件"; 328 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "需要访问桌面文件夹来保存本地化文件"; 329 | INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "需要访问文档文件夹来保存本地化文件"; 330 | INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "需要访问下载文件夹来保存本地化文件"; 331 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 332 | LD_RUNPATH_SEARCH_PATHS = ( 333 | "$(inherited)", 334 | "@executable_path/../Frameworks", 335 | ); 336 | MACOSX_DEPLOYMENT_TARGET = 14.0; 337 | MARKETING_VERSION = 1.0.5; 338 | PRODUCT_BUNDLE_IDENTIFIER = com.huazi.LanguageTool; 339 | PRODUCT_NAME = "$(TARGET_NAME)"; 340 | SWIFT_EMIT_LOC_STRINGS = YES; 341 | SWIFT_VERSION = 5.0; 342 | }; 343 | name = Debug; 344 | }; 345 | 89D94CD42D5CC566006DE9FB /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 349 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 350 | CODE_SIGN_ENTITLEMENTS = LanguageTool/LanguageTool.entitlements; 351 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 352 | CODE_SIGN_STYLE = Automatic; 353 | COMBINE_HIDPI_IMAGES = YES; 354 | CURRENT_PROJECT_VERSION = 2025040201; 355 | DEVELOPMENT_ASSET_PATHS = "\"LanguageTool/Preview Content\""; 356 | DEVELOPMENT_TEAM = R9TC286V25; 357 | ENABLE_HARDENED_RUNTIME = YES; 358 | ENABLE_PREVIEWS = YES; 359 | GENERATE_INFOPLIST_FILE = YES; 360 | INFOPLIST_FILE = LanguageTool/Info.plist; 361 | INFOPLIST_KEY_CFBundleDisplayName = LanguageTool; 362 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 363 | INFOPLIST_KEY_NSAppleEventsUsageDescription = "需要访问系统权限来保存本地化文件"; 364 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "需要访问桌面文件夹来保存本地化文件"; 365 | INFOPLIST_KEY_NSDocumentsFolderUsageDescription = "需要访问文档文件夹来保存本地化文件"; 366 | INFOPLIST_KEY_NSDownloadsFolderUsageDescription = "需要访问下载文件夹来保存本地化文件"; 367 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 368 | LD_RUNPATH_SEARCH_PATHS = ( 369 | "$(inherited)", 370 | "@executable_path/../Frameworks", 371 | ); 372 | MACOSX_DEPLOYMENT_TARGET = 14.0; 373 | MARKETING_VERSION = 1.0.5; 374 | PRODUCT_BUNDLE_IDENTIFIER = com.huazi.LanguageTool; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | SWIFT_EMIT_LOC_STRINGS = YES; 377 | SWIFT_VERSION = 5.0; 378 | }; 379 | name = Release; 380 | }; 381 | /* End XCBuildConfiguration section */ 382 | 383 | /* Begin XCConfigurationList section */ 384 | 89D94CBC2D5CC564006DE9FB /* Build configuration list for PBXProject "LanguageTool" */ = { 385 | isa = XCConfigurationList; 386 | buildConfigurations = ( 387 | 89D94CD02D5CC566006DE9FB /* Debug */, 388 | 89D94CD12D5CC566006DE9FB /* Release */, 389 | ); 390 | defaultConfigurationIsVisible = 0; 391 | defaultConfigurationName = Release; 392 | }; 393 | 89D94CD22D5CC566006DE9FB /* Build configuration list for PBXNativeTarget "LanguageTool" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | 89D94CD32D5CC566006DE9FB /* Debug */, 397 | 89D94CD42D5CC566006DE9FB /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | /* End XCConfigurationList section */ 403 | }; 404 | rootObject = 89D94CB92D5CC564006DE9FB /* Project object */; 405 | } 406 | -------------------------------------------------------------------------------- /LanguageTool.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/1024@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/1024@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/128@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/256@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/256@2x 1.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/256@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/32@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/512@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/512@2x 1.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/512@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aSynch1889/LanguageTool/e8f574060eb88f5f347dc93bb9e7118ea76e7016/LanguageTool/Assets.xcassets/AppIcon.appiconset/64@2x.png -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "filename" : "32@2x.png", 10 | "idiom" : "mac", 11 | "scale" : "2x", 12 | "size" : "16x16" 13 | }, 14 | { 15 | "idiom" : "mac", 16 | "scale" : "1x", 17 | "size" : "32x32" 18 | }, 19 | { 20 | "filename" : "64@2x.png", 21 | "idiom" : "mac", 22 | "scale" : "2x", 23 | "size" : "32x32" 24 | }, 25 | { 26 | "filename" : "128@2x.png", 27 | "idiom" : "mac", 28 | "scale" : "1x", 29 | "size" : "128x128" 30 | }, 31 | { 32 | "filename" : "256@2x 1.png", 33 | "idiom" : "mac", 34 | "scale" : "2x", 35 | "size" : "128x128" 36 | }, 37 | { 38 | "filename" : "256@2x.png", 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "filename" : "512@2x 1.png", 45 | "idiom" : "mac", 46 | "scale" : "2x", 47 | "size" : "256x256" 48 | }, 49 | { 50 | "filename" : "512@2x.png", 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "512x512" 54 | }, 55 | { 56 | "filename" : "1024@2x.png", 57 | "idiom" : "mac", 58 | "scale" : "2x", 59 | "size" : "512x512" 60 | } 61 | ], 62 | "info" : { 63 | "author" : "xcode", 64 | "version" : 1 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LanguageTool/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LanguageTool/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftData 3 | 4 | struct ContentView: View { 5 | var body: some View { 6 | VStack(spacing: 0) { 7 | HStack { 8 | Text("Language Tool") 9 | .font(.system(size: 13, weight: .medium, design: .rounded)) 10 | .foregroundColor(.secondary) 11 | Spacer() 12 | } 13 | .padding(.horizontal) 14 | .padding(.vertical, 8) 15 | .background(Color(NSColor.windowBackgroundColor)) 16 | .overlay( 17 | Divider(), 18 | alignment: .bottom 19 | ) 20 | 21 | // 主要内容 22 | TransferView() 23 | .frame(minWidth: 600, minHeight: 500) 24 | .padding() 25 | } 26 | } 27 | } 28 | 29 | #Preview { 30 | ContentView() 31 | .modelContainer(for: [], inMemory: true) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /LanguageTool/Extensions/View+Extensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { 5 | if condition { 6 | transform(self) 7 | } else { 8 | self 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /LanguageTool/Extensions/View+Localization.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | var localized: String { 3 | return LocalizationManager.shared.localizedString(for: self) 4 | } 5 | } -------------------------------------------------------------------------------- /LanguageTool/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | NSAppTransportSecurity 8 | 9 | NSAllowsArbitraryLoads 10 | 11 | 12 | NSFileProviderUsageDescription 13 | 需要访问选择的文件夹来保存本地文件 14 | 15 | 16 | -------------------------------------------------------------------------------- /LanguageTool/LanguageTool.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LanguageTool/LanguageToolApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftData 3 | 4 | @main 5 | struct LanguageToolApp: App { 6 | init() { 7 | // 读取保存的语言设置 8 | if let savedLanguage = UserDefaults.standard.string(forKey: "appLanguage") { 9 | LocalizationManager.shared.setLanguage(savedLanguage) 10 | } else { 11 | // 默认设置为英语 12 | UserDefaults.standard.set("en", forKey: "appLanguage") 13 | LocalizationManager.shared.setLanguage("en") 14 | } 15 | } 16 | 17 | var sharedModelContainer: ModelContainer = { 18 | let schema = Schema([]) 19 | let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) 20 | 21 | do { 22 | return try ModelContainer(for: schema, configurations: [modelConfiguration]) 23 | } catch { 24 | fatalError("Could not create ModelContainer: \(error)") 25 | } 26 | }() 27 | 28 | var body: some Scene { 29 | WindowGroup { 30 | ContentView() 31 | .modelContainer(sharedModelContainer) 32 | .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in 33 | // 当语言改变时,更新本地化管理器 34 | if let language = UserDefaults.standard.string(forKey: "appLanguage") { 35 | LocalizationManager.shared.setLanguage(language) 36 | } 37 | } 38 | } 39 | .windowStyle(.hiddenTitleBar) // 隐藏默认标题栏 40 | .defaultSize(width: 600, height: 600) // 设置默认窗口大小 41 | 42 | // 添加设置窗口 43 | Settings { 44 | SettingsView() 45 | .modelContainer(sharedModelContainer) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LanguageTool/Models/AIError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum AIError: LocalizedError { 4 | case invalidURL 5 | case networkError(Error) 6 | case invalidResponse 7 | case jsonError(Error) 8 | case apiError(String) 9 | case rateLimitExceeded 10 | case unauthorized 11 | case invalidConfiguration(String) 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .invalidURL: 16 | return "Invalid URL".localized 17 | case .networkError(let error): 18 | return "Network Error: \(error.localizedDescription)".localized 19 | case .invalidResponse: 20 | return "Invalid Response from Server".localized 21 | case .jsonError(let error): 22 | return "JSON Error: \(error.localizedDescription)".localized 23 | case .apiError(let message): 24 | return "API Error: \(message)".localized 25 | case .rateLimitExceeded: 26 | return "Rate Limit Exceeded".localized 27 | case .unauthorized: 28 | return "Invalid API Key".localized 29 | case .invalidConfiguration(let message): 30 | return "⚠️ 配置错误: \(message)" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LanguageTool/Models/AIServiceType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum AIServiceType: String, CaseIterable { 4 | case deepseek = "DeepSeek" 5 | case gemini = "Gemini" 6 | case aliyun 7 | 8 | var description: String { 9 | switch self { 10 | case .deepseek: 11 | return "DeepSeek Chat" 12 | case .gemini: 13 | return "Google Gemini" 14 | case .aliyun: 15 | return "Aliyun" 16 | } 17 | } 18 | 19 | var modelName: String { 20 | switch self { 21 | case .deepseek: 22 | return "deepseek-chat" 23 | case .gemini: 24 | return "gemini-pro" 25 | case .aliyun: 26 | return "aliyun-pro" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /LanguageTool/Models/AppSettings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class AppSettings: ObservableObject { 4 | static let shared = AppSettings() 5 | 6 | @Published var apiKey: String { 7 | didSet { 8 | UserDefaults.standard.set(apiKey, forKey: "apiKey") 9 | } 10 | } 11 | 12 | private init() { 13 | // 从 UserDefaults 读取存储的设置 14 | self.apiKey = UserDefaults.standard.string(forKey: "apiKey") ?? "" 15 | } 16 | } -------------------------------------------------------------------------------- /LanguageTool/Models/Language.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Language: Identifiable, Hashable { 4 | let id = UUID() 5 | let code: String 6 | let name: String 7 | let localizedName: String 8 | 9 | // 支持 Hashable 10 | func hash(into hasher: inout Hasher) { 11 | hasher.combine(code) 12 | } 13 | 14 | static func == (lhs: Language, rhs: Language) -> Bool { 15 | lhs.code == rhs.code 16 | } 17 | 18 | // Xcode 支持的完整语言列表 19 | static let supportedLanguages: [Language] = [ 20 | // 中文 21 | Language(code: "zh-Hans", name: "Chinese, Simplified", localizedName: "简体中文"), 22 | Language(code: "zh-Hant", name: "Chinese, Traditional", localizedName: "繁體中文"), 23 | Language(code: "zh-HK", name: "Chinese, Hong Kong", localizedName: "繁體中文(香港)"), 24 | 25 | // 英语变体 26 | Language(code: "en", name: "English", localizedName: "English"), 27 | Language(code: "en-AU", name: "English, Australia", localizedName: "English (Australia)"), 28 | Language(code: "en-GB", name: "English, UK", localizedName: "English (UK)"), 29 | Language(code: "en-IN", name: "English, India", localizedName: "English (India)"), 30 | Language(code: "en-CA", name: "English, Canada", localizedName: "English (Canada)"), 31 | 32 | // 欧洲语言 33 | Language(code: "fr", name: "French", localizedName: "Français"), 34 | Language(code: "fr-CA", name: "French, Canada", localizedName: "Français (Canada)"), 35 | Language(code: "es", name: "Spanish", localizedName: "Español"), 36 | Language(code: "es-419", name: "Spanish, Latin America", localizedName: "Español (Latinoamérica)"), 37 | Language(code: "de", name: "German", localizedName: "Deutsch"), 38 | Language(code: "it", name: "Italian", localizedName: "Italiano"), 39 | Language(code: "pt", name: "Portuguese", localizedName: "Português"), 40 | Language(code: "pt-BR", name: "Portuguese, Brazil", localizedName: "Português (Brasil)"), 41 | Language(code: "pt-PT", name: "Portuguese, Portugal", localizedName: "Português (Portugal)"), 42 | Language(code: "ru", name: "Russian", localizedName: "Русский"), 43 | Language(code: "pl", name: "Polish", localizedName: "Polski"), 44 | Language(code: "tr", name: "Turkish", localizedName: "Türkçe"), 45 | Language(code: "nl", name: "Dutch", localizedName: "Nederlands"), 46 | Language(code: "sv", name: "Swedish", localizedName: "Svenska"), 47 | Language(code: "da", name: "Danish", localizedName: "Dansk"), 48 | Language(code: "fi", name: "Finnish", localizedName: "Suomi"), 49 | Language(code: "nb", name: "Norwegian Bokmål", localizedName: "Norsk bokmål"), 50 | Language(code: "el", name: "Greek", localizedName: "Ελληνικά"), 51 | Language(code: "cs", name: "Czech", localizedName: "Čeština"), 52 | Language(code: "hu", name: "Hungarian", localizedName: "Magyar"), 53 | Language(code: "sk", name: "Slovak", localizedName: "Slovenčina"), 54 | Language(code: "uk", name: "Ukrainian", localizedName: "Українська"), 55 | Language(code: "hr", name: "Croatian", localizedName: "Hrvatski"), 56 | Language(code: "ca", name: "Catalan", localizedName: "Català"), 57 | Language(code: "ro", name: "Romanian", localizedName: "Română"), 58 | Language(code: "he", name: "Hebrew", localizedName: "עברית"), 59 | 60 | // 亚洲语言 61 | Language(code: "ja", name: "Japanese", localizedName: "日本語"), 62 | Language(code: "ko", name: "Korean", localizedName: "한국어"), 63 | Language(code: "th", name: "Thai", localizedName: "ไทย"), 64 | Language(code: "vi", name: "Vietnamese", localizedName: "Tiếng Việt"), 65 | Language(code: "hi", name: "Hindi", localizedName: "हिन्दी"), 66 | Language(code: "bn", name: "Bengali", localizedName: "বাংলা"), 67 | Language(code: "id", name: "Indonesian", localizedName: "Bahasa Indonesia"), 68 | Language(code: "ms", name: "Malay", localizedName: "Bahasa Melayu"), 69 | 70 | // 中东语言 71 | Language(code: "ar", name: "Arabic", localizedName: "العربية"), 72 | Language(code: "ar-SA", name: "Arabic, Saudi Arabia", localizedName: "العربية (السعودية)"), 73 | Language(code: "fa", name: "Persian", localizedName: "فارسی"), 74 | Language(code: "ur", name: "Urdu", localizedName: "اردو"), 75 | 76 | // 其他语言 77 | Language(code: "fil", name: "Filipino", localizedName: "Filipino"), 78 | Language(code: "km", name: "Khmer", localizedName: "ខ្មែរ"), 79 | Language(code: "mn", name: "Mongolian", localizedName: "Монгол"), 80 | Language(code: "my", name: "Burmese", localizedName: "မြန်မာ"), 81 | Language(code: "ne", name: "Nepali", localizedName: "नेपाली"), 82 | Language(code: "si", name: "Sinhala", localizedName: "සිංහල"), 83 | Language(code: "az", name: "Azerbaijani", localizedName: "Azərbaycan"), 84 | Language(code: "kk", name: "Kazakh", localizedName: "Қазақ"), 85 | Language(code: "hy", name: "Armenian", localizedName: "Հայերեն"), 86 | Language(code: "ka", name: "Georgian", localizedName: "ქართული") 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /LanguageTool/Models/LocalizationFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// 本地化文件格式 4 | enum LocalizationFormat { 5 | /// Xcode Strings Catalog (.xcstrings) 6 | case xcstrings 7 | /// Strings File (.strings) 8 | case strings 9 | /// Flutter ARB File (.arb) 10 | case arb 11 | /// Electron JSON File (.json) 12 | case electron 13 | } -------------------------------------------------------------------------------- /LanguageTool/Models/PlatformType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum PlatformType: String, CaseIterable { 4 | case iOS = "iOS" 5 | case flutter = "Flutter" 6 | case electron = "Electron" 7 | 8 | var description: String { 9 | switch self { 10 | case .iOS: 11 | return "iOS (.strings/.xcstrings)" 12 | case .flutter: 13 | return "Flutter (.arb)" 14 | case .electron: 15 | return "Electron (.json)" 16 | } 17 | } 18 | 19 | var fileTypes: [String] { 20 | switch self { 21 | case .iOS: 22 | return ["strings", "xcstrings"] 23 | case .flutter: 24 | return ["arb"] 25 | case .electron: 26 | return ["json"] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /LanguageTool/Models/TranslationManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class TranslationManager { 4 | static let shared = TranslationManager() 5 | 6 | private init() {} 7 | 8 | func parseInputFile(at path: String, platform: PlatformType) async -> [TranslationItem] { 9 | do { 10 | print("Attempting to parse file at path:", path) 11 | print("Selected platform:", platform) 12 | 13 | let fileURL = URL(fileURLWithPath: path) 14 | let data = try Data(contentsOf: fileURL) 15 | print("Successfully read file data, size:", data.count) 16 | 17 | let items: [TranslationItem] 18 | switch platform { 19 | case .iOS: 20 | if path.hasSuffix(".xcstrings") { 21 | print("Parsing as xcstrings file") 22 | items = try await parseXCStrings(data: data) 23 | } else { 24 | print("Parsing as strings file") 25 | items = try await parseStringsFile(data: data) 26 | } 27 | case .electron: 28 | print("Parsing as JSON file") 29 | items = try await parseJsonFile(data: data) 30 | case .flutter: 31 | print("Parsing as ARB file") 32 | items = try await parseArbFile(data: data) 33 | } 34 | 35 | print("Successfully parsed items count:", items.count) 36 | if items.isEmpty { 37 | print("Warning: No items were parsed from the file") 38 | } else { 39 | print("Sample item - Key:", items[0].key) 40 | print("Sample item - Translations:", items[0].translations) 41 | } 42 | 43 | return items 44 | } catch { 45 | print("Error parsing file:", error) 46 | print("Error details:", String(describing: error)) 47 | if let decodingError = error as? DecodingError { 48 | switch decodingError { 49 | case .keyNotFound(let key, let context): 50 | print("Missing key:", key) 51 | print("Context:", context) 52 | case .typeMismatch(let type, let context): 53 | print("Type mismatch:", type) 54 | print("Context:", context) 55 | default: 56 | print("Other decoding error:", decodingError) 57 | } 58 | } 59 | return [] 60 | } 61 | } 62 | 63 | private func parseXCStrings(data: Data) async throws -> [TranslationItem] { 64 | print("Starting parseXCStrings") 65 | let decoder = JSONDecoder() 66 | 67 | // 更新数据结构以匹配实际的 JSON 格式 68 | struct XCStringsContainer: Codable { 69 | struct StringEntry: Codable { 70 | // 源语言的值 71 | let source: Source? 72 | // 注释 73 | let comment: String? 74 | // 翻译 75 | let localizations: [String: Localization]? 76 | 77 | struct Source: Codable { 78 | let stringUnit: StringUnit 79 | } 80 | 81 | struct Localization: Codable { 82 | let stringUnit: StringUnit 83 | } 84 | 85 | struct StringUnit: Codable { 86 | let state: String? 87 | let value: String 88 | } 89 | } 90 | 91 | let sourceLanguage: String 92 | let strings: [String: StringEntry] 93 | let version: String 94 | } 95 | 96 | if let jsonString = String(data: data, encoding: .utf8) { 97 | print("Raw JSON data:", jsonString) 98 | } 99 | 100 | let xcstrings = try decoder.decode(XCStringsContainer.self, from: data) 101 | 102 | print("Parsed sourceLanguage:", xcstrings.sourceLanguage) 103 | print("Number of strings:", xcstrings.strings.count) 104 | print("Available keys:", xcstrings.strings.keys) 105 | 106 | let items = xcstrings.strings.map { key, entry in 107 | var translations: [String: String] = [:] 108 | 109 | // 添加源语言的值 110 | if let sourceValue = entry.source?.stringUnit.value { 111 | translations[xcstrings.sourceLanguage] = sourceValue 112 | } 113 | 114 | // 添加其他语言的翻译 115 | if let localizations = entry.localizations { 116 | for (languageCode, localization) in localizations { 117 | translations[languageCode] = localization.stringUnit.value 118 | } 119 | } 120 | 121 | let item = TranslationItem( 122 | key: key, 123 | translations: translations, 124 | comment: entry.comment ?? "" 125 | ) 126 | print("Created item - Key:", key) 127 | print("Created item - Translations:", translations) 128 | return item 129 | } 130 | 131 | print("Finished parsing, returning \(items.count) items") 132 | return items 133 | } 134 | 135 | private func parseStringsFile(data: Data) async throws -> [TranslationItem] { 136 | guard let content = String(data: data, encoding: .utf8) else { 137 | throw NSError(domain: "TranslationManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid strings file encoding"]) 138 | } 139 | 140 | var translations: [TranslationItem] = [] 141 | let lines = content.components(separatedBy: .newlines) 142 | 143 | for line in lines { 144 | let trimmed = line.trimmingCharacters(in: .whitespaces) 145 | guard !trimmed.isEmpty && !trimmed.hasPrefix("//") else { continue } 146 | 147 | if let match = try? NSRegularExpression(pattern: "\"(.*)\"\\s*=\\s*\"(.*)\";") 148 | .firstMatch(in: trimmed, range: NSRange(trimmed.startIndex..., in: trimmed)) { 149 | 150 | let key = String(trimmed[Range(match.range(at: 1), in: trimmed)!]) 151 | let value = String(trimmed[Range(match.range(at: 2), in: trimmed)!]) 152 | 153 | // 对于 .strings 文件,我们假设它是基础语言(通常是英语) 154 | translations.append(TranslationItem( 155 | key: key, 156 | translations: ["en": value] 157 | )) 158 | } 159 | } 160 | 161 | return translations 162 | } 163 | 164 | private func parseJsonFile(data: Data) async throws -> [TranslationItem] { 165 | let decoder = JSONDecoder() 166 | let json = try decoder.decode([String: String].self, from: data) 167 | 168 | return json.map { key, value in 169 | // 对于 JSON 文件,我们假设它是基础语言 170 | TranslationItem( 171 | key: key, 172 | translations: ["en": value] 173 | ) 174 | } 175 | } 176 | 177 | private func parseArbFile(data: Data) async throws -> [TranslationItem] { 178 | let decoder = JSONDecoder() 179 | let json = try decoder.decode([String: String].self, from: data) 180 | 181 | return json.compactMap { key, value in 182 | guard !key.hasPrefix("@") else { return nil } 183 | // 对于 ARB 文件,我们假设它是基础语言 184 | return TranslationItem( 185 | key: key, 186 | translations: ["en": value] 187 | ) 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /LanguageTool/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/AIService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | protocol AIServiceProtocol { 5 | var baseURL: String { get } 6 | func buildRequestBody(messages: [Message], translationOptions: [String: String]?) -> [String: Any] 7 | func parseResponse(data: Data) throws -> String 8 | } 9 | 10 | struct Message: Codable { 11 | let role: String 12 | let content: String 13 | } 14 | 15 | class AIService { 16 | static let shared = AIService() 17 | 18 | @AppStorage("selectedAIService") private var selectedService: AIServiceType = .deepseek 19 | @AppStorage("geminiApiKey") private var geminiApiKey: String = "" 20 | @AppStorage("aliyunApiKey") private var aliyunApiKey: String = "" 21 | 22 | private var apiKey: String { 23 | AppSettings.shared.apiKey 24 | } 25 | 26 | func sendMessage(messages: [Message], completion: @escaping (Result) -> Void) { 27 | let service: AIServiceProtocol 28 | 29 | switch selectedService { 30 | case .deepseek: 31 | service = DeepSeekService() 32 | case .gemini: 33 | service = GeminiService() 34 | case .aliyun: 35 | service = AliyunService() 36 | } 37 | 38 | sendMessage(messages: messages, service: service, completion: completion) 39 | } 40 | 41 | func translate(text: String, to targetLanguage: String) async throws -> String { 42 | switch selectedService { 43 | case .deepseek: 44 | return try await translateWithDeepseek(text: text, to: targetLanguage) 45 | case .gemini: 46 | return try await translateWithGemini(text: text, to: targetLanguage) 47 | case .aliyun: 48 | return try await makeAliyunRequest(prompt: text, targetLanguage: targetLanguage) 49 | } 50 | } 51 | 52 | /// 批量翻译文本 53 | func batchTranslate(texts: [String], to targetLanguage: String) async throws -> [String] { 54 | // 将所有文本合并成一个字符串,使用特殊分隔符 55 | let separator = "|||" 56 | let combinedText = texts.joined(separator: separator) 57 | 58 | // 生成翻译提示 59 | let prompt = """ 60 | 请将以下文本翻译成\(targetLanguage)。 61 | 每个文本之间使用 ||| 分隔,请保持这个分隔符,只返回翻译结果: 62 | 63 | \(combinedText) 64 | """ 65 | 66 | let messages = [Message(role: "user", content: prompt)] 67 | 68 | // 根据选择的服务创建对应的实例 69 | let service: AIServiceProtocol 70 | let translationOptions: [String: String]? 71 | 72 | switch selectedService { 73 | case .deepseek: 74 | service = DeepSeekService() 75 | translationOptions = nil 76 | case .gemini: 77 | service = GeminiService() 78 | translationOptions = nil 79 | case .aliyun: 80 | service = AliyunService() 81 | translationOptions = [ 82 | "source_lang": "auto", 83 | "target_lang": targetLanguage 84 | ] 85 | } 86 | 87 | // 发送翻译请求 88 | let response = try await withCheckedThrowingContinuation { continuation in 89 | sendMessage(messages: messages, service: service, translationOptions: translationOptions) { result in 90 | switch result { 91 | case .success(let content): 92 | continuation.resume(returning: content) 93 | case .failure(let error): 94 | continuation.resume(throwing: error) 95 | } 96 | } 97 | } 98 | 99 | // 清理并分割翻译结果 100 | let cleanedResponse = response 101 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 102 | .replacingOccurrences(of: "\n", with: " ") 103 | .replacingOccurrences(of: "\r", with: " ") 104 | .replacingOccurrences(of: " ", with: " ") 105 | 106 | let translations = cleanedResponse.components(separatedBy: separator) 107 | .map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } 108 | .filter { !$0.isEmpty } 109 | 110 | // 确保翻译结果数量与原文本数量匹配 111 | guard translations.count == texts.count else { 112 | throw AIError.invalidResponse 113 | } 114 | 115 | return translations 116 | } 117 | 118 | /// 生成翻译提示 119 | // private func generateTranslationPrompt(texts: [String], targetLanguage: String) -> String { 120 | // let numberedTexts = texts.enumerated().map { index, text in 121 | // "\(index + 1). \(text)" 122 | // }.joined(separator: "\n") 123 | // 124 | // return """ 125 | // 请将以下文本翻译成\(targetLanguage)语言。 126 | // 只需返回翻译结果,每行一个翻译,保持原有的编号顺序: 127 | // 128 | // \(numberedTexts) 129 | // """ 130 | // } 131 | 132 | /// 解析翻译结果 133 | // private func parseTranslations(from response: String) -> [String] { 134 | // // 移除可能的序号和额外标记 135 | // let lines = response 136 | // .components(separatedBy: .newlines) 137 | // .map { line -> String in 138 | // var cleaned = line 139 | // .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 140 | // .replacingOccurrences(of: "^\\d+\\.\\s*", with: "", options: .regularExpression) 141 | // .replacingOccurrences(of: "^-\\s*", with: "", options: .regularExpression) 142 | // 143 | // // 如果翻译文本被引号包围,移除引号 144 | // if cleaned.hasPrefix("\"") && cleaned.hasSuffix("\"") { 145 | // cleaned = String(cleaned.dropFirst().dropLast()) 146 | // } 147 | // 148 | // return cleaned 149 | // } 150 | // .filter { !$0.isEmpty } 151 | // 152 | // return lines 153 | // } 154 | 155 | // 原有的 DeepSeek 翻译方法 156 | private func translateWithDeepseek(text: String, to targetLanguage: String) async throws -> String { 157 | let message = Message(role: "system", 158 | content: "将以下文本翻译成\(targetLanguage)语言,只需要返回翻译结果,不需要任何解释:\n\(text)") 159 | 160 | return try await withCheckedThrowingContinuation { continuation in 161 | sendMessage(messages: [message]) { result in 162 | switch result { 163 | case .success(let translation): 164 | continuation.resume(returning: translation.trimmingCharacters(in: .whitespacesAndNewlines)) 165 | case .failure(let error): 166 | continuation.resume(throwing: error) 167 | } 168 | } 169 | } 170 | } 171 | 172 | // 新增的 Gemini 翻译方法 173 | private func translateWithGemini(text: String, to targetLanguage: String) async throws -> String { 174 | let urlString = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=\(geminiApiKey)" 175 | guard let url = URL(string: urlString) else { 176 | throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]) 177 | } 178 | 179 | let prompt = "Translate the following text to \(targetLanguage). Only return the translation, no explanations: \(text)" 180 | 181 | let requestBody: [String: Any] = [ 182 | "contents": [ 183 | [ 184 | "parts": [ 185 | [ 186 | "text": prompt 187 | ] 188 | ] 189 | ] 190 | ] 191 | ] 192 | 193 | var request = URLRequest(url: url) 194 | request.httpMethod = "POST" 195 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 196 | request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) 197 | 198 | let (data, _) = try await URLSession.shared.data(for: request) 199 | 200 | if let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any], 201 | let candidates = jsonResponse["candidates"] as? [[String: Any]], 202 | let firstCandidate = candidates.first, 203 | let content = firstCandidate["content"] as? [String: Any], 204 | let parts = content["parts"] as? [[String: Any]], 205 | let firstPart = parts.first, 206 | let translation = firstPart["text"] as? String { 207 | return translation 208 | } 209 | 210 | throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to parse response"]) 211 | } 212 | 213 | /// 测试 Gemini API 连接 214 | // func testGemini() async throws { 215 | // let apiKey = "YOUR_API_KEY" // 替换为实际的 API key 216 | // let urlString = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=\(apiKey)" 217 | // let url = URL(string: urlString)! 218 | // 219 | // // 构建请求体 220 | // let requestBody: [String: Any] = [ 221 | // "contents": [ 222 | // [ 223 | // "parts": [ 224 | // [ 225 | // "text": "Hello, this is a test message." 226 | // ] 227 | // ] 228 | // ] 229 | // ] 230 | // ] 231 | // 232 | // var request = URLRequest(url: url) 233 | // request.httpMethod = "POST" 234 | // request.setValue("application/json", forHTTPHeaderField: "Content-Type") 235 | // request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) 236 | // 237 | // print("开始测试 Gemini API...") 238 | // print("请求 URL: \(urlString)") 239 | // print("请求体: \(requestBody)") 240 | // 241 | // do { 242 | // let (data, response) = try await URLSession.shared.data(for: request) 243 | // 244 | // if let httpResponse = response as? HTTPURLResponse { 245 | // print("响应状态码: \(httpResponse.statusCode)") 246 | // } 247 | // 248 | // if let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] { 249 | // print("响应数据: \(jsonResponse)") 250 | // } 251 | // 252 | // print("API 测试成功") 253 | // } catch { 254 | // print("API 测试失败: \(error.localizedDescription)") 255 | // throw error 256 | // } 257 | // } 258 | 259 | private func getApiKey() -> String { 260 | var apiKeyToUse = "" 261 | switch selectedService { 262 | case .deepseek: 263 | apiKeyToUse = apiKey 264 | case .gemini: 265 | apiKeyToUse = geminiApiKey 266 | case .aliyun: 267 | apiKeyToUse = aliyunApiKey 268 | } 269 | return apiKeyToUse 270 | } 271 | 272 | private func makeAliyunRequest(prompt: String, targetLanguage: String) async throws -> String { 273 | let messages = [Message(role: "user", content: prompt)] 274 | let translationOptions = [ 275 | "source_lang": "auto", 276 | "target_lang": targetLanguage 277 | ] 278 | 279 | let service = AliyunService() 280 | let requestBody = service.buildRequestBody(messages: messages, translationOptions: translationOptions) 281 | 282 | let url = URL(string: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions")! 283 | var request = URLRequest(url: url) 284 | request.httpMethod = "POST" 285 | request.setValue("Bearer \(aliyunApiKey)", forHTTPHeaderField: "Authorization") 286 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 287 | 288 | request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) 289 | 290 | let (data, response) = try await URLSession.shared.data(for: request) 291 | 292 | guard let httpResponse = response as? HTTPURLResponse else { 293 | throw AIError.invalidResponse 294 | } 295 | 296 | guard httpResponse.statusCode == 200 else { 297 | throw AIError.apiError("HTTP Status: \(httpResponse.statusCode)") 298 | } 299 | 300 | guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], 301 | let choices = json["choices"] as? [[String: Any]], 302 | let firstChoice = choices.first, 303 | let message = firstChoice["message"] as? [String: Any], 304 | let content = message["content"] as? String else { 305 | throw AIError.invalidResponse 306 | } 307 | 308 | return content 309 | } 310 | } 311 | 312 | 313 | // 扩展 AIService 以实现协议 314 | extension AIService { 315 | func sendMessage(messages: [Message], service: T, translationOptions: [String: String]? = nil, completion: @escaping (Result) -> Void) { 316 | let apiKeyToUse: String 317 | switch selectedService { 318 | case .deepseek: 319 | apiKeyToUse = apiKey 320 | case .gemini: 321 | apiKeyToUse = geminiApiKey 322 | case .aliyun: 323 | apiKeyToUse = aliyunApiKey 324 | } 325 | 326 | guard !apiKeyToUse.isEmpty else { 327 | completion(.failure(.invalidConfiguration("未设置 API Key"))) 328 | return 329 | } 330 | 331 | print("🔑 使用的 API Key: \(apiKeyToUse)") // 打印 API Key(注意:在生产环境中请勿打印敏感信息) 332 | 333 | let urlString: String 334 | switch selectedService { 335 | case .deepseek: 336 | urlString = service.baseURL 337 | case .gemini: 338 | urlString = service.baseURL + "?key=\(apiKeyToUse)" 339 | case .aliyun: 340 | urlString = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" 341 | } 342 | 343 | guard let url = URL(string: urlString) else { 344 | completion(.failure(.invalidURL)) 345 | return 346 | } 347 | // 打印完整的请求 URL 348 | print("🔗 请求的完整 URL: \(url.absoluteString)") 349 | print("📝 准备发送的消息内容: \(messages)") 350 | 351 | var request = URLRequest(url: url) 352 | request.httpMethod = "POST" 353 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 354 | 355 | // 修改认证头设置 356 | switch selectedService { 357 | case .deepseek: 358 | request.setValue("Bearer \(apiKeyToUse)", forHTTPHeaderField: "Authorization") 359 | case .gemini: 360 | // Gemini 不需要认证头,因为 API key 已经在 URL 中了 361 | break 362 | case .aliyun: 363 | request.setValue("Bearer \(aliyunApiKey)", forHTTPHeaderField: "Authorization") 364 | } 365 | 366 | let body = service.buildRequestBody(messages: messages, translationOptions: translationOptions) 367 | 368 | guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else { 369 | completion(.failure(.jsonError(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "JSON 序列化失败"])))) 370 | return 371 | } 372 | 373 | request.httpBody = jsonData 374 | print("📤 发送请求体: \(String(data: jsonData, encoding: .utf8) ?? "")") 375 | 376 | let task = URLSession.shared.dataTask(with: request) { data, response, error in 377 | if let error = error { 378 | print("❌ 网络错误: \(error.localizedDescription)") 379 | completion(.failure(.networkError(error))) 380 | return 381 | } 382 | 383 | guard let httpResponse = response as? HTTPURLResponse else { 384 | completion(.failure(.invalidResponse)) 385 | return 386 | } 387 | 388 | print("📡 HTTP 状态码: \(httpResponse.statusCode)") // 打印状态码 389 | 390 | guard (200...299).contains(httpResponse.statusCode) else { 391 | print("❌ 无效的响应状态码: \(httpResponse.statusCode)") 392 | completion(.failure(.invalidResponse)) 393 | return 394 | } 395 | 396 | guard let data = data else { 397 | completion(.failure(.invalidResponse)) 398 | return 399 | } 400 | 401 | print("📥 收到响应数据: \(String(data: data, encoding: .utf8) ?? "")") 402 | 403 | do { 404 | let responseText = try service.parseResponse(data: data) 405 | DispatchQueue.main.async { 406 | completion(.success(responseText)) 407 | } 408 | } catch { 409 | print("❌ JSON 解析错误: \(error.localizedDescription)") 410 | completion(.failure(.jsonError(error))) 411 | } 412 | } 413 | 414 | task.resume() 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/ARBFileHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ARBFileHandler { 4 | /// ARB 文件中的占位符信息 5 | struct Placeholder: Codable { 6 | let type: String 7 | let example: String? 8 | let format: String? 9 | } 10 | 11 | /// ARB 文件中的元数据信息 12 | struct MetadataInfo: Codable { 13 | let description: String? 14 | let placeholders: [String: Placeholder]? 15 | } 16 | 17 | /// 从 ARB 文件中提取需要翻译的文本 18 | static func extractTranslatableContent(from arbData: [String: Any]) -> [String] { 19 | var translatableContent: [String] = [] 20 | 21 | func extractFromValue(_ value: Any) { 22 | if let stringValue = value as? String { 23 | // 检查是否包含占位符或复数形式 24 | if !stringValue.isEmpty { 25 | translatableContent.append(stringValue) 26 | } 27 | } else if let dict = value as? [String: Any] { 28 | // 处理嵌套字典 29 | for (key, nestedValue) in dict { 30 | if key == "description" { 31 | // 提取 description 字段进行翻译 32 | if let description = nestedValue as? String { 33 | translatableContent.append(description) 34 | } 35 | } else if !key.hasPrefix("@") { 36 | extractFromValue(nestedValue) 37 | } 38 | } 39 | } 40 | } 41 | 42 | for (key, value) in arbData { 43 | // 处理元数据中的 description 44 | if key.hasPrefix("@") && key != "@@locale" { 45 | if let metaDict = value as? [String: Any] { 46 | if let description = metaDict["description"] as? String { 47 | translatableContent.append(description) 48 | } else if let nestedMeta = metaDict as? [String: [String: Any]] { 49 | // 处理嵌套元数据(如 @settings 中的多个 description) 50 | for (_, subMeta) in nestedMeta { 51 | if let description = subMeta["description"] as? String { 52 | translatableContent.append(description) 53 | } 54 | } 55 | } 56 | } 57 | continue 58 | } 59 | 60 | // 跳过 @@locale 61 | if key == "@@locale" { 62 | continue 63 | } 64 | 65 | extractFromValue(value) 66 | } 67 | 68 | return translatableContent 69 | } 70 | 71 | /// 生成目标语言的 ARB 文件 72 | static func generateARBFile(originalData: [String: Any], translations: [String], targetLanguage: String) -> [String: Any] { 73 | var resultARB: [String: Any] = [:] 74 | resultARB["@@locale"] = targetLanguage 75 | 76 | var translationIndex = 0 77 | 78 | func translateValue(_ value: Any) -> Any { 79 | if let stringValue = value as? String { 80 | if !stringValue.isEmpty && translationIndex < translations.count { 81 | let translation = translations[translationIndex] 82 | translationIndex += 1 83 | return translation 84 | } 85 | return stringValue 86 | } else if let dict = value as? [String: Any] { 87 | var translatedDict: [String: Any] = [:] 88 | for (key, nestedValue) in dict { 89 | if key == "description" { 90 | // 翻译 description 字段 91 | if let _ = nestedValue as? String, translationIndex < translations.count { 92 | translatedDict[key] = translations[translationIndex] 93 | translationIndex += 1 94 | } else { 95 | translatedDict[key] = nestedValue 96 | } 97 | } else if key.hasPrefix("@") { 98 | // 保持其他元数据不变 99 | translatedDict[key] = nestedValue 100 | } else { 101 | translatedDict[key] = translateValue(nestedValue) 102 | } 103 | } 104 | return translatedDict 105 | } 106 | return value 107 | } 108 | 109 | for (key, value) in originalData { 110 | if key.hasPrefix("@") && key != "@@locale" { 111 | // 处理元数据 112 | if var metaDict = value as? [String: Any] { 113 | if let _ = metaDict["description"] as? String, translationIndex < translations.count { 114 | metaDict["description"] = translations[translationIndex] 115 | translationIndex += 1 116 | } else if var nestedMeta = metaDict as? [String: [String: Any]] { 117 | // 处理嵌套元数据 118 | for (subKey, var subMeta) in nestedMeta { 119 | if let _ = subMeta["description"] as? String, translationIndex < translations.count { 120 | subMeta["description"] = translations[translationIndex] 121 | translationIndex += 1 122 | } 123 | nestedMeta[subKey] = subMeta 124 | } 125 | metaDict = nestedMeta 126 | } 127 | resultARB[key] = metaDict 128 | } 129 | } else if key == "@@locale" { 130 | resultARB[key] = targetLanguage 131 | } else { 132 | resultARB[key] = translateValue(value) 133 | } 134 | } 135 | 136 | return resultARB 137 | } 138 | 139 | /// 处理 ARB 文件转换 140 | static func processARBFile(from inputPath: String, 141 | to outputPath: String, 142 | languages: [String]) async -> Result { 143 | do { 144 | // 读取原始 ARB 文件 145 | let inputURL = URL(fileURLWithPath: inputPath) 146 | let arbData = try Data(contentsOf: inputURL) 147 | guard let originalDict = try JSONSerialization.jsonObject(with: arbData) as? [String: Any] else { 148 | return .failure(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid ARB file format"])) 149 | } 150 | 151 | // 提取需要翻译的文本 152 | let translatableContent = extractTranslatableContent(from: originalDict) 153 | 154 | // 创建输出目录 155 | let outputURL = URL(fileURLWithPath: outputPath) 156 | try FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil) 157 | 158 | // 为每种语言生成翻译 159 | for language in languages { 160 | print("正在处理语言: \(language)") 161 | 162 | // 使用 AIService 进行批量翻译 163 | let translations = try await AIService.shared.batchTranslate( 164 | texts: translatableContent, 165 | to: language 166 | ) 167 | 168 | // 生成目标语言的 ARB 文件 169 | let translatedARB = generateARBFile( 170 | originalData: originalDict, 171 | translations: translations, 172 | targetLanguage: language 173 | ) 174 | 175 | // 保存翻译后的 ARB 文件 176 | let languageFileName = "app_\(language).arb" 177 | let languageFileURL = outputURL.appendingPathComponent(languageFileName) 178 | 179 | let jsonData = try JSONSerialization.data(withJSONObject: translatedARB, options: [.prettyPrinted]) 180 | try jsonData.write(to: languageFileURL) 181 | 182 | print("✅ 已生成 \(language) 的 ARB 文件") 183 | } 184 | 185 | return .success("Successfully generated ARB files for all languages".localized) 186 | } catch { 187 | return .failure(error) 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/AliyunService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class AliyunService: AIServiceProtocol { 4 | var baseURL: String { 5 | return "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions" 6 | } 7 | 8 | func buildRequestBody(messages: [Message], translationOptions: [String: String]? = nil) -> [String: Any] { 9 | var body: [String: Any] = [ 10 | "model": "qwen-mt-turbo", 11 | "messages": messages.map { [ 12 | "role": $0.role, 13 | "content": $0.content 14 | ]} 15 | ] 16 | 17 | if let options = translationOptions { 18 | body["translation_options"] = [ 19 | "source_lang": options["source_lang"] ?? "auto", 20 | "target_lang": options["target_lang"] ?? "English" 21 | ] 22 | } 23 | 24 | return body 25 | } 26 | 27 | func parseResponse(data: Data) throws -> String { 28 | let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] 29 | 30 | guard let choices = json?["choices"] as? [[String: Any]], 31 | let firstChoice = choices.first, 32 | let message = firstChoice["message"] as? [String: Any], 33 | let content = message["content"] as? String else { 34 | throw AIError.invalidResponse 35 | } 36 | 37 | // 寻找最后一个换行符后的内容 38 | if let lastNewlineRange = content.range(of: "\n\n", options: .backwards) { 39 | let translationResult = content[lastNewlineRange.upperBound...] 40 | .trimmingCharacters(in: .whitespacesAndNewlines) 41 | return translationResult 42 | } 43 | 44 | return content 45 | } 46 | } -------------------------------------------------------------------------------- /LanguageTool/Utilities/DeepSeekService.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DeepSeekService: AIServiceProtocol { 4 | var baseURL: String { 5 | return "https://api.deepseek.com/v1/chat/completions" 6 | } 7 | 8 | func buildRequestBody(messages: [Message], translationOptions: [String: String]? = nil) -> [String: Any] { 9 | return [ 10 | "model": "deepseek-chat", 11 | "messages": messages.map { ["role": $0.role, "content": $0.content] } 12 | ] 13 | } 14 | 15 | func parseResponse(data: Data) throws -> String { 16 | let jsonDict = try JSONSerialization.jsonObject(with: data) as? [String: Any] 17 | 18 | // 检查错误响应 19 | if let error = jsonDict?["error"] as? [String: Any], 20 | let message = error["message"] as? String { 21 | if message.contains("rate limit") { 22 | throw AIError.rateLimitExceeded 23 | } else if message.contains("invalid api key") { 24 | throw AIError.unauthorized 25 | } 26 | throw AIError.apiError(message) 27 | } 28 | 29 | // 解析正常响应 30 | if let choices = jsonDict?["choices"] as? [[String: Any]], 31 | let firstChoice = choices.first, 32 | let message = firstChoice["message"] as? [String: Any], 33 | let content = message["content"] as? String { 34 | return content 35 | } 36 | throw AIError.invalidResponse 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/ElectronLocalizationHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ElectronLocalizationHandler { 4 | /// 从 Electron 本地化 JSON 文件中提取需要翻译的文本 5 | static func extractTranslatableContent(from jsonData: [String: Any]) -> [String] { 6 | var translatableContent: [String] = [] 7 | 8 | for (_, value) in jsonData { 9 | if let stringValue = value as? String { 10 | translatableContent.append(stringValue) 11 | } 12 | } 13 | 14 | return translatableContent 15 | } 16 | 17 | /// 生成目标语言的 JSON 文件 18 | static func generateLocalizationFile(originalData: [String: Any], translations: [String]) -> [String: Any] { 19 | var resultDict: [String: Any] = [:] 20 | var translationIndex = 0 21 | 22 | for (key, _) in originalData { 23 | if translationIndex < translations.count { 24 | resultDict[key] = translations[translationIndex] 25 | translationIndex += 1 26 | } 27 | } 28 | 29 | return resultDict 30 | } 31 | 32 | /// 处理 Electron 本地化文件转换 33 | static func processLocalizationFile( 34 | from inputPath: String, 35 | to outputPath: String, 36 | languages: [String] 37 | ) async -> Result { 38 | do { 39 | // 读取原始 JSON 文件 40 | let inputURL = URL(fileURLWithPath: inputPath) 41 | let jsonData = try Data(contentsOf: inputURL) 42 | guard let originalDict = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { 43 | return .failure(NSError(domain: "", code: -1, 44 | userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])) 45 | } 46 | 47 | // 提取需要翻译的文本 48 | let translatableContent = extractTranslatableContent(from: originalDict) 49 | 50 | // 创建输出目录 51 | let outputURL = URL(fileURLWithPath: outputPath) 52 | try FileManager.default.createDirectory( 53 | at: outputURL, 54 | withIntermediateDirectories: true, 55 | attributes: nil 56 | ) 57 | 58 | // 为每种语言生成翻译 59 | for language in languages { 60 | print("正在处理语言: \(language)") 61 | 62 | // 使用 AIService 进行批量翻译 63 | let translations = try await AIService.shared.batchTranslate( 64 | texts: translatableContent, 65 | to: language 66 | ) 67 | 68 | // 生成目标语言的 JSON 文件 69 | let translatedJSON = generateLocalizationFile( 70 | originalData: originalDict, 71 | translations: translations 72 | ) 73 | 74 | // 保存翻译后的 JSON 文件 75 | let languageFileName = "locale-\(language).json" 76 | let languageFileURL = outputURL.appendingPathComponent(languageFileName) 77 | 78 | let jsonData = try JSONSerialization.data( 79 | withJSONObject: translatedJSON, 80 | options: [.prettyPrinted] 81 | ) 82 | try jsonData.write(to: languageFileURL) 83 | 84 | print("✅ 已生成 \(language) 的本地化文件") 85 | } 86 | 87 | return .success("Successfully generated localized files for all languages".localized) 88 | } catch { 89 | return .failure(error) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/GeminiService.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GeminiService: AIServiceProtocol { 4 | var baseURL: String { 5 | return "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent" 6 | } 7 | 8 | func buildRequestBody(messages: [Message], translationOptions: [String: String]? = nil) -> [String: Any] { 9 | return [ 10 | "contents": [ 11 | [ 12 | "parts": [ 13 | [ 14 | "text": messages.last?.content ?? "" 15 | ] 16 | ] 17 | ] 18 | ] 19 | ] 20 | } 21 | 22 | func parseResponse(data: Data) throws -> String { 23 | let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] 24 | 25 | // 检查错误响应 26 | if let error = jsonResponse?["error"] as? [String: Any], 27 | let message = error["message"] as? String { 28 | if message.contains("quota") { 29 | throw AIError.rateLimitExceeded 30 | } else if message.contains("API key") || message.contains("authentication") { 31 | throw AIError.unauthorized 32 | } 33 | throw AIError.apiError(message) 34 | } 35 | 36 | // 解析正常响应 37 | if let candidates = jsonResponse?["candidates"] as? [[String: Any]], 38 | let firstCandidate = candidates.first, 39 | let content = firstCandidate["content"] as? [String: Any], 40 | let parts = content["parts"] as? [[String: Any]], 41 | let firstPart = parts.first, 42 | let text = firstPart["text"] as? String { 43 | return text 44 | } 45 | throw AIError.invalidResponse 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/JsonUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class JsonUtils { 4 | /// 从 JSON 文件中提取键为中文字符串的键,删除重复项并写入 TXT 文件。 5 | /// 6 | /// - Parameters: 7 | /// - jsonFilePath: JSON 文件路径。 8 | /// - outputFilePath: 输出 TXT 文件路径。 9 | static func extractChineseKeys(from jsonFilePath: String, to outputFilePath: String) { 10 | guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: jsonFilePath)) else { 11 | print("错误:文件 \(jsonFilePath) 未找到。") 12 | return 13 | } 14 | 15 | guard let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) else { 16 | print("错误:文件 \(jsonFilePath) 不是有效的 JSON 格式。") 17 | return 18 | } 19 | 20 | var chineseKeys = Set() 21 | 22 | func extractKeys(from object: Any) { 23 | if let dictionary = object as? [String: Any] { 24 | for (key, value) in dictionary { 25 | if key.range(of: "\\p{Han}", options: .regularExpression) != nil { 26 | chineseKeys.insert(key) 27 | } 28 | extractKeys(from: value) 29 | } 30 | } else if let array = object as? [Any] { 31 | for item in array { 32 | extractKeys(from: item) 33 | } 34 | } 35 | } 36 | 37 | extractKeys(from: jsonObject) 38 | 39 | let keysArray = Array(chineseKeys) 40 | 41 | do { 42 | try keysArray.joined(separator: "\n").write(toFile: outputFilePath, atomically: true, encoding: .utf8) 43 | print("成功提取 \(chineseKeys.count) 个中文键并写入 \(outputFilePath)。") 44 | } catch { 45 | print("写入文件 \(outputFilePath) 出错: \(error)") 46 | } 47 | } 48 | 49 | /// 新增方法:提取中文并返回字符串数组 50 | static func extractChineseKeysAsArray(from inputFilePath: String) -> [String]? { 51 | do { 52 | guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: inputFilePath)), 53 | let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) else { 54 | print("❌ JSON 文件读取或解析失败") 55 | return nil 56 | } 57 | 58 | var chineseKeys = Set() 59 | 60 | func extractKeys(from object: Any) { 61 | if let dictionary = object as? [String: Any] { 62 | for (key, value) in dictionary { 63 | if key.range(of: "\\p{Han}", options: .regularExpression) != nil { 64 | chineseKeys.insert(key) 65 | } 66 | extractKeys(from: value) 67 | } 68 | } else if let array = object as? [Any] { 69 | for item in array { 70 | extractKeys(from: item) 71 | } 72 | } 73 | } 74 | 75 | extractKeys(from: jsonObject) 76 | print("✅ 成功提取 \(chineseKeys.count) 个中文键") 77 | return Array(chineseKeys) 78 | 79 | } catch { 80 | print("❌ 处理失败: \(error)") 81 | return nil 82 | } 83 | } 84 | 85 | /// 从 JSON 文件中提取所有需要翻译的值和源语言 86 | static func extractValuesFromXCStrings(from inputFilePath: String) -> (values: [String], sourceLanguage: String)? { 87 | do { 88 | guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: inputFilePath)), 89 | let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any], 90 | let strings = jsonObject["strings"] as? [String: Any], 91 | let sourceLanguage = jsonObject["sourceLanguage"] as? String else { 92 | print("❌ JSON 文件读取或解析失败") 93 | return nil 94 | } 95 | 96 | var values = Set() 97 | 98 | // 遍历 strings 下的所有条目 99 | for (_, entry) in strings { 100 | if let entryDict = entry as? [String: Any], 101 | let localizations = entryDict["localizations"] as? [String: Any], 102 | let sourceLocalization = localizations[sourceLanguage] as? [String: Any], 103 | let stringUnit = sourceLocalization["stringUnit"] as? [String: Any], 104 | let value = stringUnit["value"] as? String { 105 | values.insert(value) 106 | } 107 | } 108 | 109 | print("✅ 成功提取 \(values.count) 个待翻译值") 110 | return (Array(values), sourceLanguage) 111 | } catch { 112 | print("❌ 处理失败: \(error)") 113 | return nil 114 | } 115 | } 116 | 117 | /// 从JSON文件中提取值并生成本地化文件 118 | static func convertToLocalizationFile(from inputPath: String, to outputPath: String, languages: [String]) async -> (success: Bool, message: String) { 119 | guard let extractedData = extractValuesFromXCStrings(from: inputPath) else { 120 | return (false, "❌ 提取待翻译值失败") 121 | } 122 | 123 | guard let jsonData = await LocalizationJSONGenerator.generateJSON( 124 | for: extractedData.values, 125 | languages: languages, 126 | sourceLanguage: extractedData.sourceLanguage 127 | ) else { 128 | return (false, "❌ 生成 JSON 失败") 129 | } 130 | 131 | do { 132 | try jsonData.write(to: URL(fileURLWithPath: outputPath)) 133 | return (true, "Successfully generated localized JSON file containing \(extractedData.values.count) translation items".localized) 134 | } catch { 135 | return (false, "❌ 写入文件失败: \(error.localizedDescription)") 136 | } 137 | } 138 | 139 | /// 从JSON文件中提取中文键并生成文本文件 140 | static func extractChineseKeysToFile(from inputPath: String, to outputPath: String) -> (success: Bool, message: String) { 141 | do { 142 | guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: inputPath)) else { 143 | return (false, "错误:文件未找到") 144 | } 145 | 146 | guard let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) else { 147 | return (false, "错误:不是有效的 JSON 格式") 148 | } 149 | 150 | var chineseKeys = Set() 151 | 152 | func extractKeys(from object: Any) { 153 | if let dictionary = object as? [String: Any] { 154 | for (key, value) in dictionary { 155 | if key.range(of: "\\p{Han}", options: .regularExpression) != nil { 156 | chineseKeys.insert(key) 157 | } 158 | extractKeys(from: value) 159 | } 160 | } else if let array = object as? [Any] { 161 | for item in array { 162 | extractKeys(from: item) 163 | } 164 | } 165 | } 166 | 167 | extractKeys(from: jsonObject) 168 | let keysArray = Array(chineseKeys) 169 | 170 | try keysArray.joined(separator: "\n").write(toFile: outputPath, atomically: true, encoding: .utf8) 171 | return (true, "✅ 成功提取 \(chineseKeys.count) 个中文键并写入文件") 172 | } catch { 173 | return (false, "❌ 转换失败:\(error.localizedDescription)") 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/LocalizationJSONGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | 4 | class LocalizationJSONGenerator { 5 | static func generateJSON(for keys: [String], languages: [String], sourceLanguage: String) async -> Data? { 6 | var localizationData: [String: Any] = [ 7 | "version": "1.0", 8 | "sourceLanguage": sourceLanguage, 9 | "strings": [:] 10 | ] 11 | 12 | var stringsDict: [String: Any] = [:] 13 | 14 | // 语言名称映射 15 | let languageNames = [ 16 | "en": "English", 17 | "zh-Hans": "Simplified Chinese", 18 | "zh-Hant": "Traditional Chinese", 19 | "ja": "Japanese", 20 | "ko": "Korean", 21 | "es": "Spanish", 22 | "fr": "French", 23 | "de": "German" 24 | ] 25 | 26 | // 为每种语言批量翻译所有键 27 | for language in languages { 28 | do { 29 | // 使用优化后的批量翻译方法 30 | print("Starting batch translation [\(language)]...") 31 | let translations = try await AIService.shared.batchTranslate( 32 | texts: keys, 33 | to: languageNames[language] ?? language 34 | ) 35 | 36 | // 将翻译结果添加到字典中 37 | for (index, key) in keys.enumerated() { 38 | if stringsDict[key] == nil { 39 | stringsDict[key] = ["localizations": [:]] 40 | } 41 | if var localizations = stringsDict[key] as? [String: Any], 42 | var localizationsDict = localizations["localizations"] as? [String: Any], 43 | index < translations.count { 44 | localizationsDict[language] = [ 45 | "stringUnit": [ 46 | "state": "translated", 47 | "value": translations[index] 48 | ] 49 | ] 50 | localizations["localizations"] = localizationsDict 51 | stringsDict[key] = localizations 52 | } 53 | } 54 | 55 | print("✅ Batch translation successful [\(language)]: \(keys.count) entries") 56 | } catch { 57 | print("❌ Batch translation failed [\(language)]: \(error.localizedDescription)") 58 | // 翻译失败时为所有键设置空值 59 | for key in keys { 60 | if stringsDict[key] == nil { 61 | stringsDict[key] = ["localizations": [:]] 62 | } 63 | if var localizations = stringsDict[key] as? [String: Any], 64 | var localizationsDict = localizations["localizations"] as? [String: Any] { 65 | localizationsDict[language] = [ 66 | "stringUnit": [ 67 | "state": "needs_review", 68 | "value": "" 69 | ] 70 | ] 71 | localizations["localizations"] = localizationsDict 72 | stringsDict[key] = localizations 73 | } 74 | } 75 | } 76 | } 77 | 78 | localizationData["strings"] = stringsDict 79 | 80 | do { 81 | return try JSONSerialization.data(withJSONObject: localizationData, options: [.prettyPrinted, .sortedKeys]) 82 | } catch { 83 | print("❌ 生成 JSON 失败: \(error)") 84 | return nil 85 | } 86 | } 87 | 88 | static func saveJSONToFile(data: Data?, fileName: String) { // 修改参数为文件名 89 | guard let data = data, let jsonString = String(data: data, encoding: .utf8) else { 90 | print("Invalid JSON data") 91 | return 92 | } 93 | 94 | // 获取 Documents 目录的路径 95 | guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { 96 | print("Could not access Documents directory") 97 | return 98 | } 99 | 100 | // 创建文件路径 101 | let filePath = documentsDirectory.appendingPathComponent(fileName).path 102 | 103 | do { 104 | try jsonString.write(toFile: filePath, atomically: true, encoding: .utf8) 105 | print("JSON file saved to \(filePath)") 106 | } catch { 107 | print("Error writing JSON to file: \(error)") 108 | } 109 | } 110 | 111 | /// 选择保存路径保存 json 文件 112 | /// - Parameter data: 待保存的数据 113 | static func saveJSONToFile(data: Data?) { 114 | guard let data = data, let jsonString = String(data: data, encoding: .utf8) else { 115 | print("Invalid JSON data") 116 | return 117 | } 118 | 119 | let savePanel = NSSavePanel() 120 | savePanel.canCreateDirectories = true // 允许用户创建文件夹 121 | savePanel.title = "Save JSON File" // 设置窗口标题 122 | 123 | let dateFormatter = DateFormatter() 124 | dateFormatter.dateFormat = "yyyyMMdd_HHmmss" 125 | let fileName = "\(dateFormatter.string(from: Date())).json" 126 | 127 | savePanel.nameFieldStringValue = fileName // 设置默认文件名 128 | 129 | // 显示保存面板 130 | savePanel.begin { (result) in 131 | if result == .OK { 132 | guard let url = savePanel.url else { 133 | print("No URL selected") 134 | return 135 | } 136 | 137 | do { 138 | try jsonString.write(to: url, atomically: true, encoding: .utf8) 139 | print("JSON file saved to \(url)") 140 | } catch { 141 | print("Error writing JSON to file: \(error)") 142 | } 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/LocalizationManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class LocalizationManager { 4 | static let shared = LocalizationManager() 5 | 6 | private var bundle: Bundle? 7 | 8 | init() { 9 | // 初始化时加载当前语言的 Bundle 10 | bundle = Bundle.main 11 | } 12 | 13 | func localizedString(for key: String) -> String { 14 | return bundle?.localizedString(forKey: key, value: nil, table: nil) ?? key 15 | } 16 | 17 | func setLanguage(_ language: String) { 18 | // 更新语言设置 19 | UserDefaults.standard.set([language], forKey: "AppleLanguages") 20 | UserDefaults.standard.synchronize() 21 | 22 | // 重新加载 Bundle 23 | if let languagePath = Bundle.main.path(forResource: language, ofType: "lproj"), 24 | let bundle = Bundle(path: languagePath) { 25 | self.bundle = bundle 26 | } else { 27 | self.bundle = Bundle.main 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LanguageTool/Utilities/StringsFileParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class StringsFileParser { 4 | /// 从 .strings 文件中提取键值对 5 | static func parseStringsFile(at path: String) -> Result<[String: String], Error> { 6 | do { 7 | let content = try String(contentsOfFile: path, encoding: .utf8) 8 | var result: [String: String] = [:] 9 | 10 | // 按行分割 11 | let lines = content.components(separatedBy: .newlines) 12 | 13 | for line in lines { 14 | let trimmed = line.trimmingCharacters(in: .whitespaces) 15 | // 跳过注释和空行 16 | if trimmed.isEmpty || trimmed.hasPrefix("//") || trimmed.hasPrefix("/*") { 17 | continue 18 | } 19 | 20 | // 匹配 "key" = "value"; 格式 21 | if let regex = try? NSRegularExpression(pattern: "\"(.+?)\"\\s*=\\s*\"(.+?)\";") { 22 | let nsRange = NSRange(trimmed.startIndex.. Result { 42 | var content = "/* Generated by Language Tool */\n\n" 43 | 44 | // 按键排序以保持一致性 45 | let sortedKeys = translations.keys.sorted() 46 | 47 | for key in sortedKeys { 48 | if let value = translations[key] { 49 | // 处理特殊字符 50 | let escapedKey = key.replacingOccurrences(of: "\"", with: "\\\"") 51 | let escapedValue = value.replacingOccurrences(of: "\"", with: "\\\"") 52 | content += "\"\(escapedKey)\" = \"\(escapedValue)\";\n" 53 | } 54 | } 55 | 56 | do { 57 | try content.write(to: URL(fileURLWithPath: path), atomically: true, encoding: .utf8) 58 | return .success(()) 59 | } catch { 60 | return .failure(error) 61 | } 62 | } 63 | 64 | /// 将 .strings 格式转换为 .xcstrings 格式 65 | static func convertToXCStrings(translations: [String: String], languages: [String]) async -> Result { 66 | var xcstringsDict: [String: Any] = [ 67 | "version": "1.0", 68 | "sourceLanguage": "zh-Hans", 69 | "strings": [:] as [String: Any] 70 | ] 71 | 72 | var stringsDict: [String: Any] = [:] 73 | 74 | for (key, sourceValue) in translations { 75 | var localizationsDict: [String: Any] = [:] 76 | 77 | // 为每种语言创建翻译 78 | for language in languages { 79 | do { 80 | // 使用 AI 服务翻译 81 | let translation = try await AIService.shared.translate(text: sourceValue, to: language) 82 | localizationsDict[language] = [ 83 | "stringUnit": [ 84 | "state": "translated", 85 | "value": translation 86 | ] 87 | ] 88 | } catch { 89 | print("翻译失败 [\(language)]: \(error.localizedDescription)") 90 | localizationsDict[language] = [ 91 | "stringUnit": [ 92 | "state": "needs_review", 93 | "value": "" 94 | ] 95 | ] 96 | } 97 | } 98 | 99 | stringsDict[key] = ["localizations": localizationsDict] 100 | } 101 | 102 | xcstringsDict["strings"] = stringsDict 103 | 104 | do { 105 | return .success(try JSONSerialization.data(withJSONObject: xcstringsDict, options: [.prettyPrinted, .sortedKeys])) 106 | } catch { 107 | return .failure(error) 108 | } 109 | } 110 | 111 | /// 处理 .strings 文件的转换 112 | static func processStringsFile(from inputPath: String, 113 | to outputPath: String, 114 | format: LocalizationFormat, 115 | languages: Set) async -> Result { 116 | do { 117 | let parseResult = parseStringsFile(at: inputPath) 118 | switch parseResult { 119 | case .success(let translations): 120 | if format == .xcstrings { 121 | // 转换为 xcstrings 122 | let xcstringsResult = await convertToXCStrings( 123 | translations: translations, 124 | languages: Array(languages).map { $0.code } 125 | ) 126 | 127 | switch xcstringsResult { 128 | case .success(let data): 129 | try data.write(to: URL(fileURLWithPath: outputPath)) 130 | return .success("Conversion successful!".localized) 131 | case .failure(let error): 132 | return .failure(error) 133 | } 134 | } else { 135 | print("开始生成 .strings 文件...") 136 | let baseURL = URL(fileURLWithPath: outputPath) 137 | 138 | for language in languages { 139 | print("处理语言: \(language.code)") 140 | let langURL = baseURL.appendingPathComponent("\(language.code).lproj") 141 | let stringsURL = langURL.appendingPathComponent("Localizable.strings") 142 | 143 | try FileManager.default.createDirectory( 144 | at: langURL, 145 | withIntermediateDirectories: true, 146 | attributes: nil 147 | ) 148 | 149 | // 批量翻译优化 150 | let values = Array(translations.values) 151 | let keys = Array(translations.keys) 152 | 153 | print("开始批量翻译: \(language.code)") 154 | let translatedValues = try await AIService.shared.batchTranslate( 155 | texts: values, 156 | to: language.code 157 | ) 158 | 159 | // 将翻译结果与键重新组合 160 | var translatedDict: [String: String] = [:] 161 | for (index, key) in keys.enumerated() { 162 | if index < translatedValues.count { 163 | translatedDict[key] = translatedValues[index] 164 | } 165 | } 166 | 167 | print("生成翻译文件: \(language.code)") 168 | let result = generateStringsFile( 169 | translations: translatedDict, 170 | to: stringsURL.path 171 | ) 172 | if case .failure(let error) = result { 173 | throw error 174 | } 175 | } 176 | return .success("Conversion successful!".localized) 177 | } 178 | case .failure(let error): 179 | return .failure(error) 180 | } 181 | } catch { 182 | return .failure(error) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /LanguageTool/ViewModels/TransferViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | import UniformTypeIdentifiers 4 | import Foundation 5 | 6 | @MainActor 7 | class TransferViewModel: ObservableObject { 8 | @Published var inputPath = "No file selected" 9 | @Published var outputPath = "No save location selected" 10 | @Published var isInputSelected: Bool = false 11 | @Published var isOutputSelected: Bool = false 12 | @Published var conversionResult: String = "" 13 | @Published var showResult: Bool = false 14 | @Published var selectedLanguages: Set = [Language.supportedLanguages[0]] 15 | @Published var isLoading: Bool = false 16 | @Published var showSuccessActions: Bool = false 17 | @Published var outputFormat: LocalizationFormat = .xcstrings 18 | @Published var selectedPlatform: PlatformType = .iOS 19 | @Published var languageChanged = false 20 | @Published var translationItems: [TranslationItem] = [] 21 | 22 | // 添加一个属性来保持对窗口的强引用 23 | private var localizationWindow: NSWindow? 24 | 25 | enum ExportFormat { 26 | case csv 27 | case excel 28 | 29 | var fileExtension: String { 30 | switch self { 31 | case .csv: return "csv" 32 | case .excel: return "xlsx" 33 | } 34 | } 35 | 36 | var contentType: UTType { 37 | switch self { 38 | case .csv: return UTType.commaSeparatedText 39 | case .excel: return UTType.spreadsheet 40 | } 41 | } 42 | } 43 | 44 | func selectInputFile() { 45 | let panel = NSOpenPanel() 46 | panel.allowsMultipleSelection = false 47 | panel.canChooseDirectories = false 48 | panel.canChooseFiles = true 49 | 50 | // 根据选择的平台设置允许的文件类型 51 | switch selectedPlatform { 52 | case .electron: 53 | panel.allowedContentTypes = [.json] 54 | panel.allowsOtherFileTypes = false 55 | 56 | case .iOS: 57 | var allowedTypes: [UTType] = [] 58 | if let xcstringsType = UTType(filenameExtension: "xcstrings") { 59 | allowedTypes.append(xcstringsType) 60 | } 61 | if let stringsType = UTType(filenameExtension: "strings") { 62 | allowedTypes.append(stringsType) 63 | } 64 | panel.allowedContentTypes = allowedTypes 65 | 66 | case .flutter: 67 | if let arbType = UTType(filenameExtension: "arb") { 68 | panel.allowedContentTypes = [arbType] 69 | } 70 | } 71 | 72 | // 根据平台设置提示信息 73 | panel.title = "Select Input File".localized 74 | switch selectedPlatform { 75 | case .iOS: 76 | panel.message = "Please select .strings or .xcstrings file".localized 77 | case .flutter: 78 | panel.message = "Please select .arb file".localized 79 | case .electron: 80 | panel.message = "Please select .json file".localized 81 | } 82 | 83 | panel.begin { response in 84 | if response == .OK, let fileURL = panel.url { 85 | // 关键修复 2: 强制二次验证扩展名 86 | if self.selectedPlatform == .electron { 87 | let fileExtension = fileURL.pathExtension.lowercased() 88 | guard fileExtension == "json" else { 89 | self.showAlert(message: "Must select .json file".localized, isError: true) 90 | return 91 | } 92 | } 93 | 94 | self.inputPath = fileURL.path 95 | self.isInputSelected = true 96 | 97 | // 根据选择的平台设置输出格式 98 | switch self.selectedPlatform { 99 | case .iOS: 100 | let fileExtension = fileURL.pathExtension.lowercased() 101 | self.outputFormat = fileExtension == "strings" ? .strings : .xcstrings 102 | case .flutter: 103 | self.outputFormat = .arb 104 | case .electron: 105 | self.outputFormat = .electron 106 | } 107 | } 108 | } 109 | } 110 | 111 | func selectOutputPath() { 112 | switch outputFormat { 113 | case .electron, .arb, .strings: 114 | selectDirectory() 115 | case .xcstrings: 116 | selectXCStringsFile() 117 | } 118 | } 119 | 120 | private func selectDirectory() { 121 | let openPanel = NSOpenPanel() 122 | openPanel.canChooseFiles = false 123 | openPanel.canChooseDirectories = true 124 | openPanel.allowsMultipleSelection = false 125 | openPanel.message = "Select directory for output files".localized 126 | openPanel.prompt = "Select".localized 127 | openPanel.title = "Select Save Directory".localized 128 | 129 | openPanel.begin { [weak self] response in 130 | guard let self = self else { return } 131 | if response == .OK, let directoryURL = openPanel.url { 132 | self.outputPath = directoryURL.path 133 | self.isOutputSelected = true 134 | } 135 | } 136 | } 137 | 138 | private func selectXCStringsFile() { 139 | let panel = NSSavePanel() 140 | if let xcstringsType = UTType(filenameExtension: "xcstrings") { 141 | panel.allowedContentTypes = [xcstringsType] 142 | } 143 | 144 | let dateFormatter = DateFormatter() 145 | dateFormatter.dateFormat = "yyyyMMdd_HHmmss" 146 | let timestamp = dateFormatter.string(from: Date()) 147 | 148 | panel.nameFieldStringValue = "Localizable_\(timestamp)" 149 | panel.canCreateDirectories = true 150 | panel.title = "Save Localization File".localized 151 | panel.message = "Select location to save .xcstrings file".localized 152 | 153 | panel.begin { [weak self] response in 154 | guard let self = self else { return } 155 | if response == .OK, let fileURL = panel.url { 156 | self.outputPath = fileURL.path 157 | self.isOutputSelected = true 158 | } 159 | } 160 | } 161 | 162 | func convertToLocalization() { 163 | Task { 164 | await MainActor.run { 165 | isLoading = true 166 | showResult = false 167 | showSuccessActions = false 168 | translationItems = [] 169 | } 170 | 171 | // 先解析输入文件获取翻译项 172 | translationItems = await TranslationManager.shared.parseInputFile(at: inputPath, platform: selectedPlatform) 173 | 174 | let fileExtension = (inputPath as NSString).pathExtension.lowercased() 175 | let result: (message: String, success: Bool) 176 | 177 | switch selectedPlatform { 178 | case .iOS: 179 | switch fileExtension { 180 | case "strings": 181 | let processResult = await StringsFileParser.processStringsFile( 182 | from: inputPath, 183 | to: outputPath, 184 | format: outputFormat, 185 | languages: selectedLanguages 186 | ) 187 | switch processResult { 188 | case .success(let message): 189 | result = (message: message, success: true) 190 | case .failure(let error): 191 | result = (message: "❌ 转换失败:\(error.localizedDescription)", success: false) 192 | } 193 | case "xcstrings": 194 | let conversionResult = await JsonUtils.convertToLocalizationFile( 195 | from: inputPath, 196 | to: outputPath, 197 | languages: Array(selectedLanguages).map { $0.code } 198 | ) 199 | result = (message: conversionResult.message, success: conversionResult.success) 200 | default: 201 | result = (message: "❌ 不支持的文件格式", success: false) 202 | } 203 | 204 | case .flutter: 205 | let processResult = await ARBFileHandler.processARBFile( 206 | from: inputPath, 207 | to: outputPath, 208 | languages: Array(selectedLanguages).map { $0.code } 209 | ) 210 | switch processResult { 211 | case .success(let message): 212 | result = (message: message, success: true) 213 | case .failure(let error): 214 | result = (message: "❌ 转换失败:\(error.localizedDescription)", success: false) 215 | } 216 | 217 | case .electron: 218 | // 严格校验输入文件类型 219 | guard fileExtension == "json" else { 220 | result = (message: "❌ Electron 平台仅支持 .json 文件", success: false) 221 | break 222 | } 223 | let processResult = await ElectronLocalizationHandler.processLocalizationFile( 224 | from: inputPath, 225 | to: outputPath, 226 | languages: Array(selectedLanguages).map { $0.code } 227 | ) 228 | switch processResult { 229 | case .success(let message): 230 | result = (message: message, success: true) 231 | case .failure(let error): 232 | result = (message: "❌ 转换失败:\(error.localizedDescription)", success: false) 233 | } 234 | } 235 | 236 | await MainActor.run { 237 | conversionResult = result.message 238 | showSuccessActions = result.success 239 | isLoading = false 240 | showResult = true 241 | } 242 | } 243 | } 244 | 245 | func handleDroppedFile(_ providers: [NSItemProvider]) -> Bool { 246 | guard let provider = providers.first else { return false } 247 | 248 | if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { 249 | provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in 250 | guard error == nil, 251 | let data = item as? Data, 252 | let url = URL(dataRepresentation: data, relativeTo: nil) else { 253 | return 254 | } 255 | 256 | // 验证文件类型 257 | let fileExtension = url.pathExtension.lowercased() 258 | var isValidFile = false 259 | 260 | switch self.selectedPlatform { 261 | case .iOS: 262 | isValidFile = ["strings", "xcstrings"].contains(fileExtension) 263 | case .flutter: 264 | isValidFile = fileExtension == "arb" 265 | case .electron: 266 | isValidFile = fileExtension == "json" 267 | } 268 | 269 | if !isValidFile { 270 | DispatchQueue.main.async { 271 | self.showAlert(message: "Invalid file type for selected platform".localized, isError: true) 272 | } 273 | return 274 | } 275 | 276 | DispatchQueue.main.async { 277 | self.inputPath = url.path 278 | self.isInputSelected = true 279 | 280 | // 根据选择的平台设置输出格式 281 | switch self.selectedPlatform { 282 | case .iOS: 283 | self.outputFormat = fileExtension == "strings" ? .strings : .xcstrings 284 | case .flutter: 285 | self.outputFormat = .arb 286 | case .electron: 287 | self.outputFormat = .electron 288 | } 289 | } 290 | } 291 | return true 292 | } 293 | return false 294 | } 295 | 296 | func resetAll() { 297 | // 重置文件路径 298 | inputPath = "No file selected".localized 299 | outputPath = "No save location selected".localized 300 | isInputSelected = false 301 | isOutputSelected = false 302 | 303 | // 重置语言选择(只保留简体中文) 304 | selectedLanguages = [Language.supportedLanguages[0]] 305 | 306 | // 重置结果显示 307 | showResult = false 308 | conversionResult = "" 309 | showSuccessActions = false 310 | } 311 | 312 | func showAlert(message: String, isError: Bool = false) { 313 | let alert = NSAlert() 314 | alert.messageText = (isError ? "Error" : "Success").localized 315 | alert.informativeText = message 316 | alert.alertStyle = isError ? .warning : .informational 317 | alert.addButton(withTitle: "OK".localized) 318 | alert.runModal() 319 | } 320 | 321 | func openInFinder() { 322 | NSWorkspace.shared.selectFile(outputPath, inFileViewerRootedAtPath: "") 323 | } 324 | 325 | func syncToSource() { 326 | let sourceURL = URL(fileURLWithPath: inputPath) 327 | let outputURL = URL(fileURLWithPath: outputPath) 328 | 329 | do { 330 | try FileManager.default.removeItem(at: sourceURL) 331 | try FileManager.default.copyItem(at: outputURL, to: sourceURL) 332 | showAlert(message: "Sync completed successfully".localized) 333 | } catch { 334 | showAlert(message: "Sync failed: \(error.localizedDescription)".localized, isError: true) 335 | } 336 | } 337 | 338 | func openInNewWindow() { 339 | Task { @MainActor in // 确保在主线程上下文中执行 340 | // 如果窗口已经存在,就把它带到前面 341 | if let existingWindow = localizationWindow { 342 | existingWindow.makeKeyAndOrderFront(nil) 343 | return 344 | } 345 | 346 | // 创建新窗口 347 | let window = NSWindow( 348 | contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), 349 | styleMask: [.titled, .closable, .miniaturizable, .resizable], 350 | backing: .buffered, 351 | defer: false 352 | ) 353 | 354 | // 创建窗口控制器(但不设置为代理) 355 | let windowController = NSWindowController(window: window) 356 | 357 | // 创建视图模型并设置必要的属性 358 | let viewModel = TransferViewModel() 359 | viewModel.inputPath = self.inputPath 360 | viewModel.outputPath = self.outputPath 361 | viewModel.selectedPlatform = self.selectedPlatform 362 | 363 | window.title = "Localization Master" 364 | let masterView = LocalizationMasterView(viewModel: viewModel) 365 | window.contentView = NSHostingView(rootView: masterView) 366 | window.center() 367 | 368 | // 保存对窗口的引用 369 | self.localizationWindow = window 370 | 371 | // 设置窗口关闭时的回调 372 | window.isReleasedWhenClosed = false 373 | 374 | // 创建并设置正确的窗口代理 375 | let delegate = WindowDelegate(onClose: { [weak self] in 376 | self?.localizationWindow = nil 377 | }) 378 | window.delegate = delegate 379 | 380 | // 保持对代理的引用(否则代理可能被过早释放) 381 | objc_setAssociatedObject(window, "delegateReference", delegate, .OBJC_ASSOCIATION_RETAIN) 382 | 383 | window.makeKeyAndOrderFront(nil) 384 | 385 | // 如果有输入文件,立即解析并显示内容 386 | if !inputPath.isEmpty && inputPath != "No file selected" { 387 | await viewModel.reloadSourceFile() 388 | } 389 | } 390 | } 391 | 392 | func exportToExcel() { 393 | let alert = NSAlert() 394 | alert.messageText = "Choose Export Format".localized 395 | alert.informativeText = "Please select the format you want to export to".localized 396 | alert.addButton(withTitle: "CSV") 397 | alert.addButton(withTitle: "Excel") 398 | alert.addButton(withTitle: "Cancel".localized) 399 | 400 | let response = alert.runModal() 401 | guard response != .alertThirdButtonReturn else { return } 402 | 403 | let format: ExportFormat = response == .alertFirstButtonReturn ? .csv : .excel 404 | 405 | let panel = NSSavePanel() 406 | panel.allowedContentTypes = [format.contentType] 407 | panel.nameFieldStringValue = "translations.\(format.fileExtension)" 408 | panel.canCreateDirectories = true 409 | 410 | panel.begin { response in 411 | if response == .OK, let url = panel.url { 412 | do { 413 | // 读取本地化文件 414 | let jsonData = try Data(contentsOf: URL(fileURLWithPath: self.outputPath)) 415 | let decoder = JSONDecoder() 416 | 417 | // 根据文件类型选择不同的解析方式 418 | let fileExtension = (self.outputPath as NSString).pathExtension.lowercased() 419 | var translations: [String: [String: String]] = [:] 420 | 421 | if fileExtension == "xcstrings" { 422 | // 解析 xcstrings 格式 423 | struct XCStringsContainer: Codable { 424 | struct StringEntry: Codable { 425 | struct LocalizationEntry: Codable { 426 | let stringUnit: StringUnit 427 | } 428 | let localizations: [String: LocalizationEntry] 429 | } 430 | struct StringUnit: Codable { 431 | let value: String 432 | } 433 | let strings: [String: StringEntry] 434 | } 435 | 436 | let xcstrings = try decoder.decode(XCStringsContainer.self, from: jsonData) 437 | 438 | // 转换格式 439 | for (key, entry) in xcstrings.strings { 440 | var languageValues: [String: String] = [:] 441 | for (languageCode, localization) in entry.localizations { 442 | languageValues[languageCode] = localization.stringUnit.value 443 | } 444 | translations[key] = languageValues 445 | } 446 | } else { 447 | // 其他格式直接解析 448 | translations = try decoder.decode([String: [String: String]].self, from: jsonData) 449 | } 450 | 451 | // 收集所有有翻译的语言代码 452 | var usedLanguageCodes = Set() 453 | for (_, values) in translations { 454 | for (code, value) in values { 455 | if !value.isEmpty { 456 | usedLanguageCodes.insert(code) 457 | } 458 | } 459 | } 460 | 461 | // 将语言代码转换为Language对象,并按照supportedLanguages的顺序排序 462 | let usedLanguages = Language.supportedLanguages.filter { usedLanguageCodes.contains($0.code) } 463 | 464 | let writeContent = { (format: ExportFormat) in 465 | var csvContent = "Key," 466 | csvContent += usedLanguages.map { $0.code }.joined(separator: ",") 467 | csvContent += "\n" 468 | 469 | // 添加每一行翻译内容 470 | for (key, values) in translations { 471 | csvContent += "\(key)," 472 | csvContent += usedLanguages.map { language in 473 | let value = values[language.code] ?? "" 474 | return "\"\(value.replacingOccurrences(of: "\"", with: "\"\""))\"" 475 | }.joined(separator: ",") 476 | csvContent += "\n" 477 | } 478 | 479 | // 写入文件,使用 UTF-8 BOM 以确保 Excel 正确识别编码 480 | let bom = Data([0xEF, 0xBB, 0xBF]) 481 | try bom.write(to: url) 482 | try csvContent.data(using: .utf8)?.write(to: url, options: .atomic) 483 | } 484 | 485 | // 根据选择的格式写入文件 486 | try writeContent(format) 487 | 488 | DispatchQueue.main.async { 489 | NSWorkspace.shared.open(url) 490 | } 491 | } catch { 492 | DispatchQueue.main.async { 493 | self.showAlert(message: "Export failed: \(error.localizedDescription)", isError: true) 494 | } 495 | } 496 | } 497 | } 498 | } 499 | 500 | // 在解析文件后更新 translations 数组 501 | func updateTranslations(from translations: [String: [String: String]]) { 502 | self.translationItems = translations.map { key, values in 503 | TranslationItem( 504 | key: key, 505 | translations: values 506 | ) 507 | } 508 | } 509 | 510 | func reloadSourceFile() async { 511 | guard !inputPath.isEmpty && inputPath != "No file selected" else { return } 512 | 513 | isLoading = true 514 | translationItems = await TranslationManager.shared.parseInputFile(at: inputPath, platform: selectedPlatform) 515 | isLoading = false 516 | } 517 | } 518 | 519 | // 添加一个窗口代理类来处理窗口关闭事件 520 | private class WindowDelegate: NSObject, NSWindowDelegate { 521 | let onClose: () -> Void 522 | 523 | init(onClose: @escaping () -> Void) { 524 | self.onClose = onClose 525 | super.init() 526 | } 527 | 528 | func windowWillClose(_ notification: Notification) { 529 | onClose() 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /LanguageTool/Views/Components/DragDropButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | import UniformTypeIdentifiers 4 | 5 | struct DragDropButton: View { 6 | let title: String 7 | let action: () -> Void 8 | let isSelected: Bool 9 | let onDrop: ([NSItemProvider]) -> Bool 10 | let useDefaultStyle: Bool 11 | 12 | var body: some View { 13 | Button(action: action) { 14 | HStack(spacing: 8) { 15 | Text(title) 16 | Image(systemName: "arrow.down.doc") 17 | .font(.system(size: 14)) 18 | .foregroundColor(.secondary) 19 | } 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | .contentShape(Rectangle()) // 确保整个区域可点击 22 | } 23 | .buttonStyle(DragDropButtonStyle(isSelected: isSelected, useDefaultStyle: useDefaultStyle)) 24 | .onDrop(of: [.fileURL], isTargeted: nil, perform: onDrop) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LanguageTool/Views/Components/LanguageToggle.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LanguageToggle: View { 4 | let language: Language 5 | let isSelected: Bool 6 | 7 | var body: some View { 8 | HStack { 9 | Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") 10 | .foregroundColor(isSelected ? .blue : .gray) 11 | VStack(alignment: .leading) { 12 | Text(language.localizedName) 13 | .font(.system(.body, design: .rounded)) 14 | Text(language.code) 15 | .font(.caption) 16 | .foregroundColor(.gray) 17 | } 18 | } 19 | .padding(8) 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | .background(isSelected ? Color.blue.opacity(0.1) : Color.clear) 22 | .cornerRadius(8) 23 | } 24 | } -------------------------------------------------------------------------------- /LanguageTool/Views/DeepseekDemo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | struct DeepseekDemo: View { 5 | @State private var messages: [Message] = [] 6 | @State private var inputText: String = "" 7 | @State private var responseMessage: String = "" 8 | @State private var isLoading: Bool = false 9 | 10 | func sendMessage() { 11 | isLoading = true 12 | messages.append(Message(role: "user", content: inputText)) 13 | 14 | AIService.shared.sendMessage(messages: messages) { result in 15 | DispatchQueue.main.async { 16 | isLoading = false 17 | switch result { 18 | case .success(let response): 19 | responseMessage = response 20 | messages.append(Message(role: "assistant", content: response)) 21 | case .failure(let error): 22 | responseMessage = error.localizedDescription 23 | } 24 | } 25 | } 26 | 27 | // 使用示例 28 | // let messages: [Message] = [ 29 | // Message(role: "system", content: "你是一个中英文翻译专家..."), 30 | // Message(role: "user", content: "牛顿第一定律...") 31 | // ] 32 | // 33 | // AIService.shared.sendMessage(messages: messages) { result in 34 | // switch result { 35 | // case .success(let translation): 36 | // print("翻译结果:\(translation)") 37 | // case .failure(let error): 38 | // print("错误:\(error.localizedDescription)") // 使用 localizedDescription 39 | // } 40 | // } 41 | 42 | } 43 | 44 | var body: some View { 45 | VStack { 46 | TextField("输入消息", text: $inputText) 47 | .textFieldStyle(RoundedBorderTextFieldStyle()) 48 | .padding() 49 | 50 | Button("发送") { 51 | sendMessage() 52 | } 53 | 54 | if isLoading { 55 | ProgressView() 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LanguageTool/Views/LocalizationMasterView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LocalizationMasterView: View { 4 | @State private var remainingTrials = 4 5 | @State private var translateSelectedOnly = true 6 | @StateObject var viewModel: TransferViewModel 7 | @State private var searchText = "" 8 | 9 | // 初始化器 10 | init(viewModel: TransferViewModel) { 11 | _viewModel = StateObject(wrappedValue: viewModel) 12 | } 13 | 14 | // 添加一个计算属性来获取所有可用的语言 15 | private var availableLanguages: [String] { 16 | // 从所有翻译项中收集语言代码 17 | var languageCodes = Set() 18 | for translation in viewModel.translationItems { 19 | languageCodes.formUnion(translation.translations.keys) 20 | } 21 | return Array(languageCodes).sorted() 22 | } 23 | 24 | // 添加一个计算属性来过滤和搜索翻译项 25 | private var filteredTranslations: [TranslationItem] { 26 | let items = viewModel.translationItems 27 | if searchText.isEmpty { 28 | return items 29 | } 30 | return items.filter { item in 31 | // 搜索key 32 | if item.key.localizedCaseInsensitiveContains(searchText) { 33 | return true 34 | } 35 | // 搜索translations中的值 36 | if item.translations.values.contains(where: { $0.localizedCaseInsensitiveContains(searchText) }) { 37 | return true 38 | } 39 | // 搜索comment 40 | if item.comment.localizedCaseInsensitiveContains(searchText) { 41 | return true 42 | } 43 | return false 44 | } 45 | } 46 | 47 | var body: some View { 48 | VStack(spacing: 0) { 49 | HStack { 50 | Toggle("仅对勾选了 “翻译“ 的内容进行翻译。", isOn: $translateSelectedOnly) 51 | Spacer() 52 | HStack { 53 | Image(systemName: "magnifyingglass") 54 | TextField("搜索", text: $searchText) 55 | .textFieldStyle(RoundedBorderTextFieldStyle()) 56 | .frame(width: 150) 57 | } 58 | } 59 | .padding() 60 | 61 | Divider() 62 | 63 | ScrollView { 64 | LazyVStack { 65 | headerRow() 66 | .padding(.horizontal) 67 | .padding(.vertical, 5) 68 | Divider() 69 | ForEach(filteredTranslations) { translation in 70 | TranslationRow( 71 | translation: binding(for: translation), 72 | translateSelectedOnly: $translateSelectedOnly 73 | ) 74 | .padding(.horizontal) 75 | .padding(.vertical, 3) 76 | Divider() 77 | } 78 | } 79 | } 80 | 81 | Divider() 82 | 83 | HStack { 84 | Button("新增语言") { 85 | // TODO: Implement add language functionality 86 | } 87 | Spacer() 88 | Button("重新加载源文件") { 89 | Task { 90 | await viewModel.reloadSourceFile() 91 | } 92 | } 93 | Button("同步到源文件") { 94 | viewModel.syncToSource() 95 | } 96 | Button("导出") { 97 | viewModel.exportToExcel() 98 | } 99 | Button("立即翻译") { 100 | // TODO: Implement immediate translation functionality 101 | } 102 | } 103 | .padding() 104 | } 105 | .frame(minWidth: 800, minHeight: 600) 106 | } 107 | 108 | // 辅助函数:为特定的翻译项创建绑定 109 | private func binding(for translation: TranslationItem) -> Binding { 110 | Binding( 111 | get: { 112 | translation 113 | }, 114 | set: { newValue in 115 | if let index = viewModel.translationItems.firstIndex(where: { $0.id == translation.id }) { 116 | viewModel.translationItems[index] = newValue 117 | } 118 | } 119 | ) 120 | } 121 | 122 | private func headerRow() -> some View { 123 | HStack { 124 | // 翻译选择框 125 | Text("翻译") 126 | .frame(width: 40) 127 | 128 | // Key 列 129 | Text("Key") 130 | .frame(maxWidth: .infinity, alignment: .leading) 131 | 132 | // 动态语言列 133 | ForEach(availableLanguages, id: \.self) { languageCode in 134 | Text(getLanguageDisplay(for: languageCode)) 135 | .frame(maxWidth: .infinity, alignment: .leading) 136 | } 137 | 138 | // Comment 列 139 | Text("Comment") 140 | .frame(maxWidth: .infinity, alignment: .leading) 141 | } 142 | .font(.subheadline) 143 | .foregroundColor(.gray) 144 | } 145 | 146 | // 辅助函数:获取语言的显示名称 147 | private func getLanguageDisplay(for code: String) -> String { 148 | let languageName = Locale.current.localizedString(forLanguageCode: code) ?? code 149 | return "\(code) (\(languageName))" 150 | } 151 | } 152 | 153 | struct TranslationItem: Identifiable { 154 | let id = UUID() 155 | var isSelected: Bool = true 156 | var key: String 157 | var translations: [String: String] // 语言代码到翻译的映射 158 | var comment: String = "" 159 | 160 | // 便利初始化器用于兼容现有代码 161 | init(isSelected: Bool = true, 162 | key: String, 163 | translations: [String: String] = [:], 164 | comment: String = "") { 165 | self.isSelected = isSelected 166 | self.key = key 167 | self.translations = translations 168 | self.comment = comment 169 | } 170 | } 171 | 172 | struct TranslationRow: View { 173 | @Binding var translation: TranslationItem 174 | @Binding var translateSelectedOnly: Bool 175 | 176 | var body: some View { 177 | HStack { 178 | // 翻译选择框 179 | Toggle("", isOn: $translation.isSelected) 180 | .frame(width: 40) 181 | 182 | // Key 183 | Text(translation.key) 184 | .frame(maxWidth: .infinity, alignment: .leading) 185 | .opacity(translation.isSelected ? 1.0 : 0.5) 186 | .disabled(!translation.isSelected) 187 | 188 | // 动态语言输入框 189 | ForEach(Array(translation.translations.keys).sorted(), id: \.self) { languageCode in 190 | TextField(getLanguageDisplay(for: languageCode), 191 | text: Binding( 192 | get: { translation.translations[languageCode] ?? "" }, 193 | set: { translation.translations[languageCode] = $0 } 194 | )) 195 | .textFieldStyle(RoundedBorderTextFieldStyle()) 196 | .frame(maxWidth: .infinity) 197 | .opacity(translation.isSelected ? 1.0 : 0.5) 198 | .disabled(!translation.isSelected) 199 | } 200 | 201 | // Comment 202 | TextField("Comment", text: $translation.comment) 203 | .textFieldStyle(RoundedBorderTextFieldStyle()) 204 | .frame(maxWidth: .infinity) 205 | .opacity(translation.isSelected ? 1.0 : 0.5) 206 | .disabled(!translation.isSelected) 207 | } 208 | // .opacity(translation.isSelected ? 1.0 : 0.5) 209 | // .disabled(!translation.isSelected) 210 | } 211 | 212 | private func getLanguageDisplay(for code: String) -> String { 213 | Locale.current.localizedString(forLanguageCode: code) ?? code 214 | } 215 | } 216 | 217 | struct LocalizationMasterView_Previews: PreviewProvider { 218 | static var previews: some View { 219 | LocalizationMasterView(viewModel: TransferViewModel()) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /LanguageTool/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsView: View { 4 | @AppStorage("apiKey") private var apiKey: String = "" 5 | @AppStorage("selectedAIService") private var selectedService: AIServiceType = .deepseek 6 | @AppStorage("geminiApiKey") private var geminiApiKey: String = "" 7 | @AppStorage("aliyunApiKey") private var aliyunApiKey: String = "" 8 | @AppStorage("appLanguage") private var appLanguage: String = "en" // 默认为英语 9 | @AppStorage("isDarkMode") private var isDarkMode: Bool = false // 添加暗黑模式存储 10 | 11 | // 修改为使用原生语言名称 12 | private let supportedLanguages = [ 13 | ("en", "English"), 14 | ("en-CA", "English (Canada)"), 15 | ("en-GB", "English (UK)"), 16 | ("en-IN", "English (India)"), 17 | ("de", "Deutsch"), 18 | ("fr", "Français"), 19 | ("zh-Hans", "简体中文"), 20 | ("zh-Hant", "繁體中文"), 21 | ("ja", "日本語"), 22 | ("ko", "한국어") 23 | ] 24 | 25 | // 添加语言切换通知 26 | @State private var languageChanged = false 27 | 28 | @Environment(\.colorScheme) var colorScheme // 获取当前颜色方案 29 | 30 | var body: some View { 31 | Form { 32 | Section(header: Text("API Settings".localized)) { 33 | // AI 服务选择 34 | Picker("AI Service".localized, selection: $selectedService) { 35 | ForEach(AIServiceType.allCases, id: \.self) { service in 36 | Text(service.description).tag(service) 37 | } 38 | } 39 | 40 | switch selectedService { 41 | case .deepseek: 42 | SecureField("DeepSeek API Key".localized, text: $apiKey) 43 | .textFieldStyle(.roundedBorder) 44 | case .gemini: 45 | SecureField("Gemini API Key".localized, text: $geminiApiKey) 46 | .textFieldStyle(.roundedBorder) 47 | case .aliyun: 48 | SecureField("Aliyun API Key".localized, text: $aliyunApiKey) 49 | .textFieldStyle(.roundedBorder) 50 | } 51 | } 52 | 53 | Section(header: Text("Language Settings".localized)) { 54 | Picker("Interface Language".localized, selection: $appLanguage) { 55 | ForEach(supportedLanguages, id: \.0) { code, nativeName in 56 | Text(nativeName).tag(code) 57 | } 58 | } 59 | .onChange(of: appLanguage) { oldValue, newValue in 60 | // 更新语言设置 61 | UserDefaults.standard.set([newValue], forKey: "AppleLanguages") 62 | UserDefaults.standard.synchronize() 63 | 64 | // 发送语言变更通知 65 | NotificationCenter.default.post(name: .languageChanged, object: nil) 66 | languageChanged.toggle() 67 | } 68 | } 69 | 70 | Section(header: Text("Appearance Settings".localized)) { // 添加外观设置部分 71 | Toggle("Dark Mode".localized, isOn: $isDarkMode) // 暗黑模式切换 72 | } 73 | 74 | Section("Other Settings".localized) { 75 | Text("More Settings Under Development...".localized) 76 | .foregroundColor(.secondary) 77 | } 78 | } 79 | .formStyle(.grouped) 80 | .padding(.horizontal, 20) 81 | .frame(width: 400) 82 | .frame(minHeight: 200) 83 | .id(languageChanged) // 强制视图刷新 84 | .preferredColorScheme(isDarkMode ? .dark : .light) // 根据 isDarkMode 设置颜色方案 85 | } 86 | } 87 | 88 | // 添加语言变更通知名称 89 | extension Notification.Name { 90 | static let languageChanged = Notification.Name("com.app.languageChanged") 91 | } 92 | -------------------------------------------------------------------------------- /LanguageTool/Views/TransferView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import AppKit 3 | import UniformTypeIdentifiers 4 | 5 | struct TransferView: View { 6 | @AppStorage("isDarkMode") private var isDarkMode: Bool = false 7 | @StateObject private var viewModel = TransferViewModel() 8 | 9 | private let columns = [ 10 | GridItem(.adaptive(minimum: 160)) 11 | ] 12 | 13 | var body: some View { 14 | ZStack { 15 | ScrollView { 16 | VStack(spacing: 20) { 17 | platformSelectionView 18 | fileSelectionView 19 | languageSelectionView 20 | actionButtonsView 21 | 22 | if viewModel.showResult { 23 | resultsView 24 | } 25 | } 26 | .padding() 27 | .frame(maxWidth: 600) 28 | } 29 | .frame(minHeight: 500) 30 | .blur(radius: viewModel.isLoading ? 3 : 0) 31 | .preferredColorScheme(isDarkMode ? .dark : .light) 32 | 33 | if viewModel.isLoading { 34 | loadingView 35 | } 36 | } 37 | .allowsHitTesting(!viewModel.isLoading) 38 | .id(viewModel.languageChanged) 39 | .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in 40 | viewModel.languageChanged.toggle() 41 | } 42 | } 43 | 44 | private var platformSelectionView: some View { 45 | VStack(alignment: .leading, spacing: 10) { 46 | Text("Select Platform".localized) 47 | .font(.headline) 48 | 49 | Picker("Platform".localized, selection: $viewModel.selectedPlatform) { 50 | ForEach(PlatformType.allCases, id: \.self) { platform in 51 | Text(platform.description) 52 | .tag(platform) 53 | } 54 | } 55 | .pickerStyle(.segmented) 56 | .onChange(of: viewModel.selectedPlatform) { oldValue, newValue in 57 | viewModel.resetAll() 58 | } 59 | } 60 | .padding(.bottom, 10) 61 | } 62 | 63 | private var fileSelectionView: some View { 64 | VStack(alignment: .leading, spacing: 20) { 65 | VStack(alignment: .leading, spacing: 10) { 66 | DragDropButton( 67 | title: "Select Input File".localized, 68 | action: viewModel.selectInputFile, 69 | isSelected: viewModel.isInputSelected, 70 | onDrop: viewModel.handleDroppedFile, 71 | useDefaultStyle: false 72 | ) 73 | 74 | Text(viewModel.inputPath.localized) 75 | .foregroundColor(.gray) 76 | .font(.system(.body, design: .monospaced)) 77 | .lineLimit(1) 78 | .truncationMode(.middle) 79 | } 80 | 81 | VStack(alignment: .leading, spacing: 10) { 82 | DragDropButton( 83 | title: "Select Output Location".localized + " (Optional)".localized, 84 | action: viewModel.selectOutputPath, 85 | isSelected: viewModel.isOutputSelected, 86 | onDrop: { _ in false }, 87 | useDefaultStyle: true 88 | ) 89 | 90 | if viewModel.isOutputSelected { 91 | Text(viewModel.outputPath.localized) 92 | .foregroundColor(.gray) 93 | .font(.system(.body, design: .monospaced)) 94 | .lineLimit(1) 95 | .truncationMode(.middle) 96 | } 97 | } 98 | } 99 | } 100 | 101 | private var languageSelectionView: some View { 102 | VStack(alignment: .leading, spacing: 10) { 103 | HStack { 104 | Text("Select Target Languages".localized) 105 | .font(.headline) 106 | 107 | Spacer() 108 | 109 | Button(action: { 110 | if viewModel.selectedLanguages.count == Language.supportedLanguages.count { 111 | viewModel.selectedLanguages = [Language.supportedLanguages[0]] 112 | } else { 113 | viewModel.selectedLanguages = Set(Language.supportedLanguages) 114 | } 115 | }) { 116 | Text(viewModel.selectedLanguages.count == Language.supportedLanguages.count ? "Deselect All".localized : "Select All".localized) 117 | .font(.subheadline) 118 | } 119 | .buttonStyle(.borderless) 120 | } 121 | 122 | ScrollView { 123 | LazyVGrid(columns: columns, spacing: 10) { 124 | ForEach(Language.supportedLanguages) { language in 125 | LanguageToggle( 126 | language: language, 127 | isSelected: viewModel.selectedLanguages.contains(language) 128 | ) 129 | .onTapGesture { 130 | if viewModel.selectedLanguages.contains(language) { 131 | if viewModel.selectedLanguages.count > 1 { 132 | viewModel.selectedLanguages.remove(language) 133 | } 134 | } else { 135 | viewModel.selectedLanguages.insert(language) 136 | } 137 | } 138 | } 139 | } 140 | .padding(.horizontal) 141 | } 142 | .frame(height: 200) 143 | .background(Color.gray.opacity(0.1)) 144 | .cornerRadius(8) 145 | } 146 | } 147 | 148 | private var actionButtonsView: some View { 149 | HStack(spacing: 12) { 150 | Button("Start Conversion".localized) { 151 | viewModel.convertToLocalization() 152 | } 153 | .disabled(!viewModel.isInputSelected || !viewModel.isOutputSelected || viewModel.selectedLanguages.isEmpty || viewModel.isLoading) 154 | .buttonStyle(.borderedProminent) 155 | 156 | Button(action: viewModel.resetAll) { 157 | HStack(spacing: 4) { 158 | Image(systemName: "arrow.counterclockwise") 159 | Text("Reset".localized) 160 | } 161 | } 162 | .buttonStyle(.bordered) 163 | .disabled(viewModel.isLoading) 164 | 165 | // Button(action: viewModel.openInNewWindow) { 166 | // HStack { 167 | // Image(systemName: "window") 168 | // Text("Open in New Window".localized) 169 | // } 170 | // } 171 | // .buttonStyle(.bordered) 172 | // .disabled(viewModel.isLoading) 173 | } 174 | } 175 | 176 | private var resultsView: some View { 177 | VStack(spacing: 12) { 178 | Text(viewModel.conversionResult) 179 | .foregroundColor(viewModel.conversionResult.hasPrefix("✅") ? .green : .red) 180 | .font(.system(.body, design: .rounded)) 181 | 182 | if viewModel.showSuccessActions { 183 | VStack(spacing: 8) { 184 | HStack(spacing: 16) { 185 | Text("Save Path:".localized) 186 | .font(.subheadline) 187 | .foregroundColor(.secondary) 188 | 189 | Button(action: viewModel.openInFinder) { 190 | HStack { 191 | Image(systemName: "folder") 192 | Text("Show in Finder".localized) 193 | } 194 | } 195 | 196 | Button(action: viewModel.syncToSource) { 197 | HStack { 198 | Image(systemName: "arrow.triangle.2.circlepath") 199 | Text("Sync to Source".localized) 200 | } 201 | } 202 | 203 | Button(action: viewModel.exportToExcel) { 204 | HStack { 205 | Image(systemName: "arrow.down.doc") 206 | Text("Export to Excel".localized) 207 | } 208 | } 209 | 210 | 211 | } 212 | .buttonStyle(.borderless) 213 | .padding(.top, 4) 214 | 215 | Text(viewModel.outputPath) 216 | .font(.system(.body, design: .monospaced)) 217 | .foregroundColor(.primary) 218 | .padding(8) 219 | .background(Color.gray.opacity(0.1)) 220 | .cornerRadius(6) 221 | } 222 | .padding() 223 | .background( 224 | RoundedRectangle(cornerRadius: 8) 225 | .fill(Color.gray.opacity(0.05)) 226 | ) 227 | .overlay( 228 | RoundedRectangle(cornerRadius: 8) 229 | .stroke(Color.gray.opacity(0.1), lineWidth: 1) 230 | ) 231 | } 232 | } 233 | .padding(.vertical) 234 | } 235 | 236 | private var loadingView: some View { 237 | VStack(spacing: 16) { 238 | ProgressView() 239 | .scaleEffect(1.5) 240 | Text("Translating...".localized) 241 | .font(.headline) 242 | Text("Please wait, this may take a while".localized) 243 | .font(.subheadline) 244 | .foregroundColor(.gray) 245 | } 246 | .padding(30) 247 | .background { 248 | RoundedRectangle(cornerRadius: 12) 249 | .fill(.background) 250 | .shadow(radius: 20) 251 | } 252 | } 253 | } 254 | 255 | 256 | // 自定义按钮样式 257 | struct DragDropButtonStyle: ButtonStyle { 258 | let isSelected: Bool 259 | let useDefaultStyle: Bool 260 | 261 | func makeBody(configuration: Configuration) -> some View { 262 | configuration.label 263 | .padding(.vertical, 8) 264 | .padding(.horizontal, 12) 265 | .background( 266 | RoundedRectangle(cornerRadius: 8) 267 | .strokeBorder( 268 | style: StrokeStyle( 269 | lineWidth: 2, 270 | dash: [5] 271 | ) 272 | ) 273 | .foregroundColor(isSelected ? .blue : .gray) 274 | ) 275 | .background( 276 | RoundedRectangle(cornerRadius: 8) 277 | .fill(configuration.isPressed ? Color.gray.opacity(0.1) : Color.clear) 278 | ) 279 | } 280 | } 281 | 282 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Language Tool 2 | [English](README-en.md) | [中文](README.md) 3 | Language Tool 是一个 macOS 应用程序,用于自动化生成多平台的多语言本地化文件。支持 iOS、Flutter 和 Electron 项目的本地化文件生成。 4 | 5 | ## 功能特点 6 | 7 | - 📱 支持多个平台: 8 | - iOS: `.xcstrings` 和 `.strings` 文件 9 | - Flutter: `.arb` 文件 10 | - Electron: 本地化 `.json` 文件 11 | - 🌍 支持 50+ 种语言的自动翻译 12 | - 🔄 批量翻译处理 13 | - 💾 按平台生成标准格式的本地化文件 14 | - ⚡️ 简单直观的用户界面 15 | - 🎯 完全适配各平台的本地化工作流 16 | - 🔁 支持同步更新源文件内容 17 | - 📊 支持导出为 Excel 格式便于管理 18 | 19 | ## 支持的语言 20 | 21 | 包括但不限于: 22 | - 中文(简体、繁体、香港繁体) 23 | - 英语(美国、英国、澳大利亚等变体) 24 | - 日语 25 | - 韩语 26 | - 欧洲语言(法语、德语、西班牙语等) 27 | - 东南亚语言(泰语、越南语等) 28 | - 中东语言(阿拉伯语等) 29 | 30 | ## 使用方法 31 | 32 | 1. 启动应用程序 33 | ![](https://raw.githubusercontent.com/aSynch1889/image/master/uPic/s0edU520250331001441.png) 34 | 2. 在设置中配置 AI 服务的 API Key 35 | ![](https://raw.githubusercontent.com/aSynch1889/image/master/uPic/7Rp8GC20250331001235.png) 36 | 3. 选择目标平台(iOS/Flutter/Electron) 37 | 4. 选择源文件: 38 | - iOS: 选择 `.xcstrings` 或 `.strings` 文件 39 | - Flutter: 选择 `.arb` 文件 40 | - Electron: 选择 `.json` 文件 41 | 5. 选择目标语言 42 | 6. 选择保存位置 43 | 7. 点击"开始转换" 44 | 8. 等待转换完成 45 | 9. 将生成的文件添加到你的项目中: 46 | - iOS: 添加 `.xcstrings` 或 `.strings` 文件到 Xcode 项目 47 | - Flutter: 将 `.arb` 文件放入 `lib/l10n` 目录 48 | - Electron: 将生成的 JSON 文件放入项目的语言资源目录 49 | 50 | ## 系统要求 51 | 52 | - macOS 13.0 或更高版本 53 | - 对于 iOS 开发:Xcode 15.0 或更高版本(用于 .xcstrings 支持) 54 | - 对于 Flutter 开发:Flutter SDK 55 | - 对于 Electron 开发:Node.js 环境 56 | 57 | ## 安装 58 | 59 | 由于这是一个开源项目,目前没有经过 Apple 公证,安装时需要一些额外步骤: 60 | 61 | 1. 从 Releases 页面下载最新的 .zip 文件 62 | 2. 解压缩文件 63 | 3. 将 .app 文件拖入 Applications 文件夹 64 | 4. 首次运行时: 65 | - 右键点击应用图标 66 | - 选择"打开" 67 | - 在弹出的警告对话框中选择"打开" 68 | 69 | 70 | 注意:由于应用没有经过 Apple 签名,首次运行时系统会显示安全警告,这是正常的。如果你担心安全问题,可以查看源代码并自行编译。 71 | 72 | ### 从源码构建 73 | 74 | 如果你更倾向于自己构建应用: 75 | 76 | 1. 克隆仓库: 77 | ```bash 78 | git clone https://github.com/aSynch1889/LanguageTool.git 79 | ``` 80 | 2. 使用 Xcode 打开项目 81 | 3. 选择 Product > Build 82 | 4. 构建完成后,应用会出现在 Xcode 的 product文件夹中 83 | 84 | ## 开发环境 85 | 86 | - Swift 5.9 87 | - SwiftUI 88 | - Xcode 15.0+ 89 | 90 | ## 注意事项 91 | 92 | - 使用前需要配置有效的 DeepSeek AI 或者 Gemini(免费但限制地区) 服务 API Key 93 | - 建议在使用前备份原有的本地化文件 94 | - 翻译结果可能需要人工审核以确保准确性 95 | - 不同平台的本地化文件格式有所不同,请确保选择正确的平台 96 | - deepseek API [申请地址](https://platform.deepseek.com/api_keys) 97 | - Gemini API [申请地址](https://aistudio.google.com/app/apikey?hl=zh-cn) 98 | 99 | ## 贡献 100 | 101 | 欢迎提交 Issue 和 Pull Request! 102 | 103 | ## 许可证 104 | 105 | 本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 106 | 107 | ## 致谢 108 | 109 | - DeepSeek AI、Gemini 提供翻译服务 110 | - SwiftUI 框架 111 | - 所有贡献者和用户 112 | 113 | ## 联系方式 114 | 115 | 如有问题或建议,请通过 GitHub Issues 与我们联系。 116 | 117 | --- 118 | 119 | Made with ❤️ by [华子] 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Language Tool 2 | [English](README.md) | [中文](README-zh.md) 3 | Language Tool is a macOS application designed for the automated generation of multi-platform localization files in multiple languages. It supports the generation of localization files for iOS, Flutter, and Electron projects. 4 | 5 | ## Features 6 | 7 | - 📱 Multi-platform support: 8 | - iOS: `.xcstrings` and `.strings` files 9 | - Flutter: `.arb` files 10 | - Electron: localized `.json` files 11 | - 🌍 Supports automatic translation in 50+ languages 12 | - 🔄 Batch translation processing 13 | - 💾 Generates standardized localization files by platform 14 | - ⚡️ Simple and intuitive user interface 15 | - 🎯 Fully compatible with localization workflows across platforms 16 | - 🔁 Supports synchronization with source file content 17 | - 📊 Export to Excel format for easier management 18 | 19 | ## Supported Languages 20 | 21 | Including but not limited to: 22 | - Chinese (Simplified, Traditional, Hong Kong Traditional) 23 | - English (US, UK, Australian variants, etc.) 24 | - Japanese 25 | - Korean 26 | - European languages (French, German, Spanish, etc.) 27 | - Southeast Asian languages (Thai, Vietnamese, etc.) 28 | - Middle Eastern languages (Arabic, etc.) 29 | 30 | ## How to Use 31 | 32 | 1. Launch the application 33 | ![](https://raw.githubusercontent.com/aSynch1889/image/master/uPic/TvuKkX20250331001509.png) 34 | 2. Configure the API Key for the AI service in the settings 35 | ![](https://raw.githubusercontent.com/aSynch1889/image/master/uPic/RFS8Kk20250331001528.png) 36 | 3. Select the target platform (iOS/Flutter/Electron) 37 | 4. Choose the source file: 38 | - iOS: Select `.xcstrings` or `.strings` files 39 | - Flutter: Select `.arb` files 40 | - Electron: Select `.json` files 41 | 5. Select the target language 42 | 6. Choose the save location 43 | 7. Click "Start Conversion" 44 | 8. Wait for the conversion to complete 45 | 9. Add the generated files to your project: 46 | - iOS: Add `.xcstrings` or `.strings` files to the Xcode project 47 | - Flutter: Place `.arb` files in the `lib/l10n` directory 48 | - Electron: Place the generated JSON files in the project's language resource directory 49 | 50 | ## System Requirements 51 | 52 | - macOS 13.0 or later 53 | - For iOS development: Xcode 15.0 or later (for .xcstrings support) 54 | - For Flutter development: Flutter SDK 55 | - For Electron development: Node.js environment 56 | 57 | ## Installation 58 | 59 | As this is an open-source project, it has not been notarized by Apple, and some additional steps are required during installation: 60 | 61 | 1. Download the latest .zip file from the Releases page 62 | 2. Unzip the file 63 | 3. Drag the .app file into the Applications folder 64 | 4. On the first run: 65 | - Right-click the application icon 66 | - Select "Open" 67 | - In the pop-up warning dialog, select "Open" 68 | 69 | **Note**: Since the application has not been signed by Apple, the system will display a security warning on the first run. This is normal. If you are concerned about security, you can review the source code and compile it yourself. 70 | 71 | ### Building from Source 72 | 73 | If you prefer to build the application yourself: 74 | 75 | 1. Clone the repository: 76 | ```bash 77 | git clone https://github.com/aSynch1889/LanguageTool.git 78 | ``` 79 | 2. Open the project using Xcode 80 | 3. Select Product > Build 81 | 4. Once built, the application will appear in the product folder of Xcode 82 | 83 | ## Development Environment 84 | 85 | - Swift 5.9 86 | - SwiftUI 87 | - Xcode 15.0+ 88 | 89 | ## Notes 90 | 91 | - You need to configure a valid DeepSeek AI or Gemini service API Key before use 92 | - It is recommended to back up existing localization files before use 93 | - Translation results may require manual review to ensure accuracy 94 | - Different platforms have different localization file formats, please ensure to select the correct platform 95 | - deepseek [application portal](https://platform.deepseek.com/api_keys) 96 | - Gemini api [application portal](https://aistudio.google.com/app/apikey?hl=zh-cn) 97 | 98 | ## Contribution 99 | 100 | Feel free to submit Issues and Pull Requests! 101 | 102 | ## License 103 | 104 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 105 | 106 | ## Acknowledgments 107 | 108 | - DeepSeek AI and Gemini for providing translation services 109 | - SwiftUI framework 110 | - All contributors and users 111 | 112 | ## Contact 113 | 114 | If you have any questions or suggestions, please contact us via GitHub Issues. --------------------------------------------------------------------------------