├── Demo └── Untouched │ ├── Demo │ ├── de.lproj │ │ ├── Main.strings │ │ ├── LaunchScreen.strings │ │ └── Localizable.strings │ ├── tr.lproj │ │ ├── Main.strings │ │ ├── LaunchScreen.strings │ │ └── Localizable.strings │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── en.lproj │ │ ├── Localizable.strings │ │ └── Main.strings │ ├── ViewController.swift │ ├── BartyCrouch.swift │ ├── Info.plist │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── DemoUITests │ ├── DemoUITests.swift │ └── Info.plist │ ├── DemoTests │ ├── DemoTests.swift │ └── Info.plist │ └── Demo.xcodeproj │ └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests ├── Resources │ ├── StringsFiles │ │ ├── zh-Hans.lproj │ │ │ ├── CustomName.strings │ │ │ └── Localizable.strings │ │ ├── NewExample.strings │ │ ├── OldExample.strings │ │ ├── en.lproj │ │ │ ├── InfoPlist.strings │ │ │ └── Localizable.strings │ │ ├── de.lproj │ │ │ └── Localizable.strings │ │ ├── ja.lproj │ │ │ └── Localizable.strings │ │ └── UnsortedKeys │ │ │ └── Base.lproj │ │ │ └── Localizable.strings │ ├── CodeFiles │ │ ├── UnsortedKeys │ │ │ └── SwiftExample3.swift │ │ ├── Subfolder │ │ │ ├── SwiftExample2.swift │ │ │ └── Subfolder │ │ │ │ └── SwiftExample3.swift │ │ └── SwiftExample1.swift │ ├── MultipleArgumentsCode │ │ ├── 2Arguments │ │ │ └── SwiftExample2Arguments.swift │ │ ├── 3Arguments │ │ │ └── SwiftExample3Arguments.swift │ │ └── 4Arguments │ │ │ └── SwiftExample4Arguments.swift │ ├── CodeFilesCustomFunction │ │ ├── UnsortedKeys │ │ │ └── SwiftExample3.swift │ │ ├── Subfolder │ │ │ └── SwiftExample2.swift │ │ └── SwiftExample1.swift │ ├── MultipleArgumentsCodeCustomFunction │ │ ├── 2Arguments │ │ │ └── SwiftExample2Arguments.swift │ │ ├── 3Arguments │ │ │ └── SwiftExample3Arguments.swift │ │ └── 4Arguments │ │ │ └── SwiftExample4Arguments.swift │ └── Storyboards │ │ ├── macOS │ │ ├── de.lproj │ │ │ └── Example.strings │ │ ├── en.lproj │ │ │ └── Example.strings │ │ ├── ja.lproj │ │ │ └── Example.strings │ │ ├── zh-Hans.lproj │ │ │ └── Example.strings │ │ └── Base.lproj │ │ │ └── Example.storyboard │ │ ├── iOS │ │ ├── de.lproj │ │ │ └── Example.strings │ │ ├── en.lproj │ │ │ └── Example.strings │ │ ├── ja.lproj │ │ │ └── Example.strings │ │ ├── zh-Hans.lproj │ │ │ └── Example.strings │ │ └── Base.lproj │ │ │ └── Example.storyboard │ │ └── tvOS │ │ ├── de.lproj │ │ └── Example.strings │ │ ├── en.lproj │ │ └── Example.strings │ │ ├── ja.lproj │ │ └── Example.strings │ │ ├── zh-Hans.lproj │ │ └── Example.strings │ │ └── Base.lproj │ │ └── Example.storyboard ├── SupportingFiles │ ├── Constants.h │ ├── Constants.m │ ├── BartyCrouchKitTests-Bridging-Header.h │ └── Info.plist ├── BartyCrouchTranslatorTests │ ├── Secrets │ │ ├── secrets.json.sample │ │ └── Secrets.swift │ ├── DeepLTranslatorApiTests.swift │ └── MicrosoftTranslatorApiTests.swift ├── BartyCrouchKitTests │ ├── Helpers │ │ └── FileManagerExtension.swift │ ├── CommandLine │ │ ├── FindFilesTests.swift │ │ ├── IBToolCommanderTests.swift │ │ ├── ExtractLocStringsTests.swift │ │ ├── CommandLineActorTests.swift │ │ ├── CommandLineParserTests.swift │ │ └── ExtractLocStringsCommanderTests.swift │ ├── FileHandling │ │ ├── CodeFilesSearchTests.swift │ │ ├── CodeFileHandlerTests.swift │ │ └── StringsFilesSearchTests.swift │ └── DemoTests │ │ ├── Directory.swift │ │ └── DemoTests.swift └── BartyCrouchConfigurationTests │ └── ConfigurationTests.swift ├── Logo.png ├── Images ├── Exclusion-Example.png ├── Build-Script-Example.png ├── IB-Comment-Exclusion-Example1.png └── IB-Comment-Exclusion-Example2.png ├── Sources ├── BartyCrouchKit │ ├── TaskHandlers │ │ ├── TaskHandler.swift │ │ ├── LintTaskHandler.swift │ │ ├── InterfacesTaskHandler.swift │ │ ├── NormalizeTaskHandler.swift │ │ ├── TranslateTaskHandler.swift │ │ ├── CodeTaskHandler.swift │ │ ├── InitTaskHandler.swift │ │ └── TransformTaskHandler.swift │ ├── Globals │ │ ├── Extensions │ │ │ ├── ArrayExtension.swift │ │ │ └── StringExtension.swift │ │ ├── CommandExecution.swift │ │ ├── Env.swift │ │ ├── TestHelper.swift │ │ ├── GlobalOptions.swift │ │ ├── CommandLineErrorHandler.swift │ │ └── PrintLevel.swift │ ├── Commands │ │ ├── InitCommand.swift │ │ ├── LintCommand.swift │ │ └── UpdateCommand.swift │ ├── OldCommandLine │ │ ├── IBToolCommander.swift │ │ ├── ExtractLocStrings.swift │ │ └── CodeCommander.swift │ └── FileHandling │ │ ├── SupportedLanguagesReader.swift │ │ ├── FilesSearchable.swift │ │ ├── CodeFileHandler.swift │ │ ├── CodeFilesSearch.swift │ │ ├── StringsFilesSearch.swift │ │ └── TranslateTransformer.swift ├── BartyCrouchUtility │ ├── Transformer.swift │ ├── Secret.swift │ └── Constants.swift ├── BartyCrouchTranslator │ ├── DeeplApi │ │ ├── Model │ │ │ ├── DeepLTranslateErrorResponse.swift │ │ │ └── DeepLTranslateResponse.swift │ │ └── DeepLApi.swift │ ├── MicrosoftTranslatorApi │ │ ├── Models │ │ │ ├── TranslateRequest.swift │ │ │ ├── TranslateResponse.swift │ │ │ └── Language.swift │ │ └── MicrosoftTranslatorApi.swift │ └── BartyCrouchTranslator.swift ├── BartyCrouchConfiguration │ ├── TomlCodable.swift │ ├── Extensions │ │ └── TomlExtension.swift │ ├── Options │ │ ├── LintOptions.swift │ │ ├── UpdateOptions │ │ │ ├── InterfacesOptions.swift │ │ │ ├── NormalizeOptions.swift │ │ │ ├── TranslateOptions.swift │ │ │ ├── CodeOptions.swift │ │ │ └── TransformOptions.swift │ │ └── UpdateOptions.swift │ └── Configuration.swift ├── BartyCrouch │ └── main.swift └── SupportingFiles │ ├── BartyCrouch.h │ └── Info.plist ├── .swift-format ├── .gitignore ├── Formula └── bartycrouch.rb ├── LICENSE ├── BartyCrouch.podspec ├── Makefile ├── .github └── workflows │ └── main.yml ├── MIGRATION_GUIDES.md ├── Package.swift └── CODE_OF_CONDUCT.md /Demo/Untouched/Demo/de.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/tr.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/de.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/tr.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/zh-Hans.lproj/CustomName.strings: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Logo.png -------------------------------------------------------------------------------- /Tests/SupportingFiles/Constants.h: -------------------------------------------------------------------------------- 1 | @import Foundation; 2 | 3 | extern const NSString *BASE_DIR; 4 | -------------------------------------------------------------------------------- /Tests/SupportingFiles/Constants.m: -------------------------------------------------------------------------------- 1 | #import "Constants.h" 2 | 3 | const NSString *BASE_DIR = PROJECT_DIR; 4 | -------------------------------------------------------------------------------- /Images/Exclusion-Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Images/Exclusion-Example.png -------------------------------------------------------------------------------- /Images/Build-Script-Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Images/Build-Script-Example.png -------------------------------------------------------------------------------- /Demo/Untouched/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Images/IB-Comment-Exclusion-Example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Images/IB-Comment-Exclusion-Example1.png -------------------------------------------------------------------------------- /Images/IB-Comment-Exclusion-Example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Images/IB-Comment-Exclusion-Example2.png -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/TaskHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol TaskHandler { 4 | func perform() 5 | } 6 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/NewExample.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Tests/Resources/StringsFiles/NewExample.strings -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/OldExample.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlineDev/BartyCrouch/HEAD/Tests/Resources/StringsFiles/OldExample.strings -------------------------------------------------------------------------------- /Sources/BartyCrouchUtility/Transformer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Transformer: String, CaseIterable { 4 | case foundation 5 | case swiftgenStructured 6 | } 7 | -------------------------------------------------------------------------------- /Sources/BartyCrouchUtility/Secret.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Secret: Equatable { 4 | case microsoftTranslator(secret: String) 5 | case deepL(secret: String) 6 | } 7 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/DeeplApi/Model/DeepLTranslateErrorResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct DeepLTranslateErrorResponse: Decodable { 4 | let message: String 5 | } 6 | -------------------------------------------------------------------------------- /Tests/SupportingFiles/BartyCrouchKitTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cihat Gündüz on 11.02.16. 3 | // Copyright © 2016 Flinesoft. All rights reserved. 4 | // 5 | 6 | #import "Constants.h" 7 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/TomlCodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Toml 3 | 4 | protocol TomlCodable { 5 | static func make(toml: Toml) throws -> Self 6 | func tomlContents() -> String 7 | } 8 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* 2 | InfoPlist.strings 3 | BartyCrouch 4 | 5 | Created by Clément Padovani on 1/14/18. 6 | Copyright © 2018 Flinesoft. All rights reserved. 7 | */ 8 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/MicrosoftTranslatorApi/Models/TranslateRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // swiftlint:disable identifier_name 4 | 5 | struct TranslateRequest: Encodable { 6 | let Text: String 7 | } 8 | -------------------------------------------------------------------------------- /Demo/Untouched/DemoUITests/DemoUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class DemoUITests: XCTestCase { 4 | func testExample() { 5 | NSLocalizedString("UI Tests Love", comment: "Comment for UI Tests Love") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Demo/Untouched/DemoTests/DemoTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Demo 3 | 4 | class DemoTests: XCTestCase { 5 | func testExample() { 6 | NSLocalizedString("Tests Love", comment: "Comment for Tests Love") 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/BartyCrouchTranslatorTests/Secrets/secrets.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "deepLApiKey": "", 3 | "microsoftSubscriptionKey": "" 4 | } 5 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | /* A string already localized in all languages. */ 3 | "Test key" = "Test value (de)"; 4 | 5 | /* A string where value only available in English. */ 6 | "menu.cars" = ""; 7 | 8 | "TEST.KEY.UNESCAPED_DOUBLE_QUOTES" = ""; 9 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | /* A string already localized in all languages. */ 3 | "Test key" = "Test value (ja)"; 4 | 5 | /* A string where value only available in English. */ 6 | "menu.cars" = ""; 7 | 8 | "TEST.KEY.UNESCAPED_DOUBLE_QUOTES" = ""; 9 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | /* A string already localized in all languages. */ 3 | "Test key" = "Test value (zh-Hans)"; 4 | 5 | /* A string where value only available in English. */ 6 | "menu.cars" = ""; 7 | 8 | "TEST.KEY.UNESCAPED_DOUBLE_QUOTES" = ""; 9 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/MicrosoftTranslatorApi/Models/TranslateResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct TranslateResponse: Decodable { 4 | struct Translation: Decodable { 5 | let text: String 6 | let to: String 7 | } 8 | 9 | let translations: [Translation] 10 | } 11 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/DeeplApi/Model/DeepLTranslateResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct DeepLTranslateResponse: Decodable { 4 | struct Translation: Decodable { 5 | let detectedSourceLanguage: String 6 | let text: String 7 | } 8 | 9 | let translations: [Translation] 10 | } 11 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // Created by Frederick Pietschmann on 15.02.20. 2 | 3 | import Foundation 4 | 5 | extension Array where Element: Hashable { 6 | func withoutDuplicates() -> Array { 7 | var seen = [Element: Bool]() 8 | return filter { seen.updateValue(true, forKey: $0) == nil } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/CommandExecution.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CommandExecution { 4 | static let current = CommandExecution() 5 | 6 | var didPrintWarning: Bool = false 7 | 8 | func failIfNeeded() { 9 | if GlobalOptions.failOnWarnings.value && didPrintWarning { 10 | exit(EXIT_FAILURE) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 8 | return true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/Env.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let env = Env() // swiftlint:disable:this file_types_order 4 | 5 | struct Env { 6 | fileprivate init() {} 7 | 8 | subscript(key: String) -> String? { 9 | let env = ProcessInfo.processInfo.environment 10 | guard env.keys.contains(key) else { return nil } 11 | return env[key] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFiles/UnsortedKeys/SwiftExample3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample3.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 27.08.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample3 { 12 | func exampleFunction1() { 13 | NSLocalizedString("ccc", comment: "") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Extensions/TomlExtension.swift: -------------------------------------------------------------------------------- 1 | // Created by Frederick Pietschmann on 15.02.20. 2 | 3 | import Foundation 4 | import Toml 5 | 6 | public extension Toml { 7 | func filePaths(_ path: String..., singularKey: String, pluralKey: String) -> [String] { 8 | return array(path + [pluralKey]) ?? string(path + [singularKey]).map { [$0] } ?? ["."] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCode/2Arguments/SwiftExample2Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample2Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample2Arguments() { 12 | _ = NSLocalizedString("test", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Demo 4 | 5 | Created by Cihat Gündüz on 18.01.19. 6 | Copyright © 2019 Flinesoft. All rights reserved. 7 | */ 8 | 9 | "Existing Translation Key" = "Existing Value"; 10 | 11 | "Existing Duplicate Key" = "Value 1"; 12 | 13 | "Existing Duplicate Key" = "Value 2"; 14 | 15 | "Existing Empty Value Key" = ""; 16 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/tr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Demo 4 | 5 | Created by Cihat Gündüz on 18.01.19. 6 | Copyright © 2019 Flinesoft. All rights reserved. 7 | */ 8 | 9 | "Existing Translation Key" = "Existing Value"; 10 | 11 | "Existing Duplicate Key" = "Value 1"; 12 | 13 | "Existing Duplicate Key" = "Value 2"; 14 | 15 | "Existing Empty Value Key" = ""; 16 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFilesCustomFunction/UnsortedKeys/SwiftExample3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample3.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 27.08.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample3 { 12 | func exampleFunction1() { 13 | BCLocalizedString("ccc", comment: "") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | /* A string already localized in all languages. */ 3 | "Test key" = "Test value (en)"; 4 | 5 | /* A string where value only available in English. */ 6 | "menu.cars" = "Cars"; 7 | 8 | /* A string where key only available in English. */ 9 | "menu.bicycles" = "Bicycles"; 10 | 11 | "TEST.KEY.UNESCAPED_DOUBLE_QUOTES" = "She said: 'Stop!'"; -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCodeCustomFunction/2Arguments/SwiftExample2Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample2Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample2Arguments() { 12 | _ = BCLocalizedString("test", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCode/3Arguments/SwiftExample3Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample3Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample3Arguments() { 12 | _ = NSLocalizedString("test", value: "test value", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCodeCustomFunction/3Arguments/SwiftExample3Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample3Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample3Arguments() { 12 | _ = BCLocalizedString("test", value: "test value", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var absolutePath: String { 5 | return URL(fileURLWithPath: self).path 6 | } 7 | 8 | func firstCharacterLowercased() -> String { 9 | let firstCharacter = prefix(1) 10 | let leftoverString = suffix(from: firstCharacter.endIndex) 11 | return firstCharacter.lowercased() + leftoverString 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCode/4Arguments/SwiftExample4Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample4Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample4Arguments() { 12 | _ = NSLocalizedString("test", tableName: "Localizable", value: "test value", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Demo 4 | 5 | Created by Cihat Gündüz on 18.01.19. 6 | Copyright © 2019 Flinesoft. All rights reserved. 7 | */ 8 | 9 | "Existing Translation Key" = "Existing Value"; 10 | 11 | "Existing Duplicate Key" = "Value 1"; 12 | 13 | "Existing Duplicate Key" = "Value 2"; 14 | 15 | "Existing Empty Value Key" = ""; 16 | 17 | "Existing Only in English Key" = "Existing Value"; 18 | -------------------------------------------------------------------------------- /Sources/BartyCrouch/main.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchKit 2 | import Foundation 3 | import SwiftCLI 4 | 5 | // MARK: - CLI 6 | let cli = CLI( 7 | name: "bartycrouch", 8 | version: "4.15.0", 9 | description: "Incrementally update & translate your Strings files from code or interface files." 10 | ) 11 | 12 | cli.commands = [InitCommand(), UpdateCommand(), LintCommand()] 13 | cli.globalOptions.append(contentsOf: GlobalOptions.all) 14 | cli.goAndExit() 15 | -------------------------------------------------------------------------------- /Tests/Resources/MultipleArgumentsCodeCustomFunction/4Arguments/SwiftExample4Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample4Arguments.swift 3 | // BartyCrouch 4 | // 5 | // Created by Fyodor Volchyok on 12/9/16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | func swiftExample4Arguments() { 12 | _ = BCLocalizedString("test", tableName: "Localizable", value: "test value", comment: "test comment") 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/macOS/de.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSButtonCell"; title = "Example Button 2"; ObjectID = "3iP-3C-50Y"; */ 3 | "3iP-3C-50Y.title" = "Example Button 2"; 4 | 5 | /* Class = "NSButtonCell"; title = "Example Button 3"; ObjectID = "DAv-nc-u12"; */ 6 | "DAv-nc-u12.title" = "Example Button 3"; 7 | 8 | /* Class = "NSButtonCell"; title = "Example Button 1"; ObjectID = "xqQ-fi-vE6"; */ 9 | "xqQ-fi-vE6.title" = "Example Button 1"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/macOS/en.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSButtonCell"; title = "Example Button 2"; ObjectID = "3iP-3C-50Y"; */ 3 | "3iP-3C-50Y.title" = "Example Button 2"; 4 | 5 | /* Class = "NSButtonCell"; title = "Example Button 3"; ObjectID = "DAv-nc-u12"; */ 6 | "DAv-nc-u12.title" = "Example Button 3"; 7 | 8 | /* Class = "NSButtonCell"; title = "Example Button 1"; ObjectID = "xqQ-fi-vE6"; */ 9 | "xqQ-fi-vE6.title" = "Example Button 1"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/macOS/ja.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSButtonCell"; title = "Example Button 2"; ObjectID = "3iP-3C-50Y"; */ 3 | "3iP-3C-50Y.title" = "Example Button 2"; 4 | 5 | /* Class = "NSButtonCell"; title = "Example Button 3"; ObjectID = "DAv-nc-u12"; */ 6 | "DAv-nc-u12.title" = "Example Button 3"; 7 | 8 | /* Class = "NSButtonCell"; title = "Example Button 1"; ObjectID = "xqQ-fi-vE6"; */ 9 | "xqQ-fi-vE6.title" = "Example Button 1"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/macOS/zh-Hans.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "NSButtonCell"; title = "Example Button 2"; ObjectID = "3iP-3C-50Y"; */ 3 | "3iP-3C-50Y.title" = "Example Button 2"; 4 | 5 | /* Class = "NSButtonCell"; title = "Example Button 3"; ObjectID = "DAv-nc-u12"; */ 6 | "DAv-nc-u12.title" = "Example Button 3"; 7 | 8 | /* Class = "NSButtonCell"; title = "Example Button 1"; ObjectID = "xqQ-fi-vE6"; */ 9 | "xqQ-fi-vE6.title" = "Example Button 1"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/StringsFiles/UnsortedKeys/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | /* No comment provided by engineer. */ 3 | "DDD" = ""; 4 | 5 | /* No comment provided by engineer. */ 6 | "ggg" = "\n"; 7 | 8 | /* No comment provided by engineer. */ 9 | "BBB" = "value"; 10 | 11 | /* No comment provided by engineer. */ 12 | "aaa" = " "; 13 | 14 | /* No comment provided by engineer. */ 15 | "FFF" = "fff"; 16 | 17 | /* No comment provided by engineer. */ 18 | "eee" = "test"; 19 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/iOS/de.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "35F-cl-mdI"; */ 3 | "35F-cl-mdI.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "COa-YO-eGf"; */ 6 | "COa-YO-eGf.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "cHL-Zc-L39"; */ 9 | "cHL-Zc-L39.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/iOS/en.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "35F-cl-mdI"; */ 3 | "35F-cl-mdI.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "COa-YO-eGf"; */ 6 | "COa-YO-eGf.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "cHL-Zc-L39"; */ 9 | "cHL-Zc-L39.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/iOS/ja.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "35F-cl-mdI"; */ 3 | "35F-cl-mdI.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "COa-YO-eGf"; */ 6 | "COa-YO-eGf.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "cHL-Zc-L39"; */ 9 | "cHL-Zc-L39.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/tvOS/de.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "3Zb-h3-chj"; */ 3 | "3Zb-h3-chj.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "MFq-ZF-sBn"; */ 6 | "MFq-ZF-sBn.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "sdg-ig-q5P"; */ 9 | "sdg-ig-q5P.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/tvOS/en.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "3Zb-h3-chj"; */ 3 | "3Zb-h3-chj.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "MFq-ZF-sBn"; */ 6 | "MFq-ZF-sBn.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "sdg-ig-q5P"; */ 9 | "sdg-ig-q5P.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/tvOS/ja.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "3Zb-h3-chj"; */ 3 | "3Zb-h3-chj.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "MFq-ZF-sBn"; */ 6 | "MFq-ZF-sBn.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "sdg-ig-q5P"; */ 9 | "sdg-ig-q5P.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/iOS/zh-Hans.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "35F-cl-mdI"; */ 3 | "35F-cl-mdI.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "COa-YO-eGf"; */ 6 | "COa-YO-eGf.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "cHL-Zc-L39"; */ 9 | "cHL-Zc-L39.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/tvOS/zh-Hans.lproj/Example.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIButton"; normalTitle = "Example Button 1"; ObjectID = "3Zb-h3-chj"; */ 3 | "3Zb-h3-chj.normalTitle" = "Example Button 1"; 4 | 5 | /* Class = "UIButton"; normalTitle = "Example Button 2"; ObjectID = "MFq-ZF-sBn"; */ 6 | "MFq-ZF-sBn.normalTitle" = "Example Button 2"; 7 | 8 | /* Class = "UIButton"; normalTitle = "Example Button 3"; ObjectID = "sdg-ig-q5P"; */ 9 | "sdg-ig-q5P.normalTitle" = "Example Button 3"; 10 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "lineBreakAroundMultilineExpressionChainComponents": true, 3 | "lineBreakBeforeControlFlowKeywords": true, 4 | "lineBreakBeforeEachArgument": true, 5 | "lineBreakBeforeEachGenericRequirement": true, 6 | "lineLength": 120, 7 | "prioritizeKeepingFunctionOutputTogether": true, 8 | "rules": { 9 | "NeverUseImplicitlyUnwrappedOptionals": true, 10 | "NoLeadingUnderscores": true, 11 | "ValidateDocumentationComments": true, 12 | }, 13 | "tabWidth": 2, 14 | "version": 1, 15 | } 16 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/en.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UILabel"; text = "Label to Ignore\n(see Comment for Localizer)"; ObjectID = "Ibu-xm-woE"; Note = "#bc-ignore!"; */ 3 | "Ibu-xm-woE.text" = "Label to Ignore\n(see Comment for Localizer)"; 4 | 5 | /* Class = "UILabel"; text = "Label to Translate"; ObjectID = "cGW-hC-L0h"; */ 6 | "cGW-hC-L0h.text" = "Label to Translate"; 7 | 8 | /* Class = "UILabel"; text = "Label to Ignore #bc-ignore!"; ObjectID = "dgI-jn-hzN"; */ 9 | "dgI-jn-hzN.text" = "Label to Ignore #bc-ignore!"; 10 | -------------------------------------------------------------------------------- /Sources/SupportingFiles/BartyCrouch.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cihat Gündüz on 10.02.16. 3 | // Copyright © 2016 Flinesoft. All rights reserved. 4 | // 5 | 6 | #import 7 | 8 | //! Project version number for BartyCrouch. 9 | FOUNDATION_EXPORT double BartyCrouchVersionNumber; 10 | 11 | //! Project version string for BartyCrouch. 12 | FOUNDATION_EXPORT const unsigned char BartyCrouchVersionString[]; 13 | 14 | // In this header, you should import all the public headers of your framework using statements like #import 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Commands/InitCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public class InitCommand: Command { 5 | // MARK: - Command 6 | public let name: String = "init" 7 | public let shortDescription: String = 8 | "Creates the default configuration file & creates a build script if Xcode project found" 9 | 10 | // MARK: - Initializers 11 | public init() {} 12 | 13 | // MARK: - Instance Methods 14 | public func execute() throws { 15 | GlobalOptions.setup() 16 | InitTaskHandler().perform() 17 | CommandExecution.current.failIfNeeded() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/LintTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct LintTaskHandler { 5 | let options: LintOptions 6 | } 7 | 8 | extension LintTaskHandler: TaskHandler { 9 | func perform() { 10 | measure(task: "Lint") { 11 | mungo.do { 12 | CommandLineActor() 13 | .actOnLint( 14 | paths: options.paths, 15 | subpathsToIgnore: options.subpathsToIgnore, 16 | duplicateKeys: options.duplicateKeys, 17 | emptyValues: options.emptyValues 18 | ) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFiles/Subfolder/SwiftExample2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample2.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 03.05.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample2 { 12 | func exampleFunction2() { 13 | NSLocalizedString("TestKey2", comment: "Comment for TestKey1") 14 | String(format: NSLocalizedString("%010d and %03.f", comment: ""), 25, 89.5) 15 | String.localizableStringWithFormat( 16 | NSLocalizedString("%d ignore(s)", comment: "Ignoring stringsdict key #bc-ignore!"), 17 | 25 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFiles/Subfolder/Subfolder/SwiftExample3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample2.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 03.05.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample2 { 12 | func exampleFunction2() { 13 | NSLocalizedString("TestKey2", comment: "Comment for TestKey1") 14 | String(format: NSLocalizedString("%010d and %03.f", comment: ""), 25, 89.5) 15 | String.localizableStringWithFormat( 16 | NSLocalizedString("%d ignore(s)", comment: "Ignoring stringsdict key #bc-ignore!"), 17 | 25 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Commands/LintCommand.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | import SwiftCLI 4 | 5 | public class LintCommand: Command { 6 | // MARK: - Command 7 | public let name: String = "lint" 8 | public let shortDescription: String = "Lints your .strings file contents" 9 | 10 | // MARK: - Initializers 11 | public init() {} 12 | 13 | // MARK: - Instance Methods 14 | public func execute() throws { 15 | GlobalOptions.setup() 16 | let lintOptions = try Configuration.load().lintOptions 17 | LintTaskHandler(options: lintOptions).perform() 18 | CommandExecution.current.failIfNeeded() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFilesCustomFunction/Subfolder/SwiftExample2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample2.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 03.05.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample2 { 12 | func exampleFunction2() { 13 | BCLocalizedString("TestKey2", comment: "Comment for TestKey1") 14 | String(format: BCLocalizedString("%010d and %03.f", comment: ""), 25, 89.5) 15 | String.localizableStringWithFormat( 16 | BCLocalizedString("%d ignore(s)", comment: "Ignoring stringsdict key #bc-ignore!"), 17 | 25 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/BartyCrouchUtility/Constants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// All constants needed in BartyCrouchKit are collected in one place. 4 | public enum Constants { 5 | /// These keys can be places in the 'Comment for Localizer' in Interface Builder files to signal that they should be ignored from adding to Strings files. 6 | public static let defaultIgnoreKeys: [String] = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] 7 | 8 | /// Paths to be ignored while searching for code files to consider as sources for new translation keys added to the project. 9 | public static let defaultSubpathsToIgnore: [String] = [".git", "carthage", "pods", "build", ".build", "docs"] 10 | } 11 | -------------------------------------------------------------------------------- /Tests/BartyCrouchTranslatorTests/Secrets/Secrets.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Secrets: Decodable { 4 | let deepLApiKey: String 5 | let microsoftSubscriptionKey: String 6 | 7 | static func load() throws -> Self { 8 | let secretsFileUrl = Bundle.module.url(forResource: "secrets", withExtension: "json") 9 | 10 | guard let secretsFileUrl = secretsFileUrl, let secretsFileData = try? Data(contentsOf: secretsFileUrl) else { 11 | fatalError( 12 | "No `secrets.json` file found. Make sure to duplicate `secrets.json.sample` and remove the `.sample` extension." 13 | ) 14 | } 15 | 16 | return try JSONDecoder().decode(Self.self, from: secretsFileData) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/TestHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A helper class for Unit Testing only. Only put data in here when `isStartedByUnitTests` is set to true. 4 | /// Never read other data in framework than that property. 5 | final class TestHelper { 6 | typealias PrintOutput = (message: String, level: PrintLevel, file: String?, line: Int?) 7 | 8 | static let shared = TestHelper() 9 | 10 | /// Set to `true` within unit tests (in `setup()`). Defaults to `false`. 11 | var isStartedByUnitTests: Bool = false 12 | 13 | /// Use only in Unit Tests. 14 | var printOutputs: [PrintOutput] = [] 15 | 16 | /// Deletes all data collected until now. 17 | func reset() { 18 | printOutputs = [] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFiles/SwiftExample1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample1.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 03.05.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample1 { 12 | func exampleFunction1() { 13 | NSLocalizedString("TestKey1", comment: "Comment for TestKey1") 14 | String(format: NSLocalizedString("%@ and %.2f", comment: ""), "SomeString", 25.752_893_8) 15 | 16 | let testString1 = NSLocalizedString("test.multiline_comment", comment: "test comment 1") 17 | let testString2 = NSLocalizedString("test.multiline_comment", comment: "test comment 2") 18 | 19 | NSLocalizedString("test.brackets_comment", comment: "(test comment with brackets)") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Resources/CodeFilesCustomFunction/SwiftExample1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftExample1.swift 3 | // BartyCrouch 4 | // 5 | // Created by Cihat Gündüz on 03.05.16. 6 | // Copyright © 2016 Flinesoft. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SwiftExample1 { 12 | func exampleFunction1() { 13 | BCLocalizedString("TestKey1", comment: "Comment for TestKey1") 14 | String(format: BCLocalizedString("%@ and %.2f", comment: ""), "SomeString", 25.752_893_8) 15 | 16 | let testString1 = BCLocalizedString("test.multiline_comment", comment: "test comment 1") 17 | let testString2 = BCLocalizedString("test.multiline_comment", comment: "test comment 2") 18 | 19 | BCLocalizedString("test.brackets_comment", comment: "(test comment with brackets)") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Demo/Untouched/DemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Demo/Untouched/DemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/InterfacesTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct InterfacesTaskHandler { 5 | let options: InterfacesOptions 6 | } 7 | 8 | extension InterfacesTaskHandler: TaskHandler { 9 | func perform() { 10 | measure(task: "Update Interfaces") { 11 | mungo.do { 12 | CommandLineActor() 13 | .actOnInterfaces( 14 | paths: options.paths, 15 | subpathsToIgnore: options.subpathsToIgnore, 16 | override: false, 17 | verbose: GlobalOptions.verbose.value, 18 | defaultToBase: options.defaultToBase, 19 | unstripped: options.unstripped, 20 | ignoreEmptyStrings: options.ignoreEmptyStrings, 21 | ignoreKeys: options.ignoreKeys 22 | ) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/NormalizeTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct NormalizeTaskHandler { 5 | let options: NormalizeOptions 6 | } 7 | 8 | extension NormalizeTaskHandler: TaskHandler { 9 | func perform() { 10 | measure(task: "Normalize") { 11 | mungo.do { 12 | CommandLineActor() 13 | .actOnNormalize( 14 | paths: options.paths, 15 | subpathsToIgnore: options.subpathsToIgnore, 16 | override: false, 17 | verbose: GlobalOptions.verbose.value, 18 | locale: options.sourceLocale, 19 | sortByKeys: options.sortByKeys, 20 | harmonizeWithSource: options.harmonizeWithSource, 21 | separateWithEmptyLine: options.separateWithEmptyLine 22 | ) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/OldCommandLine/IBToolCommander.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | // NOTE: 5 | // This file was not refactored as port of the work/big-refactoring branch for version 4.0 to prevent unexpected behavior changes. 6 | // A rewrite after writing extensive tests for the expected behavior could improve readebility, extensibility and performance. 7 | 8 | /// Sends `ibtool` commands with specified input/output paths to bash. 9 | public final class IBToolCommander { 10 | // MARK: - Stored Type Properties 11 | public static let shared = IBToolCommander() 12 | 13 | // MARK: - Instance Methods 14 | public func export(stringsFileToPath stringsFilePath: String, fromIbFileAtPath ibFilePath: String) throws { 15 | let arguments = ["--export-strings-file", stringsFilePath, ibFilePath] 16 | try Task.run("/usr/bin/ibtool", arguments: arguments) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/TranslateTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct TranslateTaskHandler { 5 | let options: TranslateOptions 6 | } 7 | 8 | extension TranslateTaskHandler: TaskHandler { 9 | func perform() { 10 | // TODO: add support for multiple APIs (currently not in the parameter list of actOnTranslate) 11 | 12 | measure(task: "Translate") { 13 | mungo.do { 14 | CommandLineActor() 15 | .actOnTranslate( 16 | paths: options.paths, 17 | subpathsToIgnore: options.subpathsToIgnore, 18 | override: false, 19 | verbose: GlobalOptions.verbose.value, 20 | secret: options.secret, 21 | locale: options.sourceLocale, 22 | separateWithEmptyLine: self.options.separateWithEmptyLine 23 | ) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # Xcode 29 | ## User settings 30 | xcuserdata/ 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | 35 | ## App packaging 36 | *.ipa 37 | *.dSYM.zip 38 | *.dSYM 39 | 40 | ## Playgrounds 41 | timeline.xctimeline 42 | playground.xcworkspace 43 | 44 | # Swift Package Manager 45 | Packages/ 46 | Package.pins 47 | Package.resolved 48 | *.xcodeproj 49 | .swiftpm 50 | .build/ 51 | 52 | # Project-specific 53 | secrets.json 54 | -------------------------------------------------------------------------------- /Sources/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.15.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2016-2018 Flinesoft. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController { 4 | override func viewDidLoad() { 5 | super.viewDidLoad() 6 | 7 | NSLocalizedString("Love", comment: "Comment for Love") 8 | NSLocalizedString("How are you?", comment: "") 9 | NSLocalizedString("I'm fine", comment: "Comment for I'm fine - #bc-ignore!") 10 | 11 | title = BartyCrouch.translate(key: "onboarding.first-page.header-title", translations: [.english: "Page Title", .german: "Seitentitel"]) 12 | let lines: Int = (0 ..< 10).map { "\($0 + 1): \(BartyCrouch.translate(key: "onboarding.first-page.line", translations: [:], comment: "Line Comment"))" }.count 13 | 14 | BartyCrouch 15 | .translate( 16 | key : "ShortKey", 17 | translations : [ 18 | BartyCrouch.SupportedLanguage.english : 19 | "Some Translation" 20 | ] 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/GlobalOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | public enum GlobalOptions { 5 | static let verbose = Flag( 6 | "-v", 7 | "--verbose", 8 | description: "Prints more detailed information about the executed command" 9 | ) 10 | static let xcodeOutput = Flag( 11 | "-x", 12 | "--xcode-output", 13 | description: "Prints warnings & errors in Xcode compatible format" 14 | ) 15 | static let failOnWarnings = Flag( 16 | "-w", 17 | "--fail-on-warnings", 18 | description: "Returns a failed status code if any warning is encountered" 19 | ) 20 | static let path = Key( 21 | "-p", 22 | "--path", 23 | description: "Specifies a different path than current to run BartyCrouch from there" 24 | ) 25 | 26 | public static var all: [Option] { 27 | return [verbose, xcodeOutput, failOnWarnings, path] 28 | } 29 | 30 | static func setup() { 31 | if let path = path.value { 32 | FileManager.default.changeCurrentDirectoryPath(path) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/Helpers/FileManagerExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | 4 | extension FileManager { 5 | func removeContentsOfDirectory(at url: URL, options mask: FileManager.DirectoryEnumerationOptions = []) throws { 6 | guard fileExists(atPath: url.path) else { return } 7 | for suburl in try contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: mask) { 8 | try FileManager.default.removeItem(at: suburl) 9 | } 10 | } 11 | 12 | func createFile( 13 | atPath path: String, 14 | withIntermediateDirectories: Bool, 15 | contents: Data?, 16 | attributes: [FileAttributeKey: Any]? = nil 17 | ) throws { 18 | let directoryUrl = URL(fileURLWithPath: path).deletingLastPathComponent() 19 | 20 | if withIntermediateDirectories && !FileManager.default.fileExists(atPath: directoryUrl.path) { 21 | try createDirectory(at: directoryUrl, withIntermediateDirectories: true, attributes: attributes) 22 | } 23 | 24 | createFile(atPath: path, contents: contents, attributes: attributes) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/BartyCrouchTranslatorTests/DeepLTranslatorApiTests.swift: -------------------------------------------------------------------------------- 1 | @testable import BartyCrouchTranslator 2 | import Foundation 3 | import Microya 4 | import XCTest 5 | 6 | class DeepLTranslatorApiTests: XCTestCase { 7 | func testTranslate() { 8 | let apiKey = try! Secrets.load().deepLApiKey // swiftlint:disable:this force_try 9 | guard !apiKey.isEmpty else { return } 10 | 11 | let endpoint = DeepLApi.translate( 12 | texts: ["How old are you?", "Love"], 13 | from: .english, 14 | to: .german, 15 | apiKey: apiKey 16 | ) 17 | 18 | let apiProvider = ApiProvider(baseUrl: DeepLApi.baseUrl(for: .free)) 19 | 20 | switch apiProvider.performRequestAndWait(on: endpoint, decodeBodyTo: DeepLTranslateResponse.self) { 21 | case let .success(translateResponses): 22 | XCTAssertEqual(translateResponses.translations[0].text, "Wie alt sind Sie?") 23 | XCTAssertEqual(translateResponses.translations[1].text, "Liebe") 24 | 25 | case let .failure(failure): 26 | XCTFail(failure.localizedDescription) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/SupportedLanguagesReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import HandySwift 3 | import SwiftSyntax 4 | 5 | class SupportedLanguagesReader: SyntaxVisitor { 6 | let typeName: String 7 | var caseToLangCode: [String: String] = [:] 8 | 9 | init( 10 | typeName: String 11 | ) { 12 | self.typeName = typeName 13 | super.init(viewMode: .sourceAccurate) 14 | } 15 | 16 | override func visit(_ enumDeclaration: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 17 | if enumDeclaration.parent?.as(CodeBlockItemSyntax.self) != nil 18 | || enumDeclaration.identifier.text == "SupportedLanguage" 19 | { 20 | return .visitChildren 21 | } 22 | else { 23 | return .skipChildren 24 | } 25 | } 26 | 27 | override func visit(_ enumCaseElement: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind { 28 | if let langCodeLiteral = enumCaseElement.rawValue?.value.as(StringLiteralExprSyntax.self) { 29 | caseToLangCode[enumCaseElement.identifier.text] = langCodeLiteral.text 30 | } 31 | 32 | return .skipChildren 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/CodeTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct CodeTaskHandler { 5 | let options: CodeOptions 6 | } 7 | 8 | extension CodeTaskHandler: TaskHandler { 9 | func perform() { 10 | measure(task: "Update Code") { 11 | mungo.do { 12 | CommandLineActor() 13 | .actOnCode( 14 | paths: options.codePaths, 15 | subpathsToIgnore: options.subpathsToIgnore, 16 | override: false, 17 | verbose: GlobalOptions.verbose.value, 18 | localizables: options.localizablePaths, 19 | defaultToKeys: options.defaultToKeys, 20 | additive: options.additive, 21 | overrideComments: options.overrideComments, 22 | unstripped: options.unstripped, 23 | customFunction: options.customFunction, 24 | customLocalizableName: options.customLocalizableName, 25 | usePlistArguments: options.plistArguments, 26 | ignoreKeys: options.ignoreKeys 27 | ) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/InitTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct InitTaskHandler { 5 | /* for extension purposes only */ 6 | } 7 | 8 | extension InitTaskHandler: TaskHandler { 9 | func perform() { 10 | createDefaultConfigFile() 11 | } 12 | 13 | func createDefaultConfigFile() { 14 | measure(task: "Default Config Creation") { 15 | mungo.do { 16 | let configUrl: URL = Configuration.configUrl 17 | 18 | guard !FileManager.default.fileExists(atPath: configUrl.path) else { 19 | print("File at path \(configUrl.path) already exists. Skipping creation.", level: .warning) 20 | return 21 | } 22 | 23 | let defaultConfiguration: Configuration = try Configuration.makeDefault() 24 | let configurationContents: String = defaultConfiguration.tomlContents() 25 | try configurationContents.write(to: configUrl, atomically: true, encoding: .utf8) 26 | 27 | print("Successfully created file \(Configuration.fileName)", level: .success) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Formula/bartycrouch.rb: -------------------------------------------------------------------------------- 1 | class Bartycrouch < Formula 2 | desc "Incrementally update/translate your Strings files" 3 | homepage "https://github.com/Flinesoft/BartyCrouch" 4 | url "https://github.com/Flinesoft/BartyCrouch.git", :tag => "4.15.0", :revision => "a2fc2f6689228342c6203542257a2f3931b405dc" 5 | head "https://github.com/Flinesoft/BartyCrouch.git" 6 | 7 | depends_on :xcode => ["14.0", :build] 8 | 9 | def install 10 | system "make", "install", "prefix=#{prefix}" 11 | end 12 | 13 | test do 14 | (testpath/"Test.swift").write <<~EOS 15 | import Foundation 16 | 17 | class Test { 18 | func test() { 19 | NSLocalizedString("test", comment: "") 20 | } 21 | } 22 | EOS 23 | 24 | (testpath/"en.lproj/Localizable.strings").write <<~EOS 25 | /* No comment provided by engineer. */ 26 | "oldKey" = "Some translation"; 27 | EOS 28 | 29 | system bin/"bartycrouch", "update" 30 | assert_match /"oldKey" = "/, File.read("en.lproj/Localizable.strings") 31 | assert_match /"test" = "/, File.read("en.lproj/Localizable.strings") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2020 Flinesoft (alias Cihat Gündüz) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/FindFilesTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //class FindFilesTests: XCTestCase { 5 | // class FileFinder: CodeCommander { 6 | // func export(stringsFilesToPath stringsFilePath: String, fromCodeInDirectoryPath codeDirectoryPath: String, customFunction: String?) -> Bool { 7 | // return false 8 | // } 9 | // } 10 | // 11 | // func testFindFiles() { 12 | // let finder = FileFinder() 13 | // 14 | // let basePath = "\(BASE_DIR)/Tests/Resources/CodeFiles" 15 | // 16 | // var expectedStringsFilePaths: [String] = [ 17 | // "Subfolder/Subfolder/SwiftExample3.swift", 18 | // "Subfolder/SwiftExample2.swift", 19 | // "SwiftExample1.swift", 20 | // "UnsortedKeys/SwiftExample3.swift" 21 | // ] 22 | // expectedStringsFilePaths = expectedStringsFilePaths.map { "\(basePath)/\($0)" } 23 | // 24 | // let results = finder.findFiles(in: basePath).outputs 25 | // XCTAssertEqual(results.count, expectedStringsFilePaths.count) 26 | // XCTAssertEqual(results.sorted(), expectedStringsFilePaths.sorted()) 27 | // } 28 | //} 29 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/LintOptions.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import MungoHealer 4 | import Toml 5 | 6 | public struct LintOptions { 7 | public let paths: [String] 8 | public let subpathsToIgnore: [String] 9 | public let duplicateKeys: Bool 10 | public let emptyValues: Bool 11 | } 12 | 13 | extension LintOptions: TomlCodable { 14 | static func make(toml: Toml) throws -> LintOptions { 15 | let lint: String = "lint" 16 | 17 | return LintOptions( 18 | paths: toml.filePaths(lint, singularKey: "path", pluralKey: "paths"), 19 | subpathsToIgnore: toml.array(lint, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore, 20 | duplicateKeys: toml.bool(lint, "duplicateKeys") ?? true, 21 | emptyValues: toml.bool(lint, "emptyValues") ?? true 22 | ) 23 | } 24 | 25 | func tomlContents() -> String { 26 | var lines: [String] = ["[lint]"] 27 | 28 | lines.append("paths = \(paths)") 29 | lines.append("subpathsToIgnore = \(subpathsToIgnore)") 30 | lines.append("duplicateKeys = \(duplicateKeys)") 31 | lines.append("emptyValues = \(emptyValues)") 32 | 33 | return lines.joined(separator: "\n") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/CommandLineErrorHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MungoHealer 3 | 4 | let mungo = MungoHealer(errorHandler: CommandLineErrorHandler()) 5 | 6 | struct CommandLineErrorHandler: ErrorHandler { 7 | func handle(error: Error) { 8 | log(error, level: .warning) 9 | } 10 | 11 | func handle(baseError: BaseError) { 12 | log(baseError, level: .warning) 13 | } 14 | 15 | func handle(fatalError: FatalError) { 16 | log(fatalError, level: .error) 17 | crash() 18 | } 19 | 20 | func handle(healableError: HealableError) { // swiftlint:disable:this unavailable_function 21 | log(healableError, level: .info) 22 | fatalError("Healable Errors not supported by \(String(describing: CommandLineErrorHandler.self)).") 23 | } 24 | 25 | private func log(_ error: Error, level: PrintLevel) { 26 | if GlobalOptions.verbose.value, let baseError = error as? BaseError, 27 | let debugDescription = baseError.debugDescription, !debugDescription.isBlank 28 | { 29 | print("\(error.localizedDescription) | Details: \(debugDescription)", level: level) 30 | } 31 | else { 32 | print(error.localizedDescription, level: level) 33 | } 34 | } 35 | 36 | private func crash() { 37 | exit(EXIT_FAILURE) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/FileHandling/CodeFilesSearchTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BartyCrouchKit 3 | 4 | final class CodeFilesSearchTests: XCTestCase { 5 | func testShouldSkipFile() { 6 | let codeFilesSearch = CodeFilesSearch(baseDirectoryPath: "/") 7 | 8 | let sampleFileUrl = URL( 9 | fileURLWithPath: "/Users/Me/Developer/Project A/Pods/Lib A/Sources/Supporting Files/InfoPlist.strings" 10 | ) 11 | 12 | XCTAssertTrue(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["pods"])) 13 | XCTAssertTrue(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["InfoPlist.strings"])) 14 | XCTAssertTrue(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["pods", "InfoPlist.strings"])) 15 | XCTAssertTrue(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["Sources/Supporting Files"])) 16 | 17 | XCTAssertFalse(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: [])) 18 | XCTAssertFalse(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["InfoPlist"])) 19 | XCTAssertFalse(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: [".strings"])) 20 | XCTAssertFalse(codeFilesSearch.shouldSkipFile(at: sampleFileUrl, subpathsToIgnore: ["Sources/Resources"])) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Commands/UpdateCommand.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | import SwiftCLI 4 | 5 | public class UpdateCommand: Command { 6 | // MARK: - Command 7 | public let name: String = "update" 8 | public let shortDescription: String = 9 | "Update your .strings file contents with the configured tasks (default: interfaces, code, normalize)" 10 | 11 | // MARK: - Initializers 12 | public init() {} 13 | 14 | // MARK: - Instance Methods 15 | public func execute() throws { 16 | GlobalOptions.setup() 17 | let updateOptions = try Configuration.load().updateOptions 18 | 19 | for task in updateOptions.tasks { 20 | let taskHandler: TaskHandler = { 21 | switch task { 22 | case .interfaces: 23 | return InterfacesTaskHandler(options: updateOptions.interfaces) 24 | 25 | case .code: 26 | return CodeTaskHandler(options: updateOptions.code) 27 | 28 | case .transform: 29 | return TransformTaskHandler(options: updateOptions.transform) 30 | 31 | case .translate: 32 | return TranslateTaskHandler(options: updateOptions.translate!) 33 | 34 | case .normalize: 35 | return NormalizeTaskHandler(options: updateOptions.normalize) 36 | } 37 | }() 38 | 39 | taskHandler.perform() 40 | } 41 | 42 | CommandExecution.current.failIfNeeded() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BartyCrouch.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "BartyCrouch" 4 | s.version = "4.15.0" 5 | s.summary = "Localization/I18n: Incrementally update/translate your Strings files from .swift, .h, .m(m), .storyboard or .xib files." 6 | 7 | s.description = <<-DESC 8 | BartyCrouch incrementally updates your Strings files from your Code and from Interface Builder files. "Incrementally" means that 9 | BartyCrouch will by default keep both your already translated values and even your altered comments. Additionally you can also use 10 | BartyCrouch for machine translating from one language to 60+ other languages. Using BartyCrouch is as easy as running a few 11 | simple commands from the command line what can even be automated using a build script within your project. 12 | DESC 13 | 14 | s.homepage = "https://github.com/Flinesoft/BartyCrouch" 15 | s.license = { :type => "MIT", :file => "LICENSE" } 16 | 17 | s.author = { "Cihat Gündüz" => "cocoapods@cihatguenduez.de" } 18 | s.social_media_url = "https://twitter.com/Jeehut" 19 | 20 | s.source = { :http => "#{s.homepage}/releases/download/#{s.version}/portable_bartycrouch.zip" } 21 | s.preserve_paths = "*" 22 | 23 | s.ios.deployment_target = '11.0' 24 | s.macos.deployment_target = '13.0' 25 | s.tvos.deployment_target = '11.0' 26 | s.swift_version = '5.7' 27 | 28 | end 29 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/IBToolCommanderTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //class IBToolCommanderTests: XCTestCase { 5 | // func testiOSExampleStoryboard() { 6 | // let storyboardPath = "\(BASE_DIR)/Tests/Resources/Storyboards/iOS/base.lproj/Example.storyboard" 7 | // let stringsFilePath = storyboardPath + ".strings" 8 | // 9 | // let exportSuccess = IBToolCommander.shared.export(stringsFileToPath: stringsFilePath, fromIbFileAtPath: storyboardPath) 10 | // 11 | // XCTAssertTrue(exportSuccess) 12 | // } 13 | // 14 | // func testOSXExampleStoryboard() { 15 | // let storyboardPath = "\(BASE_DIR)/Tests/Resources/Storyboards/macOS/base.lproj/Example.storyboard" 16 | // let stringsFilePath = storyboardPath + ".strings" 17 | // 18 | // let exportSuccess = IBToolCommander.shared.export(stringsFileToPath: stringsFilePath, fromIbFileAtPath: storyboardPath) 19 | // 20 | // XCTAssertTrue(exportSuccess) 21 | // } 22 | // 23 | // func testtvOSExampleStoryboard() { 24 | // let storyboardPath = "\(BASE_DIR)/Tests/Resources/Storyboards/tvOS/base.lproj/Example.storyboard" 25 | // let stringsFilePath = storyboardPath + ".strings" 26 | // 27 | // let exportSuccess = IBToolCommander.shared.export(stringsFileToPath: stringsFilePath, fromIbFileAtPath: storyboardPath) 28 | // 29 | // XCTAssertTrue(exportSuccess) 30 | // } 31 | //} 32 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/ExtractLocStringsTests.swift: -------------------------------------------------------------------------------- 1 | // Created by Christos Koninis on 25/09/2019. 2 | 3 | @testable import BartyCrouchKit 4 | import XCTest 5 | 6 | // swiftlint:disable force_try 7 | 8 | class ExtractLocStringsTests: XCTestCase { 9 | func testEncodedArgumentPlistFormat() { 10 | let files = ["file.m", "otherfile.swift", "/path/of/anotherfile.swift"] 11 | // Disabling whitespace_comment_start due to false positives 12 | // swiftlint:disable whitespace_comment_start 13 | let expectedArgumentsPlistString = """ 14 | 15 | filespathfile.mpathotherfile.swift\ 16 | path/path/of/anotherfile.swift 17 | """ 18 | 19 | let argumentsPlistData = try! ExtractLocStrings().encodeFilesArguments(files) 20 | let argumentsPlist = try! argumentsPlistFromData(argumentsPlistData) 21 | let expectedArgumentsPlist = try! argumentsPlistFromData(expectedArgumentsPlistString.data(using: .utf8)!) 22 | 23 | XCTAssertEqual(argumentsPlist, expectedArgumentsPlist) 24 | } 25 | 26 | private func argumentsPlistFromData(_ data: Data) throws -> ExtractLocStrings.ArgumentsPlist { 27 | return try PropertyListDecoder().decode(ExtractLocStrings.ArgumentsPlist.self, from: data) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Configuration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MungoHealer 3 | import Toml 4 | 5 | public struct Configuration { 6 | public static let fileName: String = ".bartycrouch.toml" 7 | 8 | public static var configUrl: URL { 9 | return URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(Configuration.fileName) 10 | } 11 | 12 | public let updateOptions: UpdateOptions 13 | public let lintOptions: LintOptions 14 | 15 | public static func load() throws -> Configuration { 16 | let configUrl = self.configUrl 17 | 18 | guard FileManager.default.fileExists(atPath: configUrl.path) else { 19 | return try Configuration.make(toml: try Toml(withString: "")) 20 | } 21 | 22 | let toml: Toml = try Toml(contentsOfFile: configUrl.path) 23 | return try Configuration.make(toml: toml) 24 | } 25 | } 26 | 27 | extension Configuration: TomlCodable { 28 | public static func makeDefault() throws -> Configuration { 29 | return try make(toml: Toml(withString: "")) 30 | } 31 | 32 | public static func make(toml: Toml) throws -> Configuration { 33 | let updateOptions = try UpdateOptions.make(toml: toml) 34 | let lintOptions = try LintOptions.make(toml: toml) 35 | 36 | return Configuration(updateOptions: updateOptions, lintOptions: lintOptions) 37 | } 38 | 39 | public func tomlContents() -> String { 40 | let sections: [String] = [ 41 | updateOptions.tomlContents(), 42 | lintOptions.tomlContents(), 43 | ] 44 | 45 | return sections.joined(separator: "\n\n") + "\n" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/FilesSearchable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol FilesSearchable { 4 | func findAllFilePaths( 5 | inDirectoryPath baseDirectoryPath: String, 6 | subpathsToIgnore: [String], 7 | matching regularExpression: NSRegularExpression 8 | ) -> [String] 9 | } 10 | 11 | extension FilesSearchable { 12 | func findAllFilePaths( 13 | inDirectoryPath baseDirectoryPath: String, 14 | subpathsToIgnore: [String], 15 | matching regularExpression: NSRegularExpression 16 | ) -> [String] { 17 | let baseDirectoryURL = URL(fileURLWithPath: baseDirectoryPath) 18 | guard let enumerator = FileManager.default.enumerator(at: baseDirectoryURL, includingPropertiesForKeys: nil) else { 19 | return [] 20 | } 21 | 22 | var filePaths = [String]() 23 | let baseDirectoryAbsolutePath = baseDirectoryURL.path 24 | let codeFilesSearch = CodeFilesSearch(baseDirectoryPath: baseDirectoryAbsolutePath) 25 | 26 | for case let url as URL in enumerator { 27 | if codeFilesSearch.shouldSkipFile(at: url, subpathsToIgnore: subpathsToIgnore) { 28 | enumerator.skipDescendants() 29 | continue 30 | } 31 | 32 | let absolutePath = url.path 33 | let searchRange = NSRange( 34 | location: baseDirectoryAbsolutePath.count, 35 | length: absolutePath.count - baseDirectoryAbsolutePath.count 36 | ) 37 | if regularExpression.firstMatch(in: absolutePath, options: [], range: searchRange) != nil { 38 | filePaths.append(absolutePath) 39 | } 40 | } 41 | 42 | return filePaths 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | prefix ?= /opt/homebrew 4 | bindir ?= $(prefix)/bin 5 | libdir ?= $(prefix)/lib 6 | srcdir = Sources 7 | 8 | REPODIR = $(shell pwd) 9 | BUILDDIR = $(REPODIR)/.build 10 | SOURCES = $(wildcard $(srcdir)/**/*.swift) 11 | 12 | .DEFAULT_GOAL = all 13 | 14 | .PHONY: all 15 | all: bartycrouch 16 | 17 | bartycrouch: $(SOURCES) 18 | @swift build \ 19 | -c release \ 20 | --disable-sandbox \ 21 | --scratch-path "$(BUILDDIR)" 22 | 23 | bartycrouch_universal: $(SOURCES) 24 | @swift build \ 25 | -c release \ 26 | --arch arm64 --arch x86_64 \ 27 | --disable-sandbox \ 28 | --scratch-path "$(BUILDDIR)" 29 | 30 | .PHONY: install 31 | install: bartycrouch 32 | @install -d "$(bindir)" "$(libdir)" 33 | @install "$(BUILDDIR)/release/bartycrouch" "$(bindir)" 34 | 35 | .PHONY: portable_zip 36 | portable_zip: bartycrouch_universal 37 | @rm -f "$(BUILDDIR)/Apple/Products/Release/portable_bartycrouch.zip" 38 | 39 | $(eval TMP := $(shell mktemp -d)) 40 | @cp "$(BUILDDIR)/Apple/Products/Release/bartycrouch" "$(TMP)/bartycrouch" 41 | @install_name_tool -add_rpath "@executable_path/." "$(TMP)/bartycrouch" 42 | 43 | @zip -q -j "$(BUILDDIR)/Apple/Products/Release/portable_bartycrouch.zip" \ 44 | "$(TMP)/bartycrouch" \ 45 | "$(REPODIR)/LICENSE" \ 46 | @echo "Portable ZIP created at: $(BUILDDIR)/Apple/Products/Release/portable_bartycrouch.zip" 47 | @rm -rf $(TMP) 48 | 49 | .PHONY: uninstall 50 | uninstall: 51 | @rm -rf "$(bindir)/bartycrouch" 52 | 53 | .PHONY: clean 54 | distclean: 55 | @rm -f $(BUILDDIR)/release 56 | 57 | .PHONY: clean 58 | clean: distclean 59 | @rm -rf $(BUILDDIR) 60 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, versions] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | cancel-previous-runs: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Cancel previous runs of this workflow on same branch 15 | uses: rokroskar/workflow-run-cleanup-action@v0.2.2 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | swiftlint: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Run SwiftLint 26 | uses: norio-nomura/action-swiftlint@3.1.0 27 | with: 28 | args: --strict 29 | 30 | build-macos: 31 | runs-on: macos-11 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - uses: maxim-lobanov/setup-xcode@v1 37 | with: 38 | xcode-version: latest-stable 39 | 40 | - name: Run tests 41 | run: swift build -v -c release 42 | 43 | test-macos: 44 | runs-on: macos-11 45 | 46 | steps: 47 | - uses: actions/checkout@v2 48 | 49 | - uses: maxim-lobanov/setup-xcode@v1 50 | with: 51 | xcode-version: latest-stable 52 | 53 | - name: Setup secrets.json 54 | run: | 55 | echo ' 56 | { 57 | "deepLApiKey": "${{ secrets.DEEP_L_API_KEY }}", 58 | "microsoftSubscriptionKey": "${{ secrets.MICROSOFT_SUBSCRIPTION_KEY }}" 59 | } 60 | ' >> Tests/BartyCrouchTranslatorTests/Secrets/secrets.json 61 | 62 | - name: Run tests 63 | run: swift test -v 64 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/BartyCrouch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is required in order for the `transform` task of the translation helper tool BartyCrouch to work. 3 | // See here for more details: https://github.com/Flinesoft/BartyCrouch 4 | // 5 | 6 | import Foundation 7 | 8 | enum BartyCrouch { 9 | enum SupportedLanguage: String { 10 | // TODO: remove unsupported languages from the following cases list & add any missing languages 11 | case arabic = "ar" 12 | case chineseSimplified = "zh-Hans" 13 | case chineseTraditional = "zh-Hant" 14 | case english = "en" 15 | case french = "fr" 16 | case german = "de" 17 | case hindi = "hi" 18 | case italian = "it" 19 | case japanese = "ja" 20 | case korean = "ko" 21 | case malay = "ms" 22 | case portuguese = "pt-BR" 23 | case russian = "ru" 24 | case spanish = "es" 25 | case turkish = "tr" 26 | } 27 | 28 | static func translate(key: String, translations: [SupportedLanguage: String], comment: String? = nil) -> String { 29 | let typeName = String(describing: BartyCrouch.self) 30 | let methodName = #function 31 | 32 | print( 33 | "Warning: [BartyCrouch]", 34 | "Untransformed \(typeName).\(methodName) method call found with key '\(key)' and base translations '\(translations)'.", 35 | "Please ensure that BartyCrouch is installed and configured correctly." 36 | ) 37 | 38 | // fall back in case something goes wrong with BartyCrouch transformation 39 | return "BC: TRANSFORMATION FAILED!" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/UpdateOptions/InterfacesOptions.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import Toml 4 | 5 | public struct InterfacesOptions { 6 | public let paths: [String] 7 | public let subpathsToIgnore: [String] 8 | public let defaultToBase: Bool 9 | public let ignoreEmptyStrings: Bool 10 | public let unstripped: Bool 11 | public let ignoreKeys: [String] 12 | } 13 | 14 | extension InterfacesOptions: TomlCodable { 15 | static func make(toml: Toml) throws -> InterfacesOptions { 16 | let update: String = "update" 17 | let interfaces: String = "interfaces" 18 | 19 | return InterfacesOptions( 20 | paths: toml.filePaths(update, interfaces, singularKey: "path", pluralKey: "paths"), 21 | subpathsToIgnore: toml.array(update, interfaces, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore, 22 | defaultToBase: toml.bool(update, interfaces, "defaultToBase") ?? false, 23 | ignoreEmptyStrings: toml.bool(update, interfaces, "ignoreEmptyStrings") ?? false, 24 | unstripped: toml.bool(update, interfaces, "unstripped") ?? false, 25 | ignoreKeys: toml.array(update, interfaces, "ignoreKeys") ?? Constants.defaultIgnoreKeys 26 | ) 27 | } 28 | 29 | func tomlContents() -> String { 30 | var lines: [String] = ["[update.interfaces]"] 31 | 32 | lines.append("paths = \(paths)") 33 | lines.append("subpathsToIgnore = \(subpathsToIgnore)") 34 | lines.append("defaultToBase = \(defaultToBase)") 35 | lines.append("ignoreEmptyStrings = \(ignoreEmptyStrings)") 36 | lines.append("unstripped = \(unstripped)") 37 | lines.append("ignoreKeys = \(ignoreKeys)") 38 | 39 | return lines.joined(separator: "\n") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/FileHandling/CodeFileHandlerTests.swift: -------------------------------------------------------------------------------- 1 | @testable import BartyCrouchKit 2 | import XCTest 3 | 4 | class CodeFileHandlerTests: XCTestCase { 5 | static let testDemoDirectoryUrl: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Demo") 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | TestHelper.shared.reset() 11 | TestHelper.shared.isStartedByUnitTests = true 12 | try! FileManager.default.removeContentsOfDirectory(at: DemoTests.testDemoDirectoryUrl) 13 | 14 | let jsonData = DemoData.untouchedDemoDirectoryJson.data(using: .utf8)! 15 | let directory = try! JSONDecoder().decode(Directory.self, from: jsonData) 16 | directory.files.forEach { try! $0.write(into: CodeFileHandlerTests.testDemoDirectoryUrl) } 17 | 18 | FileManager.default.changeCurrentDirectoryPath(CodeFileHandlerTests.testDemoDirectoryUrl.path) 19 | } 20 | 21 | override func tearDown() { 22 | super.tearDown() 23 | 24 | try! FileManager.default.removeContentsOfDirectory(at: CodeFileHandlerTests.testDemoDirectoryUrl) 25 | } 26 | 27 | func testFindCaseToLangCodeMappins() { 28 | let supportingLanguagesCodeFilePath: String = CodeFileHandlerTests.testDemoDirectoryUrl 29 | .appendingPathComponent("Demo/BartyCrouch.swift").path 30 | let caseToLangMappings: [String: String]? = CodeFileHandler(path: supportingLanguagesCodeFilePath) 31 | .findCaseToLangCodeMappings(typeName: "BartyCrouch") 32 | 33 | XCTAssertNotNil(caseToLangMappings) 34 | XCTAssertEqual(caseToLangMappings?["german"], "de") 35 | XCTAssertEqual(caseToLangMappings?["japanese"], "ja") 36 | XCTAssertEqual(caseToLangMappings?["chineseTraditional"], "zh-Hant") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/UpdateOptions/NormalizeOptions.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import Toml 4 | 5 | public struct NormalizeOptions { 6 | public let paths: [String] 7 | public let subpathsToIgnore: [String] 8 | public let sourceLocale: String 9 | public let harmonizeWithSource: Bool 10 | public let sortByKeys: Bool 11 | public let separateWithEmptyLine: Bool 12 | } 13 | 14 | extension NormalizeOptions: TomlCodable { 15 | static func make(toml: Toml) throws -> NormalizeOptions { 16 | let update: String = "update" 17 | let normalize: String = "normalize" 18 | 19 | return NormalizeOptions( 20 | paths: toml.filePaths(update, normalize, singularKey: "path", pluralKey: "paths"), 21 | subpathsToIgnore: toml.array(update, normalize, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore, 22 | sourceLocale: toml.string(update, normalize, "sourceLocale") ?? "en", 23 | harmonizeWithSource: toml.bool(update, normalize, "harmonizeWithSource") ?? true, 24 | sortByKeys: toml.bool(update, normalize, "sortByKeys") ?? true, 25 | separateWithEmptyLine: toml.bool(update, normalize, "separateWithEmptyLine") ?? true 26 | ) 27 | } 28 | 29 | func tomlContents() -> String { 30 | var lines: [String] = ["[update.normalize]"] 31 | 32 | lines.append("paths = \(self.paths)") 33 | lines.append("subpathsToIgnore = \(self.subpathsToIgnore)") 34 | lines.append("sourceLocale = \"\(self.sourceLocale)\"") 35 | lines.append("harmonizeWithSource = \(self.harmonizeWithSource)") 36 | lines.append("sortByKeys = \(self.sortByKeys)") 37 | lines.append("separateWithEmptyLine = \(self.separateWithEmptyLine)") 38 | 39 | return lines.joined(separator: "\n") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/DemoTests/Directory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Directory: Codable { 4 | struct File: Codable { 5 | let relativePath: String 6 | let contents: String 7 | 8 | init( 9 | baseDirectoryUrl: URL, 10 | relativePath: String 11 | ) throws { 12 | self.relativePath = relativePath 13 | self.contents = try String(contentsOf: baseDirectoryUrl.appendingPathComponent(relativePath), encoding: .utf8) 14 | } 15 | 16 | func write(into directory: URL) throws { 17 | let fileUrl = directory.appendingPathComponent(relativePath) 18 | let contentsData = contents.data(using: .utf8)! 19 | try FileManager.default.createFile( 20 | atPath: fileUrl.path, 21 | withIntermediateDirectories: true, 22 | contents: contentsData 23 | ) 24 | } 25 | } 26 | 27 | let files: [File] 28 | 29 | init( 30 | files: [File] 31 | ) { 32 | self.files = files 33 | } 34 | 35 | static func read(fromDirPath directoryPath: String) throws -> Directory { 36 | let enumerator = FileManager.default.enumerator(atPath: directoryPath)! 37 | let baseDirectoryUrl = URL(fileURLWithPath: directoryPath, isDirectory: true) 38 | var files: [File] = [] 39 | 40 | while let nextObject = enumerator.nextObject() as? String { 41 | guard !nextObject.hasSuffix(".xcuserstate") else { continue } 42 | guard !nextObject.hasSuffix(".DS_Store") else { continue } 43 | guard enumerator.fileAttributes![FileAttributeKey.type] as! String == FileAttributeType.typeRegular.rawValue 44 | else { continue } 45 | 46 | let file = try File(baseDirectoryUrl: baseDirectoryUrl, relativePath: nextObject) 47 | files.append(file) 48 | } 49 | 50 | return Directory(files: files) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/UpdateOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MungoHealer 3 | import Toml 4 | 5 | public struct UpdateOptions { 6 | public enum Task: String { 7 | case interfaces 8 | case code 9 | case transform 10 | case translate 11 | case normalize 12 | } 13 | 14 | public let tasks: [Task] 15 | public let interfaces: InterfacesOptions 16 | public let code: CodeOptions 17 | public let transform: TransformOptions 18 | public let translate: TranslateOptions? 19 | public let normalize: NormalizeOptions 20 | } 21 | 22 | extension UpdateOptions: TomlCodable { 23 | static func make(toml: Toml) throws -> UpdateOptions { 24 | let translateOptions: TranslateOptions? = try? TranslateOptions.make(toml: toml) 25 | let defaultTasks: [String] = 26 | translateOptions != nil 27 | ? ["interfaces", "code", "transform", "translate", "normalize"] 28 | : ["interfaces", "code", "transform", "normalize"] 29 | 30 | return UpdateOptions( 31 | tasks: (toml.array("update", "tasks") ?? defaultTasks).compactMap { Task(rawValue: $0) }, 32 | interfaces: try InterfacesOptions.make(toml: toml), 33 | code: try CodeOptions.make(toml: toml), 34 | transform: try TransformOptions.make(toml: toml), 35 | translate: translateOptions, 36 | normalize: try NormalizeOptions.make(toml: toml) 37 | ) 38 | } 39 | 40 | func tomlContents() -> String { 41 | let sections: [String?] = [ 42 | "[update]\ntasks = \(tasks.map { $0.rawValue })", 43 | interfaces.tomlContents(), 44 | code.tomlContents(), 45 | transform.tomlContents(), 46 | translate?.tomlContents(), 47 | normalize.tomlContents(), 48 | ] 49 | 50 | return sections.compactMap { $0 }.joined(separator: "\n\n") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/OldCommandLine/ExtractLocStrings.swift: -------------------------------------------------------------------------------- 1 | // Created by Christos Koninis on 25/09/2019. 2 | 3 | import Foundation 4 | 5 | /// Class to handle extractLocStrings's tool file argument list. It provides methods to serialize the file list to an argument plist file. See 6 | /// https://github.com/Flinesoft/BartyCrouch/issues/92 7 | class ExtractLocStrings { 8 | /// extractLocStrings tools supports instead of passing files as arguments in the command line to pass a plist with the files. This is the format of that 9 | /// plist. 10 | struct File: Codable, Equatable { 11 | var path: String 12 | } 13 | struct ArgumentsPlist: Codable, Equatable { 14 | var files: [File] = [] 15 | 16 | init( 17 | filePaths: [String] 18 | ) { 19 | files = filePaths.map(File.init) 20 | } 21 | } 22 | 23 | /// Serializes the extractLocStrings's file arguments to an argument plist file. 24 | /// 25 | /// - Parameter files: A array containing the list of files. 26 | /// - Returns: The argument plist file that contains the list of file arguments. 27 | /// - Throws: An error if any value throws an error during plist encoding. 28 | func writeFilesArgumentsInPlist(_ files: [String]) throws -> String { 29 | let data = try encodeFilesArguments(files) 30 | let tempPlistFilePath = createTemporaryArgumentsPlistFile() 31 | try data.write(to: tempPlistFilePath) 32 | 33 | return tempPlistFilePath.path 34 | } 35 | 36 | /// Serializes the extractLocStrings's file arguments to byte array 37 | /// 38 | /// - Parameter files: A array containing the list of files. 39 | /// - Returns: A plist encoded value of the supplied array 40 | func encodeFilesArguments(_ files: [String]) throws -> Data { 41 | let encoder = PropertyListEncoder() 42 | encoder.outputFormat = .xml 43 | 44 | return try encoder.encode(ArgumentsPlist(filePaths: files)) 45 | } 46 | 47 | private func createTemporaryArgumentsPlistFile() -> URL { 48 | let temporaryFilename = ProcessInfo().globallyUniqueString 49 | var temporaryPath = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 50 | temporaryPath.appendPathComponent(temporaryFilename) 51 | temporaryPath.appendPathExtension("plist") 52 | 53 | return temporaryPath 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/CodeFileHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import SwiftSyntax 4 | import SwiftSyntaxParser 5 | 6 | final class CodeFileHandler { 7 | typealias TranslationElement = (langCode: String, translation: String) 8 | typealias TranslateEntry = (key: String, translations: [TranslationElement], comment: String?) 9 | 10 | private let path: String 11 | 12 | init( 13 | path: String 14 | ) { 15 | self.path = path 16 | } 17 | 18 | /// Rewrites the file using the transformer. Returns the translate entries which were found (and transformed). 19 | func transform( 20 | typeName: String, 21 | translateMethodName: String, 22 | using transformer: Transformer, 23 | caseToLangCode: [String: String] 24 | ) throws -> [TranslateEntry] { 25 | let fileUrl = URL(fileURLWithPath: path) 26 | guard try String(contentsOfFile: path).contains("\(typeName).\(translateMethodName)") else { return [] } 27 | 28 | guard let sourceFile = try? SyntaxParser.parse(fileUrl) else { 29 | print("Could not parse syntax tree of Swift file.", level: .warning, file: path) 30 | return [] 31 | } 32 | 33 | let translateTransformer = TranslateTransformer( 34 | transformer: transformer, 35 | typeName: typeName, 36 | translateMethodName: translateMethodName, 37 | caseToLangCode: caseToLangCode 38 | ) 39 | guard let transformedFile = translateTransformer.visit(sourceFile).as(SourceFileSyntax.self) else { return [] } 40 | 41 | try transformedFile.description.write(toFile: path, atomically: true, encoding: .utf8) 42 | return translateTransformer.translateEntries 43 | } 44 | 45 | func findCaseToLangCodeMappings(typeName: String) -> [String: String]? { 46 | let fileUrl = URL(fileURLWithPath: path) 47 | 48 | guard let sourceFile = try? SyntaxParser.parse(fileUrl) else { 49 | print("Could not parse syntax tree of Swift file.", level: .warning, file: path) 50 | return nil 51 | } 52 | 53 | let supportedLanguagesReader = SupportedLanguagesReader(typeName: typeName) 54 | supportedLanguagesReader.walk(sourceFile) 55 | 56 | guard !supportedLanguagesReader.caseToLangCode.isEmpty else { return nil } 57 | return supportedLanguagesReader.caseToLangCode 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/CodeFilesSearch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CodeFilesSearch: FilesSearchable { 4 | private let baseDirectoryPath: String 5 | private let basePathComponents: [String] 6 | 7 | init( 8 | baseDirectoryPath: String 9 | ) { 10 | self.baseDirectoryPath = baseDirectoryPath 11 | self.basePathComponents = URL(fileURLWithPath: baseDirectoryPath).pathComponents 12 | } 13 | 14 | func shouldSkipFile(at url: URL, subpathsToIgnore: [String]) -> Bool { 15 | var subpath = url.path 16 | 17 | if let potentialBaseDirSubstringRange = subpath.range(of: baseDirectoryPath) { 18 | if potentialBaseDirSubstringRange.lowerBound == subpath.startIndex { 19 | subpath.removeSubrange(potentialBaseDirSubstringRange) 20 | } 21 | } 22 | 23 | let subpathComponents = subpath.components(separatedBy: "/").filter { !$0.isBlank } 24 | for subpathToIgnore in subpathsToIgnore { 25 | let subpathToIgnoreComponents = subpathToIgnore.components(separatedBy: "/") 26 | if subpathComponents.containsCaseInsensitive(subarray: subpathToIgnoreComponents) { 27 | return true 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | func findCodeFiles(subpathsToIgnore: [String]) -> [String] { 35 | guard FileManager.default.fileExists(atPath: baseDirectoryPath) else { return [] } 36 | guard !baseDirectoryPath.hasSuffix(".string") else { return [baseDirectoryPath] } 37 | 38 | let codeFileRegex = try! NSRegularExpression(pattern: "\\.swift\\z", options: .caseInsensitive) // swiftlint:disable:this force_try 39 | let codeFiles: [String] = findAllFilePaths( 40 | inDirectoryPath: baseDirectoryPath, 41 | subpathsToIgnore: subpathsToIgnore, 42 | matching: codeFileRegex 43 | ) 44 | return codeFiles 45 | } 46 | } 47 | 48 | extension Array where Element == String { 49 | func containsCaseInsensitive(subarray: [Element]) -> Bool { 50 | guard let firstSubArrayElement = subarray.first else { return false } 51 | 52 | // sample: this = [a, b, c], subarray = [b, c], firstIndex = 1, subRange = 1 ..< 3 53 | for (index, element) in enumerated() where element.lowercased() == firstSubArrayElement.lowercased() { 54 | let subRange = index.. TranslateOptions { 21 | let update: String = "update" 22 | let translate: String = "translate" 23 | 24 | if let secretString: String = toml.string(update, translate, "secret") { 25 | let translator = toml.string(update, translate, "translator") ?? "microsoftTranslator" 26 | let paths = toml.filePaths(update, translate, singularKey: "path", pluralKey: "paths") 27 | let subpathsToIgnore = toml.array(update, translate, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore 28 | let sourceLocale: String = toml.string(update, translate, "sourceLocale") ?? "en" 29 | let separateWithEmptyLine = toml.bool(update, translate, "separateWithEmptyLine") ?? true 30 | let secret: Secret 31 | switch Translator(rawValue: translator) { 32 | case .microsoftTranslator, .none: 33 | secret = .microsoftTranslator(secret: secretString) 34 | 35 | case .deepL: 36 | secret = .deepL(secret: secretString) 37 | } 38 | 39 | return TranslateOptions( 40 | paths: paths, 41 | subpathsToIgnore: subpathsToIgnore, 42 | secret: secret, 43 | sourceLocale: sourceLocale, 44 | separateWithEmptyLine: separateWithEmptyLine 45 | ) 46 | } 47 | else { 48 | throw MungoError( 49 | source: .invalidUserInput, 50 | message: "Incomplete [update.translate] options provided, ignoring them all." 51 | ) 52 | } 53 | } 54 | 55 | func tomlContents() -> String { 56 | var lines: [String] = ["[update.translate]"] 57 | 58 | lines.append("paths = \(paths)") 59 | lines.append("subpathsToIgnore = \(subpathsToIgnore)") 60 | switch secret { 61 | case let .deepL(secret): 62 | lines.append(#"secret = "\#(secret)""#) 63 | 64 | case let .microsoftTranslator(secret): 65 | lines.append(#"secret = "\#(secret)""#) 66 | } 67 | 68 | lines.append(#"sourceLocale = "\#(sourceLocale)""#) 69 | lines.append("separateWithEmptyLine = \(self.separateWithEmptyLine)") 70 | 71 | return lines.joined(separator: "\n") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/MicrosoftTranslatorApi/MicrosoftTranslatorApi.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Microya 3 | 4 | // Documentation can be found here: https://docs.microsoft.com/en-us/azure/cognitive-services/translator/reference/v3-0-translate 5 | 6 | enum MicrosoftTranslatorApi { 7 | case translate(texts: [String], from: Language, to: [Language], microsoftSubscriptionKey: String) 8 | 9 | static let maximumTextsPerRequest: Int = 25 10 | static let maximumTextsLengthPerRequest: Int = 5_000 11 | 12 | static func textBatches(forTexts texts: [String]) -> [[String]] { 13 | var batches: [[String]] = [] 14 | var currentBatch: [String] = [] 15 | var currentBatchTotalLength: Int = 0 16 | 17 | for text in texts { 18 | if currentBatch.count < maximumTextsPerRequest 19 | && text.count + currentBatchTotalLength < maximumTextsLengthPerRequest 20 | { 21 | currentBatch.append(text) 22 | currentBatchTotalLength += text.count 23 | } 24 | else { 25 | batches.append(currentBatch) 26 | 27 | currentBatch = [text] 28 | currentBatchTotalLength = text.count 29 | } 30 | } 31 | 32 | return batches 33 | } 34 | } 35 | 36 | extension MicrosoftTranslatorApi: Endpoint { 37 | typealias ClientErrorType = EmptyBodyResponse 38 | 39 | static var baseUrl: URL { 40 | return URL(string: "https://api.cognitive.microsofttranslator.com")! 41 | } 42 | 43 | var decoder: JSONDecoder { 44 | return JSONDecoder() 45 | } 46 | 47 | var encoder: JSONEncoder { 48 | return JSONEncoder() 49 | } 50 | 51 | var subpath: String { 52 | switch self { 53 | case .translate: 54 | return "/translate" 55 | } 56 | } 57 | 58 | var method: HttpMethod { 59 | switch self { 60 | case let .translate(texts, _, _, _): // swiftlint:disable:next force_try 61 | return .post(body: try! encoder.encode(texts.map { TranslateRequest(Text: $0) })) 62 | } 63 | } 64 | 65 | var queryParameters: [String: QueryParameterValue] { 66 | var urlParameters: [String: QueryParameterValue] = ["api-version": "3.0"] 67 | 68 | switch self { 69 | case let .translate(_, sourceLanguage, targetLanguages, _): 70 | urlParameters["from"] = .string(sourceLanguage.rawValue) 71 | urlParameters["to"] = .array(targetLanguages.map { $0.rawValue }) 72 | } 73 | 74 | return urlParameters 75 | } 76 | 77 | var headers: [String: String] { 78 | switch self { 79 | case let .translate(_, _, _, microsoftSubscriptionKey): 80 | return [ 81 | "Ocp-Apim-Subscription-Key": microsoftSubscriptionKey, 82 | "Content-Type": "application/json", 83 | ] 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/UpdateOptions/CodeOptions.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import Toml 4 | 5 | public struct CodeOptions { 6 | public let codePaths: [String] 7 | public let subpathsToIgnore: [String] 8 | public let localizablePaths: [String] 9 | public let defaultToKeys: Bool 10 | public let additive: Bool 11 | public let customFunction: String? 12 | public let customLocalizableName: String? 13 | public let unstripped: Bool 14 | public let plistArguments: Bool 15 | public let ignoreKeys: [String] 16 | public let overrideComments: Bool 17 | } 18 | 19 | extension CodeOptions: TomlCodable { 20 | static func make(toml: Toml) throws -> CodeOptions { 21 | let update: String = "update" 22 | let code: String = "code" 23 | 24 | return CodeOptions( 25 | codePaths: toml.filePaths(update, code, singularKey: "codePath", pluralKey: "codePaths"), 26 | subpathsToIgnore: toml.array(update, code, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore, 27 | localizablePaths: toml.filePaths(update, code, singularKey: "localizablePath", pluralKey: "localizablePaths"), 28 | defaultToKeys: toml.bool(update, code, "defaultToKeys") ?? false, 29 | additive: toml.bool(update, code, "additive") ?? true, 30 | customFunction: toml.string(update, code, "customFunction") ?? "LocalizedStringResource", 31 | customLocalizableName: toml.string(update, code, "customLocalizableName"), 32 | unstripped: toml.bool(update, code, "unstripped") ?? false, 33 | plistArguments: toml.bool(update, code, "plistArguments") ?? true, 34 | ignoreKeys: toml.array(update, code, "ignoreKeys") ?? Constants.defaultIgnoreKeys, 35 | overrideComments: toml.bool(update, code, "overrideComments") ?? false 36 | ) 37 | } 38 | 39 | func tomlContents() -> String { 40 | var lines: [String] = ["[update.code]"] 41 | 42 | lines.append("codePaths = \(codePaths)") 43 | lines.append("subpathsToIgnore = \(subpathsToIgnore)") 44 | lines.append("localizablePaths = \(localizablePaths)") 45 | lines.append("defaultToKeys = \(defaultToKeys)") 46 | lines.append("additive = \(additive)") 47 | 48 | if let customFunction = customFunction { 49 | lines.append("customFunction = \"\(customFunction)\"") 50 | } 51 | 52 | if let customLocalizableName = customLocalizableName { 53 | lines.append("customLocalizableName = \"\(customLocalizableName)\"") 54 | } 55 | 56 | lines.append("unstripped = \(unstripped)") 57 | lines.append("plistArguments = \(plistArguments)") 58 | lines.append("ignoreKeys = \(Constants.defaultIgnoreKeys)") 59 | lines.append("overrideComments = \(overrideComments)") 60 | 61 | return lines.joined(separator: "\n") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/TaskHandlers/TransformTaskHandler.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchConfiguration 2 | import Foundation 3 | 4 | struct TransformTaskHandler { 5 | let options: TransformOptions 6 | } 7 | 8 | extension TransformTaskHandler: TaskHandler { 9 | func perform() { 10 | measure(task: "Code Transform") { 11 | mungo.do { 12 | var caseToLangCodeOptional: [String: String]? 13 | 14 | let codeFilesArray = CodeFilesSearch(baseDirectoryPath: options.supportedLanguageEnumPath.absolutePath) 15 | .findCodeFiles(subpathsToIgnore: options.subpathsToIgnore) 16 | 17 | for codeFile in codeFilesArray { 18 | if let foundCaseToLangCode = CodeFileHandler(path: codeFile) 19 | .findCaseToLangCodeMappings(typeName: options.typeName) 20 | { 21 | caseToLangCodeOptional = foundCaseToLangCode 22 | break 23 | } 24 | } 25 | 26 | guard let caseToLangCode = caseToLangCodeOptional else { 27 | print( 28 | "Could not find 'SupportedLanguage' enum within '\(options.typeName)' enum within path.", 29 | level: .warning, 30 | file: options.supportedLanguageEnumPath.absolutePath 31 | ) 32 | return 33 | } 34 | 35 | var translateEntries: [CodeFileHandler.TranslateEntry] = [] 36 | 37 | let codeFilesSet = Set( 38 | options.codePaths.flatMap { 39 | CodeFilesSearch(baseDirectoryPath: $0.absolutePath) 40 | .findCodeFiles(subpathsToIgnore: options.subpathsToIgnore) 41 | } 42 | ) 43 | 44 | for codeFile in codeFilesSet { 45 | let codeFileHandler = CodeFileHandler(path: codeFile) 46 | 47 | translateEntries += try codeFileHandler.transform( 48 | typeName: options.typeName, 49 | translateMethodName: options.translateMethodName, 50 | using: options.transformer, 51 | caseToLangCode: caseToLangCode 52 | ) 53 | } 54 | 55 | let stringsFiles: [String] = Array( 56 | Set( 57 | options.localizablePaths.flatMap { 58 | StringsFilesSearch.shared.findAllStringsFiles( 59 | within: $0.absolutePath, 60 | withFileName: options.customLocalizableName ?? "Localizable", 61 | subpathsToIgnore: options.subpathsToIgnore 62 | ) 63 | } 64 | ) 65 | ) 66 | 67 | for stringsFile in stringsFiles { 68 | StringsFileUpdater(path: stringsFile)! 69 | .insert( 70 | translateEntries: translateEntries, 71 | separateWithEmptyLine: self.options.separateWithEmptyLine 72 | ) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/BartyCrouchTranslatorTests/MicrosoftTranslatorApiTests.swift: -------------------------------------------------------------------------------- 1 | @testable import BartyCrouchTranslator 2 | import Foundation 3 | import Microya 4 | import XCTest 5 | 6 | class MicrosoftTranslatorApiTests: XCTestCase { 7 | func testTranslate() { 8 | let microsoftSubscriptionKey = try! Secrets.load().microsoftSubscriptionKey // swiftlint:disable:this force_try 9 | guard !microsoftSubscriptionKey.isEmpty else { return } 10 | 11 | let endpoint = MicrosoftTranslatorApi.translate( 12 | texts: ["How old are you?", "Love"], 13 | from: .english, 14 | to: [ 15 | .german, 16 | .turkish, 17 | Language.with(languageCode: "pt", region: "BR")!, 18 | Language.with(languageCode: "pt", region: "PT")!, 19 | Language.with(languageCode: "fr", region: "CA")!, 20 | ], 21 | microsoftSubscriptionKey: microsoftSubscriptionKey 22 | ) 23 | 24 | let apiProvider = ApiProvider(baseUrl: MicrosoftTranslatorApi.baseUrl) 25 | 26 | switch apiProvider.performRequestAndWait(on: endpoint, decodeBodyTo: [TranslateResponse].self) { 27 | case let .success(translateResponses): 28 | XCTAssertEqual(translateResponses[0].translations[0].to, "de") 29 | XCTAssertEqual(translateResponses[0].translations[0].text, "Wie alt bist du?") 30 | 31 | XCTAssertEqual(translateResponses[0].translations[1].to, "tr") 32 | XCTAssertEqual(translateResponses[0].translations[1].text, "Kaç yaşındasınız?") 33 | 34 | XCTAssertEqual(translateResponses[0].translations[2].to, "pt") 35 | XCTAssertEqual(translateResponses[0].translations[2].text, "Quantos anos tem?") 36 | 37 | XCTAssertEqual(translateResponses[0].translations[3].to, "pt-PT") 38 | XCTAssertEqual(translateResponses[0].translations[3].text, "Quantos anos tens?") 39 | 40 | XCTAssertEqual(translateResponses[0].translations[4].to, "fr-CA") 41 | XCTAssertEqual(translateResponses[0].translations[4].text, "Quel âge avez-vous?") 42 | 43 | XCTAssertEqual(translateResponses[1].translations[0].to, "de") 44 | XCTAssertEqual(translateResponses[1].translations[0].text.lowercased(), "Liebe".lowercased()) 45 | 46 | XCTAssertEqual(translateResponses[1].translations[1].to, "tr") 47 | XCTAssertEqual(translateResponses[1].translations[1].text, "Aşk") 48 | 49 | XCTAssertEqual(translateResponses[1].translations[2].to, "pt") 50 | XCTAssertEqual(translateResponses[1].translations[2].text, "Amor") 51 | 52 | XCTAssertEqual(translateResponses[1].translations[3].to, "pt-PT") 53 | XCTAssertEqual(translateResponses[1].translations[3].text, "O amor") 54 | 55 | XCTAssertEqual(translateResponses[1].translations[4].to, "fr-CA") 56 | XCTAssertEqual(translateResponses[1].translations[4].text, "L’amour") 57 | 58 | case let .failure(failure): 59 | XCTFail(failure.localizedDescription) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MIGRATION_GUIDES.md: -------------------------------------------------------------------------------- 1 | # Migration Guides 2 | 3 | This project follows [Semantic Versioning](http://semver.org). 4 | 5 | Please follow the appropriate guide below when **upgrading to a new major version** of BartyCrouch (e.g. 1.5 -> 2.0). 6 | 7 | ## Upgrade from 3.x to 4.x 8 | - All subcommands except `lint` were bundled into the `update` subcommand. 9 | - Choosing specific subcommands and passing options was moved to the configuration file `.bartycrouch.toml`. See the [appropriate section](https://github.com/Flinesoft/BartyCrouch#configuration) in the README. 10 | - Update your build script to the [new simplified version](https://github.com/Flinesoft/BartyCrouch#build-script). Also make sure it's run as one of the first steps. 11 | - The `--override-comments` (`-c`) option on the `code` subcommand is now always turned on, no need to configure. 12 | - The `--extract-loc-strings` (`-e`) option on the `code` subcommand is now always turned on, no need to configure. 13 | - Consider using the new [`transform` task](https://github.com/Flinesoft/BartyCrouch#localization-workflow-via-transform) instead of – or in addition to – the `code` task. 14 | 15 | ## Upgrade from 2.x to 3.x 16 | 17 | - Change structure `bartycrouch -s "$BASE_PATH"` to `bartycrouch interfaces -p "$BASE_PATH"` 18 | - Change structure `bartycrouch -t "{ id: }|{ secret: }" -s "$BASE_PATH" -l en` to `bartycrouch translate -p "$BASE_PATH" -l en -i "" -s ""` 19 | - Use automatic file search with `-p` (was `-s` before) instead of options `-i`, `-o`, `-e` (those were deleted) 20 | - Rename usages of option "force" (`-f`) to be "override" (`-o`) 21 | 22 | It is recommended to update your build script to the [currently suggested](#build-script) one if you were using it. 23 | 24 | ## Upgrade from 1.x to 2.x 25 | 26 | - Change command structure `bartycrouch "$BASE_PATH" -a` to `bartycrouch -s "$BASE_PATH"` 27 | - Remove `-c` option if you were using it, BartyCrouch 2.x creates missing keys by default 28 | - Use the new `-t` `-s` `-l` options instead of adding all Strings files manually, e.g.: 29 | 30 | Simplify this build script code 31 | 32 | ```shell 33 | bartycrouch -t $CREDS -i "$EN_PATH/Localizable.strings" -a -c 34 | bartycrouch -t $CREDS -i "$EN_PATH/Main.strings" -a 35 | bartycrouch -t $CREDS -i "$EN_PATH/LaunchScreen.strings" -a 36 | bartycrouch -t $CREDS -i "$EN_PATH/CustomView.strings" -a 37 | ``` 38 | 39 | by replacing it with this: 40 | 41 | ```shell 42 | bartycrouch -t "$CREDS" -s "$PROJECT_DIR" -l en 43 | ``` 44 | 45 | 46 | ## Upgrade from 0.x to 1.x 47 | 48 | - `--input-storyboard` and `-in` were **renamed** to `--input` and `-i` 49 | - `--output-strings-files` and `-out` were **renamed** to `--output` and `-o` 50 | - Multiple paths passed to `-output` are now **separated by whitespace instead of comma** 51 | - e.g. `-out "path/one,path/two"` should now be `-o "path/one" "path/two"` 52 | - `--output-all-languages` and `-all` were **renamed** to `--auto` and `-a` 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BartyCrouch", 6 | platforms: [.macOS(.v10_15)], 7 | products: [ 8 | .executable(name: "bartycrouch", targets: ["BartyCrouch"]), 9 | .library(name: "BartyCrouchConfiguration", targets: ["BartyCrouchConfiguration"]), 10 | .library(name: "BartyCrouchKit", targets: ["BartyCrouchKit"]), 11 | .library(name: "BartyCrouchTranslator", targets: ["BartyCrouchTranslator"]), 12 | ], 13 | dependencies: [ 14 | .package(name: "HandySwift", url: "https://github.com/Flinesoft/HandySwift.git", from: "3.2.0"), 15 | .package(name: "Microya", url: "https://github.com/Flinesoft/Microya.git", .branch("support/without-combine")), 16 | .package(name: "MungoHealer", url: "https://github.com/Flinesoft/MungoHealer.git", from: "0.3.4"), 17 | .package(name: "Rainbow", url: "https://github.com/onevcat/Rainbow.git", from: "3.1.5"), 18 | .package(name: "SwiftCLI", url: "https://github.com/jakeheis/SwiftCLI.git", from: "6.0.3"), 19 | .package(name: "Toml", url: "https://github.com/jdfergason/swift-toml.git", .branch("master")), 20 | .package(url: "https://github.com/apple/swift-syntax.git", from: "508.0.0"), 21 | 22 | // A collection of tools for debugging, diffing, and testing your application's data structures. 23 | .package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "0.3.0"), 24 | ], 25 | targets: [ 26 | .executableTarget( 27 | name: "BartyCrouch", 28 | dependencies: ["BartyCrouchKit"] 29 | ), 30 | .target( 31 | name: "BartyCrouchKit", 32 | dependencies: [ 33 | "BartyCrouchConfiguration", 34 | "BartyCrouchTranslator", 35 | "HandySwift", 36 | "MungoHealer", 37 | "Rainbow", 38 | "SwiftCLI", 39 | .product(name: "SwiftSyntaxParser", package: "swift-syntax"), 40 | .product(name: "SwiftSyntax", package: "swift-syntax"), 41 | "BartyCrouchUtility", 42 | ] 43 | ), 44 | .testTarget( 45 | name: "BartyCrouchKitTests", 46 | dependencies: ["BartyCrouchKit"] 47 | ), 48 | .target( 49 | name: "BartyCrouchConfiguration", 50 | dependencies: [ 51 | "MungoHealer", 52 | "Toml", 53 | "BartyCrouchUtility", 54 | ] 55 | ), 56 | .testTarget( 57 | name: "BartyCrouchConfigurationTests", 58 | dependencies: [ 59 | "BartyCrouchConfiguration", 60 | .product(name: "CustomDump", package: "swift-custom-dump"), 61 | "Toml", 62 | ] 63 | ), 64 | .target( 65 | name: "BartyCrouchTranslator", 66 | dependencies: ["HandySwift", "Microya", "MungoHealer"] 67 | ), 68 | .testTarget( 69 | name: "BartyCrouchTranslatorTests", 70 | dependencies: ["BartyCrouchTranslator"], 71 | exclude: ["Secrets/secrets.json.sample"], 72 | resources: [ 73 | .copy("Secrets/secrets.json") 74 | ] 75 | ), 76 | .target(name: "BartyCrouchUtility"), 77 | ] 78 | ) 79 | -------------------------------------------------------------------------------- /Sources/BartyCrouchConfiguration/Options/UpdateOptions/TransformOptions.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import MungoHealer 4 | import Toml 5 | 6 | public struct TransformOptions { 7 | public let codePaths: [String] 8 | public let subpathsToIgnore: [String] 9 | public let localizablePaths: [String] 10 | public let transformer: Transformer 11 | public let supportedLanguageEnumPath: String 12 | public let typeName: String 13 | public let translateMethodName: String 14 | public let customLocalizableName: String? 15 | public let separateWithEmptyLine: Bool 16 | } 17 | 18 | extension TransformOptions: TomlCodable { 19 | static func make(toml: Toml) throws -> TransformOptions { 20 | let update: String = "update" 21 | let transform: String = "transform" 22 | 23 | guard 24 | let transformer = Transformer( 25 | rawValue: toml.string(update, transform, "transformer") ?? Transformer.foundation.rawValue 26 | ) 27 | else { 28 | throw MungoError( 29 | source: .invalidUserInput, 30 | message: "Unknown `transformer` provided in [update.code.transform]. Supported: \(Transformer.allCases)" 31 | ) 32 | } 33 | 34 | return TransformOptions( 35 | codePaths: toml.filePaths(update, transform, singularKey: "codePath", pluralKey: "codePaths"), 36 | subpathsToIgnore: toml.array(update, transform, "subpathsToIgnore") ?? Constants.defaultSubpathsToIgnore, 37 | localizablePaths: toml.filePaths( 38 | update, 39 | transform, 40 | singularKey: "localizablePath", 41 | pluralKey: "localizablePaths" 42 | ), 43 | transformer: transformer, 44 | supportedLanguageEnumPath: toml.string(update, transform, "supportedLanguageEnumPath") ?? ".", 45 | typeName: toml.string(update, transform, "typeName") ?? "BartyCrouch", 46 | translateMethodName: toml.string(update, transform, "translateMethodName") ?? "translate", 47 | customLocalizableName: toml.string(update, transform, "customLocalizableName"), 48 | separateWithEmptyLine: toml.bool(update, transform, "separateWithEmptyLine") ?? true 49 | ) 50 | } 51 | 52 | func tomlContents() -> String { 53 | var lines: [String] = ["[update.transform]"] 54 | 55 | lines.append("codePaths = \(self.codePaths)") 56 | lines.append("subpathsToIgnore = \(self.subpathsToIgnore)") 57 | lines.append("localizablePaths = \(self.localizablePaths)") 58 | lines.append(#"transformer = "\#(self.transformer.rawValue)""#) 59 | lines.append(#"supportedLanguageEnumPath = "\#(self.supportedLanguageEnumPath)""#) 60 | lines.append(#"typeName = "\#(self.typeName)""#) 61 | lines.append(#"translateMethodName = "\#(self.translateMethodName)""#) 62 | 63 | if let customLocalizableName = customLocalizableName { 64 | lines.append(#"customLocalizableName = "\#(customLocalizableName)""#) 65 | } 66 | 67 | lines.append("separateWithEmptyLine = \(self.separateWithEmptyLine)") 68 | 69 | return lines.joined(separator: "\n") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/iOS/Base.lproj/Example.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/DeeplApi/DeepLApi.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Microya 3 | 4 | // Documentation can be found here: https://www.deepl.com/ja/docs-api/ 5 | 6 | enum DeepLApi { 7 | case translate(texts: [String], from: Language, to: Language, apiKey: String) 8 | 9 | static let maximumTextsPerRequest: Int = 25 10 | static let maximumTextsLengthPerRequest: Int = 5_000 11 | 12 | static func textBatches(forTexts texts: [String]) -> [[String]] { 13 | var batches: [[String]] = [] 14 | var currentBatch: [String] = [] 15 | var currentBatchTotalLength: Int = 0 16 | 17 | for text in texts { 18 | if currentBatch.count < maximumTextsPerRequest 19 | && text.count + currentBatchTotalLength < maximumTextsLengthPerRequest 20 | { 21 | currentBatch.append(text) 22 | currentBatchTotalLength += text.count 23 | } 24 | else { 25 | batches.append(currentBatch) 26 | 27 | currentBatch = [text] 28 | currentBatchTotalLength = text.count 29 | } 30 | } 31 | 32 | return batches 33 | } 34 | } 35 | 36 | extension DeepLApi: Endpoint { 37 | typealias ClientErrorType = DeepLTranslateErrorResponse 38 | 39 | enum ApiType { 40 | case free 41 | case pro 42 | } 43 | 44 | var decoder: JSONDecoder { 45 | let decoder = JSONDecoder() 46 | decoder.keyDecodingStrategy = .convertFromSnakeCase 47 | return decoder 48 | } 49 | 50 | var subpath: String { 51 | switch self { 52 | case .translate: 53 | return "/v2/translate" 54 | } 55 | } 56 | 57 | var method: HttpMethod { 58 | switch self { 59 | case .translate(let texts, let sourceLanguage, let targetLanguage, let authKey): 60 | 61 | let authKeyItem = URLQueryItem(name: "auth_key", value: authKey) 62 | let textItems = texts.map { URLQueryItem(name: "text", value: $0) } 63 | let targetLangItem = URLQueryItem(name: "target_lang", value: targetLanguage.deepLParameterValue) 64 | let sourceLangItem = URLQueryItem(name: "source_lang", value: sourceLanguage.deepLParameterValue) 65 | 66 | var components = URLComponents() 67 | components.queryItems = ([authKeyItem, targetLangItem, sourceLangItem] + textItems).compactMap { $0 } 68 | 69 | guard var queryItemsString = components.string else { 70 | fatalError("Invalid arguments.") 71 | } 72 | // queryItemsString starts with a ? but post API expects the query string without leading ? 73 | if queryItemsString.hasPrefix("?") { 74 | queryItemsString.removeFirst() 75 | } 76 | 77 | return .post(body: queryItemsString.data(using: .utf8)!) 78 | } 79 | } 80 | 81 | var headers: [String: String] { 82 | ["Content-Type": "application/x-www-form-urlencoded"] 83 | } 84 | 85 | static func baseUrl(for apiType: ApiType) -> URL { 86 | switch apiType { 87 | case .free: 88 | return URL(string: "https://api-free.deepl.com")! 89 | 90 | case .pro: 91 | return URL(string: "https://api.deepl.com")! 92 | } 93 | } 94 | } 95 | 96 | private extension Language { 97 | var deepLParameterValue: String { 98 | switch self { 99 | case .chineseSimplified: 100 | return "ZH" 101 | 102 | default: 103 | return rawValue.uppercased() 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/tvOS/Base.lproj/Example.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Tests/Resources/Storyboards/macOS/Base.lproj/Example.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 29 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at github [at] cihatguenduez [dot] de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | 76 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/CommandLineActorTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //// swiftlint:disable force_try 5 | // 6 | //class CommandLineActorTests: XCTestCase { 7 | // // MARK: - Stored Properties 8 | // static let stringsFilesDirPath: String = "\(BASE_DIR)/Tests/Resources/StringsFiles" 9 | // 10 | // let codeFilesDirPath: String = "\(BASE_DIR)/Tests/Resources/CodeFiles/UnsortedKeys" 11 | // let unsortedKeysStringsFilePath: String = "\(stringsFilesDirPath)/UnsortedKeys/Base.lproj/Localizable.strings" 12 | // let unsortedKeysDirPath: String = "\(stringsFilesDirPath)/UnsortedKeys" 13 | // 14 | // // MARK: - Test Callbacks 15 | // override func setUp() { 16 | // super.setUp() 17 | // 18 | // if FileManager.default.fileExists(atPath: unsortedKeysStringsFilePath + ".backup") { 19 | // try! FileManager.default.removeItem(atPath: unsortedKeysStringsFilePath + ".backup") 20 | // } 21 | // 22 | // try! FileManager.default.copyItem(atPath: unsortedKeysStringsFilePath, toPath: unsortedKeysStringsFilePath + ".backup") 23 | // } 24 | // 25 | // override func tearDown() { 26 | // super.tearDown() 27 | // 28 | // try! FileManager.default.removeItem(atPath: unsortedKeysStringsFilePath) 29 | // try! FileManager.default.copyItem(atPath: unsortedKeysStringsFilePath + ".backup", toPath: unsortedKeysStringsFilePath) 30 | // try! FileManager.default.removeItem(atPath: unsortedKeysStringsFilePath + ".backup") 31 | // } 32 | // 33 | // // MARK: - Test Methods 34 | // func testActOnCode() { 35 | // let args = ["bartycrouch", "code", "-p", codeFilesDirPath, "-l", unsortedKeysDirPath, "-a"] 36 | // 37 | // CommandLineParser(arguments: args).parse { commonOptions, subCommandOptions in 38 | // CommandLineActor().act(commonOptions: commonOptions, subCommandOptions: subCommandOptions) 39 | // 40 | // guard let updater = StringsFileUpdater(path: self.unsortedKeysStringsFilePath) else { 41 | // XCTFail("Updater could not be initialized. Is the file missing? Path: \(self.unsortedKeysStringsFilePath)") 42 | // return 43 | // } 44 | // 45 | // let resultingKeys = updater.findTranslations(inString: updater.oldContentString).map { $0.key } 46 | // let expectedKeys = ["DDD", "ggg", "BBB", "aaa", "FFF", "eee", "ccc"] 47 | // 48 | // XCTAssertEqual(resultingKeys, expectedKeys) 49 | // } 50 | // } 51 | // 52 | // func testActOnCodeWithSortedOption() { 53 | // let args = ["bartycrouch", "code", "-p", codeFilesDirPath, "-l", unsortedKeysDirPath, "-a", "-s"] 54 | // 55 | // CommandLineParser(arguments: args).parse { commonOptions, subCommandOptions in 56 | // CommandLineActor().act(commonOptions: commonOptions, subCommandOptions: subCommandOptions) 57 | // 58 | // guard let updater = StringsFileUpdater(path: self.unsortedKeysStringsFilePath) else { 59 | // XCTFail("Updater could not be initialized. Is the file missing? Path: \(self.unsortedKeysStringsFilePath)") 60 | // return 61 | // } 62 | // 63 | // let resultingKeys = updater.findTranslations(inString: updater.oldContentString).map { $0.key } 64 | // let expectedKeys = ["aaa", "BBB", "eee", "FFF", "ggg", "ccc", "DDD"] 65 | // 66 | // XCTAssertEqual(resultingKeys, expectedKeys) 67 | // } 68 | // } 69 | //} 70 | // 71 | //// swiftlint:enable force_try 72 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/OldCommandLine/CodeCommander.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCLI 3 | 4 | // NOTE: 5 | // This file was not refactored as port of the work/big-refactoring branch for version 4.0 to prevent unexpected behavior changes. 6 | // A rewrite after writing extensive tests for the expected behavior could improve readebility, extensibility and performance. 7 | 8 | enum CodeCommanderError: Error { 9 | case missingPath 10 | case pathNotADirectory 11 | case enumeratorCreationFailed 12 | case findFilesFailed 13 | } 14 | 15 | private enum CodeCommanderConstants { 16 | static let sourceCodeExtensions: Set = ["h", "m", "mm", "swift"] 17 | } 18 | 19 | /// Sends `xcrun extractLocStrings` commands with specified input/output paths to bash. 20 | public final class CodeCommander { 21 | // MARK: - Stored Type Properties 22 | public static let shared = CodeCommander() 23 | 24 | // MARK: - Instance Methods 25 | public func export( 26 | stringsFilesToPath stringsFilePath: String, 27 | fromCodeInDirectoryPath codeDirectoryPath: String, 28 | customFunction: String?, 29 | usePlistArguments: Bool, 30 | subpathsToIgnore: [String] 31 | ) throws { 32 | let files = try findFiles(in: codeDirectoryPath, subpathsToIgnore: subpathsToIgnore) 33 | let customFunctionArgs = customFunction != nil ? ["-s", "\(customFunction!)"] : [] 34 | 35 | let argumentsWithoutTheFiles = ["extractLocStrings"] + ["-o", stringsFilePath] + customFunctionArgs + ["-q"] 36 | 37 | let arguments = try appendFiles( 38 | files, 39 | inListOfArguments: argumentsWithoutTheFiles, 40 | usePlistArguments: usePlistArguments 41 | ) 42 | try Task.run("/usr/bin/xcrun", arguments: arguments) 43 | } 44 | 45 | func findFiles(in codeDirectoryPath: String, subpathsToIgnore: [String]) throws -> [String] { 46 | let fileManager = FileManager.default 47 | 48 | var isDirectory: ObjCBool = false 49 | guard fileManager.fileExists(atPath: codeDirectoryPath, isDirectory: &isDirectory) else { 50 | throw CodeCommanderError.missingPath 51 | } 52 | 53 | guard isDirectory.boolValue else { 54 | throw CodeCommanderError.pathNotADirectory 55 | } 56 | 57 | guard 58 | let enumerator = fileManager.enumerator( 59 | at: URL(fileURLWithPath: codeDirectoryPath), 60 | includingPropertiesForKeys: [] 61 | ) 62 | else { 63 | throw CodeCommanderError.enumeratorCreationFailed 64 | } 65 | 66 | var matchedFiles = [String]() 67 | let codeFilesSearch = CodeFilesSearch(baseDirectoryPath: codeDirectoryPath) 68 | 69 | while let anURL = enumerator.nextObject() as? URL { 70 | if CodeCommanderConstants.sourceCodeExtensions.contains(anURL.pathExtension) 71 | && !codeFilesSearch.shouldSkipFile(at: anURL, subpathsToIgnore: subpathsToIgnore) 72 | { 73 | matchedFiles.append(anURL.path) 74 | } 75 | } 76 | 77 | return matchedFiles 78 | } 79 | 80 | // In the existing list of arguments it appends also the files arguments. 81 | func appendFiles( 82 | _ files: [String], 83 | inListOfArguments existingArguments: [String], 84 | usePlistArguments: Bool 85 | ) throws -> [String] { 86 | if usePlistArguments { 87 | let fileArgumentsPlistFile = try ExtractLocStrings().writeFilesArgumentsInPlist(files) 88 | return existingArguments + ["-f", fileArgumentsPlistFile] 89 | } 90 | else { 91 | let completeArgumentList = existingArguments + files 92 | return completeArgumentList 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/CommandLineParserTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //class CommandLineParserTests: XCTestCase { 5 | // func testIfCommentCommandIsAdded() { 6 | // CommandLineParser(arguments: ["bartycrouch", "code", "-p", ".", "-l", ".", "--override-comments"]).parse { _, subCommandOptions in 7 | // switch subCommandOptions { 8 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 9 | // XCTAssertTrue(overrideComments.value) 10 | // 11 | // default: 12 | // XCTAssertTrue(false) 13 | // } 14 | // } 15 | // 16 | // CommandLineParser(arguments: ["bartycrouch", "code", "-p", ".", "-l", ".", "-c"]).parse { _, subCommandOptions in 17 | // switch subCommandOptions { 18 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 19 | // XCTAssertTrue(overrideComments.value) 20 | // 21 | // default: 22 | // XCTAssertTrue(false) 23 | // } 24 | // } 25 | // } 26 | // 27 | // func testIfCommentCommandIsNotAdded() { 28 | // CommandLineParser( 29 | // arguments: ["bartycrouch", "translate", "-p", ".", "-i", "no", "-s", "abc", "-l", ".", "--override-comments"] 30 | // ).parse { _, subCommandOptions in 31 | // switch subCommandOptions { 32 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 33 | // XCTAssertTrue(!overrideComments.value) 34 | // 35 | // default: 36 | // XCTAssertTrue(true) 37 | // } 38 | // } 39 | // 40 | // CommandLineParser( 41 | // arguments: ["bartycrouch", "translate", "-p", ".", "-i", "no", "-s", "abc", "-l", ".", "-c"] 42 | // ).parse { _, subCommandOptions in 43 | // switch subCommandOptions { 44 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 45 | // XCTAssertTrue(!overrideComments.value) 46 | // 47 | // default: 48 | // XCTAssertTrue(true) 49 | // } 50 | // } 51 | // 52 | // CommandLineParser( 53 | // arguments: ["bartycrouch", "interfaces", "-p", ".", "-i", "no", "--override-comments"] 54 | // ).parse { _, subCommandOptions in 55 | // switch subCommandOptions { 56 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 57 | // XCTAssertTrue(!overrideComments.value) 58 | // 59 | // default: 60 | // XCTAssertTrue(true) 61 | // } 62 | // } 63 | // 64 | // CommandLineParser( 65 | // arguments: ["bartycrouch", "interfaces", "-p", ".", "-c"] 66 | // ).parse { _, subCommandOptions in 67 | // switch subCommandOptions { 68 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 69 | // XCTAssertTrue(!overrideComments.value) 70 | // 71 | // default: 72 | // XCTAssertTrue(true) 73 | // } 74 | // } 75 | // 76 | // CommandLineParser( 77 | // arguments: ["bartycrouch", "code", "-p", ".", "-l", "."] 78 | // ).parse { _, subCommandOptions in 79 | // switch subCommandOptions { 80 | // case let .codeOptions(_, _, _, overrideComments, _, _, _, _, _): 81 | // XCTAssertTrue(!overrideComments.value) 82 | // 83 | // default: 84 | // XCTAssertTrue(true) 85 | // } 86 | // } 87 | // } 88 | //} 89 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/StringsFilesSearch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // NOTE: 4 | // This file was not refactored as part of the work/big-refactoring branch for version 4.0 to prevent unexpected behavior changes. 5 | // A rewrite after writing extensive tests for the expected behavior could improve readability, extensibility and performance. 6 | 7 | /// Searchs for `.strings` files given a base internationalized Storyboard. 8 | public final class StringsFilesSearch: FilesSearchable { 9 | // MARK: - Stored Type Properties 10 | public static let shared = StringsFilesSearch() 11 | 12 | // MARK: - Instance Methods 13 | public func findAllIBFiles( 14 | within baseDirectoryPath: String, 15 | subpathsToIgnore: [String], 16 | withLocale locale: String = "Base" 17 | ) -> [String] { 18 | // swiftlint:disable:next force_try 19 | let ibFileRegex = try! NSRegularExpression( 20 | pattern: "^(.*\\/)?\(locale).lproj.*\\.(storyboard|xib)\\z", 21 | options: .caseInsensitive 22 | ) 23 | return self.findAllFilePaths( 24 | inDirectoryPath: baseDirectoryPath, 25 | subpathsToIgnore: subpathsToIgnore, 26 | matching: ibFileRegex 27 | ) 28 | } 29 | 30 | public func findAllStringsFiles( 31 | within baseDirectoryPath: String, 32 | withLocale locale: String, 33 | subpathsToIgnore: [String] 34 | ) -> [String] { 35 | // swiftlint:disable:next force_try 36 | let stringsFileRegex = try! NSRegularExpression( 37 | pattern: "^(.*\\/)?\(locale).lproj.*\\.strings\\z", 38 | options: .caseInsensitive 39 | ) 40 | return self.findAllFilePaths( 41 | inDirectoryPath: baseDirectoryPath, 42 | subpathsToIgnore: subpathsToIgnore, 43 | matching: stringsFileRegex 44 | ) 45 | } 46 | 47 | public func findAllStringsFiles( 48 | within baseDirectoryPath: String, 49 | withFileName fileName: String, 50 | subpathsToIgnore: [String] 51 | ) -> [String] { 52 | // swiftlint:disable:next force_try 53 | let stringsFileRegex = try! NSRegularExpression( 54 | pattern: ".*\\.lproj/\(fileName)\\.strings\\z", 55 | options: .caseInsensitive 56 | ) 57 | return self.findAllFilePaths( 58 | inDirectoryPath: baseDirectoryPath, 59 | subpathsToIgnore: subpathsToIgnore, 60 | matching: stringsFileRegex 61 | ) 62 | } 63 | 64 | public func findAllStringsFiles(within baseDirectoryPath: String, subpathsToIgnore: [String]) -> [String] { 65 | // swiftlint:disable:next force_try 66 | let stringsFileRegex = try! NSRegularExpression(pattern: ".*\\.lproj/.+\\.strings\\z", options: .caseInsensitive) 67 | return self.findAllFilePaths( 68 | inDirectoryPath: baseDirectoryPath, 69 | subpathsToIgnore: subpathsToIgnore, 70 | matching: stringsFileRegex 71 | ) 72 | } 73 | 74 | public func findAllLocalesForStringsFile(sourceFilePath: String) -> [String] { 75 | var pathComponents = sourceFilePath.components(separatedBy: "/") 76 | let storyboardName: String = { 77 | var fileNameComponents = pathComponents.last!.components(separatedBy: ".") 78 | fileNameComponents.removeLast() 79 | return fileNameComponents.joined(separator: ".") 80 | }() 81 | 82 | pathComponents.removeLast() // Remove last path component from folder/base.lproj/some.storyboard 83 | pathComponents.removeLast() // Remove last path component from folder/base.lproj 84 | 85 | let folderWithLanguageSubfoldersPath = pathComponents.joined(separator: "/") 86 | 87 | do { 88 | let filesInDirectory = try FileManager.default.contentsOfDirectory(atPath: folderWithLanguageSubfoldersPath) 89 | let languageDirPaths = filesInDirectory.filter { $0.range(of: ".lproj") != nil && $0 != "Base.lproj" } 90 | return languageDirPaths.map { 91 | [folderWithLanguageSubfoldersPath, $0, "\(storyboardName).strings"].joined(separator: "/") 92 | } 93 | } 94 | catch { 95 | return [] 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/CommandLine/ExtractLocStringsCommanderTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //class ExtractLocStringsCommanderTests: XCTestCase { 5 | // // MARK: - Stored Properties 6 | // let baseMultipleArgumentFunctionDirectories: [(String?, String)] = [ 7 | // (nil, "\(BASE_DIR)/Tests/Resources/MultipleArgumentsCode"), 8 | // ("BCLocalizedString", "\(BASE_DIR)/Tests/Resources/MultipleArgumentsCodeCustomFunction") 9 | // ] 10 | // 11 | // override func tearDown() { 12 | // super.tearDown() 13 | // 14 | // for (_, directory) in baseMultipleArgumentFunctionDirectories { 15 | // removeLocalizableStringsFilesRecursively(in: URL(fileURLWithPath: directory)) 16 | // } 17 | // } 18 | // 19 | // // MARK: - Test Methods 20 | // func test2Arguments() { 21 | // for (functionName, directory) in baseMultipleArgumentFunctionDirectories { 22 | // assert( 23 | // ExtractLocStringsCommander.shared, 24 | // takesCodeIn: "\(directory)/2Arguments", 25 | // customFunction: functionName, 26 | // producesResult: [ 27 | // "/* test comment */", 28 | // "\"test\" = \"test\";", 29 | // "", 30 | // "" 31 | // ] 32 | // ) 33 | // } 34 | // } 35 | // 36 | // func test3ArgumentsValue() { 37 | // for (functionName, directory) in baseMultipleArgumentFunctionDirectories { 38 | // assert( 39 | // ExtractLocStringsCommander.shared, 40 | // takesCodeIn: "\(directory)/3Arguments", 41 | // customFunction: functionName, 42 | // producesResult: [ 43 | // "/* test comment */", 44 | // "\"test\" = \"test value\";", 45 | // "", 46 | // "" 47 | // ] 48 | // ) 49 | // } 50 | // } 51 | // 52 | // func test4ArgumentsBundleValue() { 53 | // for (functionName, directory) in baseMultipleArgumentFunctionDirectories { 54 | // assert( 55 | // ExtractLocStringsCommander.shared, 56 | // takesCodeIn: "\(directory)/4Arguments", 57 | // customFunction: functionName, 58 | // producesResult: [ 59 | // "/* test comment */", 60 | // "\"test\" = \"test value\";", 61 | // "", 62 | // "" 63 | // ] 64 | // ) 65 | // } 66 | // } 67 | // 68 | // func assert( 69 | // _ codeCommander: CodeCommander, takesCodeIn directory: String, customFunction: String?, producesResult expectedLocalizableContentLines: [String] 70 | // ) { 71 | // let exportSuccess = codeCommander.export(stringsFilesToPath: directory, fromCodeInDirectoryPath: directory, customFunction: customFunction) 72 | // XCTAssertTrue(exportSuccess, "Failed for \(directory) with function \"\(customFunction ?? "NSLocalizedString")\"") 73 | // 74 | // do { 75 | // let contentsOfStringsFile = try String(contentsOfFile: directory + "/Localizable.strings") 76 | // let linesInStringsFile = contentsOfStringsFile.components(separatedBy: CharacterSet.newlines) 77 | // XCTAssertEqual( 78 | // linesInStringsFile, expectedLocalizableContentLines, "Failed for \(directory) with function \"\(customFunction ?? "NSLocalizedString")\"" 79 | // ) 80 | // } catch { 81 | // XCTFail("Failed for \(directory) with function \"\(customFunction ?? "NSLocalizedString")\"") 82 | // } 83 | // } 84 | // 85 | // func removeLocalizableStringsFilesRecursively(in directory: URL) { 86 | // let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: [], options: [], errorHandler: nil)! 87 | // while case let file as URL = enumerator.nextObject() { 88 | // if file.pathExtension == "strings" { 89 | // try? FileManager.default.removeItem(at: file) 90 | // } 91 | // } 92 | // } 93 | //} 94 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/MicrosoftTranslatorApi/Models/Language.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The languages supported. 4 | public enum Language: String { 5 | case afrikaans = "af" 6 | case arabic = "ar" 7 | case assamese = "as" 8 | case bangla = "bn" 9 | case bosnian = "bs" 10 | case bulgarian = "bg" 11 | case cantoneseTraditional = "yue" 12 | case catalan = "ca" 13 | case chineseSimplified = "zh-Hans" 14 | case chineseTraditional = "zh-Hant" 15 | case croatian = "hr" 16 | case czech = "cs" 17 | case dari = "prs" 18 | case danish = "da" 19 | case dutch = "nl" 20 | case english = "en" 21 | case estonian = "et" 22 | case fijian = "fj" 23 | case filipino = "fil" 24 | case finnish = "fi" 25 | case french = "fr" 26 | case frenchCanada = "fr-ca" 27 | case german = "de" 28 | case greek = "el" 29 | case gujarati = "gu" 30 | case haitianCreole = "ht" 31 | case hebrew = "he" 32 | case hindi = "hi" 33 | case hmongDaw = "mww" 34 | case hungarian = "hu" 35 | case icelandic = "is" 36 | case indonesian = "id" 37 | case irish = "ga" 38 | case italian = "it" 39 | case japanese = "ja" 40 | case kannada = "kn" 41 | case kazakh = "kk" 42 | case klingon = "tlh-Latn" 43 | case klingonPlqad = "tlh-Piqd" 44 | case korean = "ko" 45 | case kurdishCentral = "ku" 46 | case kurdishNorthern = "kmr" 47 | case latvian = "lv" 48 | case lithuanian = "lt" 49 | case malagasy = "mg" 50 | case malay = "ms" 51 | case malayalam = "ml" 52 | case maltese = "mt" 53 | case maori = "mi" 54 | case marathi = "mr" 55 | case norwegian = "nb" 56 | case odia = "or" 57 | case pashto = "ps" 58 | case persian = "fa" 59 | case polish = "pl" 60 | case portugueseBrazil = "pt" 61 | case portuguesePortugal = "pt-pt" 62 | case punjabi = "pa" 63 | case queretaroOtomi = "otq" 64 | case romanian = "ro" 65 | case russian = "ru" 66 | case samoan = "sm" 67 | case serbianCyrillic = "sr-Cyrl" 68 | case serbianLatin = "sr-Latn" 69 | case slovak = "sk" 70 | case slovenian = "sl" 71 | case spanish = "es" 72 | case swahili = "sw" 73 | case swedish = "sv" 74 | case tahitian = "ty" 75 | case tamil = "ta" 76 | case telugu = "te" 77 | case thai = "th" 78 | case tongan = "to" 79 | case turkish = "tr" 80 | case ukrainian = "uk" 81 | case urdu = "ur" 82 | case vietnamese = "vi" 83 | case welsh = "cy" 84 | case yucatecMaya = "yua" 85 | 86 | /// Returns the language object matching the given lang code & region. 87 | /// 88 | /// - Parameters: 89 | /// - languageCode: The 2 or 3-letter language code. See list of languages in `Language` enum to check if yours is supported. 90 | /// - region: The region code further specifying the language. See list of languages in `Language` enum to check if yours is supported. 91 | /// - Returns: The language object best matching your specified languageCode and region combination. 92 | public static func with(languageCode: String, region: String?) -> Language? { 93 | guard let region = region else { return Language(rawValue: languageCode) } 94 | return Language(rawValue: "\(languageCode)-\(region)") 95 | ?? Language(rawValue: "\(languageCode)-\(region.lowercased())") 96 | ?? Language(rawValue: "\(languageCode)-\(region.capitalized)") 97 | ?? Language(rawValue: languageCode) 98 | } 99 | 100 | /// Returns the language object matching the given lang code & region. 101 | /// 102 | /// - Parameters: 103 | /// - languageCode: The 2 or 3-letter language code. See list of languages in `Language` enum to check if yours is supported. 104 | /// - region: The region code further specifying the language. See list of languages in `Language` enum to check if yours is supported. 105 | /// - Returns: The language object best matching your specified languageCode and region combination. 106 | public static func with(locale: String) -> Language? { 107 | let separator: Character = "-" 108 | let components = locale.split(separator: separator) 109 | if components.count > 1 { 110 | return with(languageCode: String(components[0]), region: String(components[1])) 111 | } 112 | else { 113 | return with(languageCode: String(components[0]), region: nil) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/BartyCrouchTranslator/BartyCrouchTranslator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Microya 3 | import MungoHealer 4 | 5 | /// Translator service to translate texts from one language to another. 6 | /// 7 | /// NOTE: Currently only supports Microsoft Translator Text API using a subscription key. 8 | public final class BartyCrouchTranslator { 9 | public typealias Translation = (language: Language, translatedText: String) 10 | 11 | /// The supported translation services. 12 | public enum TranslationService { 13 | /// The Microsoft Translator Text API. 14 | /// Website: https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-info-overview 15 | /// 16 | /// - Parameters: 17 | /// - subscriptionKey: The `Ocp-Apim-Subscription-Key`, also called "Azure secret key" in the docs. 18 | case microsoft(subscriptionKey: String) 19 | case deepL(apiKey: String) 20 | } 21 | 22 | private let microsoftProvider = ApiProvider(baseUrl: MicrosoftTranslatorApi.baseUrl) 23 | private let deepLProvider: ApiProvider 24 | 25 | private let translationService: TranslationService 26 | 27 | /// Creates a new translator object configured to use the specified translation service. 28 | public init( 29 | translationService: TranslationService 30 | ) { 31 | self.translationService = translationService 32 | 33 | let deepLApiType: DeepLApi.ApiType 34 | if case let .deepL(apiKey) = translationService { 35 | deepLApiType = apiKey.hasSuffix(":fx") ? .free : .pro 36 | } 37 | else { 38 | deepLApiType = .pro 39 | } 40 | 41 | deepLProvider = ApiProvider(baseUrl: DeepLApi.baseUrl(for: deepLApiType)) 42 | } 43 | 44 | /// Translates the given text from a given language to one or multiple given other languages. 45 | /// 46 | /// - Parameters: 47 | /// - text: The text to be translated. 48 | /// - sourceLanguage: The source language the given text is in. 49 | /// - targetLanguages: An array of other languages to be translated to. 50 | /// - Returns: A `Result` wrapper containing an array of translations if the request was successful, else the related error. 51 | public func translate( 52 | text: String, 53 | from sourceLanguage: Language, 54 | to targetLanguages: [Language] 55 | ) -> Result<[Translation], MungoError> { 56 | switch translationService { 57 | case let .microsoft(subscriptionKey): 58 | let endpoint = MicrosoftTranslatorApi.translate( 59 | texts: [text], 60 | from: sourceLanguage, 61 | to: targetLanguages, 62 | microsoftSubscriptionKey: subscriptionKey 63 | ) 64 | 65 | switch microsoftProvider.performRequestAndWait(on: endpoint, decodeBodyTo: [TranslateResponse].self) { 66 | case let .success(translateResponses): 67 | if let translations: [Translation] = translateResponses.first?.translations 68 | .map({ (Language.with(locale: $0.to)!, $0.text) }) 69 | { 70 | return .success(translations) 71 | } 72 | else { 73 | return .failure( 74 | MungoError(source: .internalInconsistency, message: "Could not fetch translation(s) for '\(text)'.") 75 | ) 76 | } 77 | 78 | case let .failure(failure): 79 | return .failure(MungoError(source: .internalInconsistency, message: failure.localizedDescription)) 80 | } 81 | 82 | case let .deepL(apiKey): 83 | var allTranslations: [Translation] = [] 84 | for targetLanguage in targetLanguages { 85 | let endpoint = DeepLApi.translate(texts: [text], from: sourceLanguage, to: targetLanguage, apiKey: apiKey) 86 | switch deepLProvider.performRequestAndWait(on: endpoint, decodeBodyTo: DeepLTranslateResponse.self) { 87 | case let .success(translateResponse): 88 | let translations: [Translation] = translateResponse.translations.map({ (targetLanguage, $0.text) }) 89 | allTranslations.append(contentsOf: translations) 90 | 91 | case let .failure(failure): 92 | return .failure(MungoError(source: .internalInconsistency, message: failure.localizedDescription)) 93 | } 94 | } 95 | 96 | return .success(allTranslations) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/BartyCrouchKitTests/FileHandling/StringsFilesSearchTests.swift: -------------------------------------------------------------------------------- 1 | //@testable import BartyCrouchKit 2 | //import XCTest 3 | // 4 | //class StringsFilesSearchTests: XCTestCase { 5 | // // MARK: - Test Methods 6 | // func testFindAllIBFiles() { 7 | // let basePath = "\(BASE_DIR)/Tests" 8 | // 9 | // let expectedIBFilePaths = ["iOS", "macOS", "tvOS"].map { examplePath(platform: $0, locale: "Base", type: "storyboard") } 10 | // 11 | // let results = StringsFilesSearch.shared.findAllIBFiles(within: basePath, withLocale: "Base") 12 | // 13 | // XCTAssertEqual(results.count, expectedIBFilePaths.count) 14 | // XCTAssertEqual(results.sorted(), expectedIBFilePaths.sorted()) 15 | // } 16 | // 17 | // func testFindAllIBFilesInSubFolder() { 18 | // let basePath = "\(BASE_DIR)/Tests/Resources/Storyboards/" 19 | // 20 | // let expectedIBFilePaths = ["iOS", "macOS", "tvOS"].map { examplePath(platform: $0, locale: "Base", type: "storyboard") } 21 | // 22 | // var results = [String]() 23 | // ["iOS", "macOS", "tvOS"].forEach { platform in 24 | // results += StringsFilesSearch.shared.findAllIBFiles(within: basePath + platform, withLocale: "Base") 25 | // } 26 | // 27 | // XCTAssertEqual(results.count, expectedIBFilePaths.count) 28 | // XCTAssertEqual(results.sorted(), expectedIBFilePaths.sorted()) 29 | // } 30 | // 31 | // func testFindAllStringsFiles() { 32 | // let basePath = "\(BASE_DIR)/Tests" 33 | // 34 | // let expectedStringsFilePaths = ["iOS", "macOS", "tvOS"].map { examplePath(platform: $0, locale: "de", type: "strings") } 35 | // + ["\(BASE_DIR)/Tests/Resources/StringsFiles/de.lproj/Localizable.strings"] 36 | // 37 | // let results = StringsFilesSearch.shared.findAllStringsFiles(within: basePath, withLocale: "de") 38 | // 39 | // XCTAssertEqual(results.count, expectedStringsFilePaths.count) 40 | // XCTAssertEqual(results.sorted(), expectedStringsFilePaths.sorted()) 41 | // } 42 | // 43 | // func testFindAllStringsFilesWithLocaleInSubFolder() { 44 | // let basePath = "\(BASE_DIR)/Tests/Resources/" 45 | // 46 | // let expectedStringsFilePaths = ["iOS", "macOS", "tvOS"].map { examplePath(platform: $0, locale: "de", type: "strings") } 47 | // + ["\(BASE_DIR)/Tests/Resources/StringsFiles/de.lproj/Localizable.strings"] 48 | // 49 | // var results = [String]() 50 | // ["iOS", "macOS", "tvOS"].forEach { platform in 51 | // results += StringsFilesSearch.shared.findAllStringsFiles(within: basePath + "Storyboards/" + platform, withLocale: "de") 52 | // } 53 | // results += StringsFilesSearch.shared.findAllStringsFiles(within: basePath + "StringsFiles", withLocale: "de") 54 | // 55 | // XCTAssertEqual(results.count, expectedStringsFilePaths.count) 56 | // XCTAssertEqual(results.sorted(), expectedStringsFilePaths.sorted()) 57 | // } 58 | // 59 | // func testiOSFindAllLocalesForStringsFile() { 60 | // let baseStoryboardPath = examplePath(platform: "iOS", locale: "base", type: "storyboard") 61 | // let expectedStringsPaths = ["de", "en", "ja", "zh-Hans"].map { examplePath(platform: "iOS", locale: $0, type: "strings") } 62 | // 63 | // let results = StringsFilesSearch.shared.findAllLocalesForStringsFile(sourceFilePath: baseStoryboardPath) 64 | // 65 | // XCTAssertEqual(results.count, expectedStringsPaths.count) 66 | // XCTAssertEqual(results.sorted(), expectedStringsPaths.sorted()) 67 | // } 68 | // 69 | // // MARK: - Performance Tests 70 | // func testSearchFilesPerformance() { 71 | // measure { 72 | // 100.times { 73 | // _ = StringsFilesSearch.shared.findAllIBFiles(within: "\(BASE_DIR)/Tests/Resources/Storyboards") 74 | // _ = StringsFilesSearch.shared.findAllStringsFiles(within: "\(BASE_DIR)/Tests/Resources/StringsFiles", withLocale: "en") 75 | // } 76 | // } 77 | // } 78 | // 79 | // // MARK: - Helpers 80 | // func examplePath(platform: String, locale: String, type: String) -> String { 81 | // return "\(BASE_DIR)/Tests/Resources/Storyboards/\(platform)/\(locale).lproj/Example.\(type)" 82 | // } 83 | // 84 | // func stringsFilePath(name: String, locale: String, subpath: String = "") -> String { 85 | // let path = !subpath.isEmpty && subpath.last != "/" ? "\(subpath)/" : subpath 86 | // return "\(BASE_DIR)/Tests/Resources/StringsFiles/\(path)\(locale).lproj/\(name).strings" 87 | // } 88 | //} 89 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Demo/Untouched/Demo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 35 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/Globals/PrintLevel.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable cyclomatic_complexity file_types_order 2 | 3 | import Foundation 4 | import Rainbow 5 | 6 | /// The print level type. 7 | enum PrintLevel { 8 | /// Print success information. 9 | case success 10 | 11 | /// Print (potentially) long data or less interesting information. Only printed if tool executed in vebose mode. 12 | case verbose 13 | 14 | /// Print any kind of information potentially interesting to users. 15 | case info 16 | 17 | /// Print information that might potentially be problematic. 18 | case warning 19 | 20 | /// Print information that probably is problematic. 21 | case error 22 | 23 | var color: Color { 24 | switch self { 25 | case .success: 26 | return Color.lightGreen 27 | 28 | case .verbose: 29 | return Color.lightCyan 30 | 31 | case .info: 32 | return Color.lightBlue 33 | 34 | case .warning: 35 | return Color.yellow 36 | 37 | case .error: 38 | return Color.red 39 | } 40 | } 41 | } 42 | 43 | /// The output format type. 44 | enum OutputFormatTarget { 45 | /// Output is targeted to a console to be read by developers. 46 | case human 47 | 48 | /// Output is targeted to Xcode. Native support for Xcode Warnings & Errors. 49 | case xcode 50 | } 51 | 52 | /// Prints a message to command line with proper formatting based on level, source & output target. 53 | /// 54 | /// - Parameters: 55 | /// - message: The message to be printed. Don't include `Error!`, `Warning!` or similar information at the beginning. 56 | /// - level: The level of the print statement. 57 | /// - file: The file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. 58 | /// - line: The line within the file this print statement refers to. Used for showing errors/warnings within Xcode if run as script phase. 59 | func print(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { 60 | if TestHelper.shared.isStartedByUnitTests { 61 | TestHelper.shared.printOutputs.append((message, level, file, line)) 62 | return 63 | } 64 | 65 | if GlobalOptions.failOnWarnings.value && level == .warning { 66 | CommandExecution.current.didPrintWarning = true 67 | } 68 | 69 | if GlobalOptions.xcodeOutput.value { 70 | xcodePrint(message, level: level, file: file, line: line) 71 | } 72 | else { 73 | humanPrint(message, level: level, file: file, line: line) 74 | } 75 | } 76 | 77 | private func humanPrint(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { 78 | let location = locationInfo(file: file, line: line)? 79 | .replacingOccurrences(of: FileManager.default.currentDirectoryPath, with: ".") 80 | let message = location != nil ? [location!, message].joined(separator: " ") : message 81 | 82 | switch level { 83 | case .success: 84 | print(currentDateTime(), "✅ ", message.lightGreen) 85 | 86 | case .verbose: 87 | if GlobalOptions.verbose.value { 88 | print(currentDateTime(), "🗣 ", message.lightCyan) 89 | } 90 | 91 | case .info: 92 | print(currentDateTime(), "ℹ️ ", message.lightBlue) 93 | 94 | case .warning: 95 | print(currentDateTime(), "⚠️ ", message.yellow) 96 | 97 | case .error: 98 | print(currentDateTime(), "❌ ", message.lightRed) 99 | } 100 | } 101 | 102 | private func currentDateTime() -> String { 103 | let dateFormatter = DateFormatter() 104 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" 105 | let dateTime = dateFormatter.string(from: Date()) 106 | return "\(dateTime):" 107 | } 108 | 109 | private func xcodePrint(_ message: String, level: PrintLevel, file: String? = nil, line: Int? = nil) { 110 | let location = locationInfo(file: file, line: line) 111 | 112 | switch level { 113 | case .success: 114 | if let location = location { 115 | print(location, "success: BartyCrouch: ", message) 116 | } 117 | else { 118 | print("success: BartyCrouch: ", message) 119 | } 120 | 121 | case .verbose: 122 | if GlobalOptions.verbose.value { 123 | if let location = location { 124 | print(location, "verbose: BartyCrouch: ", message) 125 | } 126 | else { 127 | print("verbose: BartyCrouch: ", message) 128 | } 129 | } 130 | 131 | case .info: 132 | if let location = location { 133 | print(location, "info: BartyCrouch: ", message) 134 | } 135 | else { 136 | print("info: BartyCrouch: ", message) 137 | } 138 | 139 | case .warning: 140 | if let location = location { 141 | print(location, "warning: BartyCrouch: ", message) 142 | } 143 | else { 144 | print("warning: BartyCrouch: ", message) 145 | } 146 | 147 | case .error: 148 | if let location = location { 149 | print(location, "error: BartyCrouch: ", message) 150 | } 151 | else { 152 | print("error: BartyCrouch: ", message) 153 | } 154 | } 155 | } 156 | 157 | private func locationInfo(file: String?, line: Int?) -> String? { 158 | guard let file = file else { return nil } 159 | guard let line = line else { return "\(file): " } 160 | return "\(file):\(line): " 161 | } 162 | 163 | private let dispatchGroup = DispatchGroup() 164 | 165 | func measure(task: String, _ closure: () throws -> ResultType) rethrows -> ResultType { 166 | let startDate = Date() 167 | print("Starting Task '\(task)' ...") 168 | 169 | let result = try closure() 170 | 171 | let passedTimeInterval = Date().timeIntervalSince(startDate) 172 | guard passedTimeInterval > 0.1 else { return result } // do not print fast enough tasks 173 | 174 | let passedTimeIntervalNum = NSNumber(value: passedTimeInterval) 175 | let measureTimeFormatter = NumberFormatter() 176 | measureTimeFormatter.minimumIntegerDigits = 1 177 | measureTimeFormatter.maximumFractionDigits = 3 178 | measureTimeFormatter.locale = Locale(identifier: "en") 179 | 180 | print("Task '\(task)' took \(measureTimeFormatter.string(from: passedTimeIntervalNum)!) seconds.") 181 | return result 182 | } 183 | -------------------------------------------------------------------------------- /Tests/BartyCrouchConfigurationTests/ConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | @testable import BartyCrouchConfiguration 2 | import BartyCrouchUtility 3 | import CustomDump 4 | import Toml 5 | import XCTest 6 | 7 | // swiftlint:disable force_try function_body_length 8 | 9 | class ConfigurationTests: XCTestCase { 10 | func testConfigurationMakeDefault() { 11 | do { 12 | let configuration: Configuration = try Configuration.makeDefault() 13 | 14 | XCTAssertEqual(configuration.updateOptions.tasks, [.interfaces, .code, .transform, .normalize]) 15 | 16 | XCTAssertEqual(configuration.updateOptions.interfaces.paths, ["."]) 17 | XCTAssertEqual(configuration.updateOptions.interfaces.defaultToBase, false) 18 | XCTAssertEqual(configuration.updateOptions.interfaces.ignoreEmptyStrings, false) 19 | XCTAssertEqual(configuration.updateOptions.interfaces.unstripped, false) 20 | 21 | XCTAssertEqual(configuration.updateOptions.code.codePaths, ["."]) 22 | XCTAssertEqual(configuration.updateOptions.code.localizablePaths, ["."]) 23 | XCTAssertEqual(configuration.updateOptions.code.additive, true) 24 | XCTAssertEqual(configuration.updateOptions.code.customFunction, "LocalizedStringResource") 25 | XCTAssertEqual(configuration.updateOptions.code.customLocalizableName, nil) 26 | XCTAssertEqual(configuration.updateOptions.code.defaultToKeys, false) 27 | XCTAssertEqual(configuration.updateOptions.code.unstripped, false) 28 | 29 | XCTAssertEqual(configuration.updateOptions.transform.codePaths, ["."]) 30 | XCTAssertEqual(configuration.updateOptions.transform.localizablePaths, ["."]) 31 | XCTAssertEqual(configuration.updateOptions.transform.transformer, .foundation) 32 | XCTAssertEqual(configuration.updateOptions.transform.typeName, "BartyCrouch") 33 | XCTAssertEqual(configuration.updateOptions.transform.translateMethodName, "translate") 34 | XCTAssertEqual(configuration.updateOptions.transform.customLocalizableName, nil) 35 | 36 | XCTAssertEqual(configuration.updateOptions.normalize.paths, ["."]) 37 | XCTAssertEqual(configuration.updateOptions.normalize.sourceLocale, "en") 38 | XCTAssertEqual(configuration.updateOptions.normalize.harmonizeWithSource, true) 39 | XCTAssertEqual(configuration.updateOptions.normalize.sortByKeys, true) 40 | 41 | XCTAssertNil(configuration.updateOptions.translate) 42 | 43 | XCTAssertEqual(configuration.lintOptions.paths, ["."]) 44 | XCTAssertEqual(configuration.lintOptions.duplicateKeys, true) 45 | XCTAssertEqual(configuration.lintOptions.emptyValues, true) 46 | } 47 | catch { 48 | XCTFail(error.localizedDescription) 49 | } 50 | } 51 | 52 | func testConfigurationMakeMostNonDefault() { 53 | let toml: Toml = try! Toml( 54 | withString: """ 55 | [update] 56 | tasks = ["interfaces", "transform", "normalize"] 57 | 58 | [update.interfaces] 59 | paths = ["Sources/ViewA", "Sources/ViewB"] 60 | defaultToBase = true 61 | ignoreEmptyStrings = true 62 | unstripped = true 63 | 64 | [update.code] 65 | codePaths = ["Sources"] 66 | localizablePaths = ["Sources/SupportingFiles"] 67 | defaultToKeys = true 68 | additive = false 69 | customFunction = "MyOwnLocalizedString" 70 | customLocalizableName = "MyOwnLocalizable" 71 | unstripped = true 72 | 73 | [update.transform] 74 | codePaths = ["Sources"] 75 | localizablePaths = ["Sources/SupportingFiles"] 76 | transformer = "swiftgenStructured" 77 | supportedLanguageEnumPath = "Sources/SupportingFiles" 78 | typeName = "BC" 79 | translateMethodName = "t" 80 | customLocalizableName = "MyOwnLocalizable" 81 | 82 | [update.normalize] 83 | paths = ["Sources"] 84 | sourceLocale = "de" 85 | harmonizeWithSource = false 86 | sortByKeys = false 87 | 88 | [update.translate] 89 | paths = ["Sources"] 90 | api = "bing" 91 | id = "bingId" 92 | secret = "bingSecret" 93 | sourceLocale = "de" 94 | 95 | [lint] 96 | paths = ["Sources"] 97 | duplicateKeys = false 98 | emptyValues = false 99 | 100 | """ 101 | ) 102 | 103 | do { 104 | let configuration: Configuration = try Configuration.make(toml: toml) 105 | 106 | XCTAssertEqual(configuration.updateOptions.tasks, [.interfaces, .transform, .normalize]) 107 | 108 | XCTAssertEqual(configuration.updateOptions.interfaces.paths, ["Sources/ViewA", "Sources/ViewB"]) 109 | XCTAssertEqual(configuration.updateOptions.interfaces.defaultToBase, true) 110 | XCTAssertEqual(configuration.updateOptions.interfaces.ignoreEmptyStrings, true) 111 | XCTAssertEqual(configuration.updateOptions.interfaces.unstripped, true) 112 | 113 | XCTAssertEqual(configuration.updateOptions.code.codePaths, ["Sources"]) 114 | XCTAssertEqual(configuration.updateOptions.code.localizablePaths, ["Sources/SupportingFiles"]) 115 | XCTAssertEqual(configuration.updateOptions.code.additive, false) 116 | XCTAssertEqual(configuration.updateOptions.code.customFunction, "MyOwnLocalizedString") 117 | XCTAssertEqual(configuration.updateOptions.code.customLocalizableName, "MyOwnLocalizable") 118 | XCTAssertEqual(configuration.updateOptions.code.defaultToKeys, true) 119 | XCTAssertEqual(configuration.updateOptions.code.unstripped, true) 120 | XCTAssertEqual(configuration.updateOptions.code.overrideComments, false) 121 | 122 | XCTAssertEqual(configuration.updateOptions.transform.codePaths, ["Sources"]) 123 | XCTAssertEqual(configuration.updateOptions.transform.localizablePaths, ["Sources/SupportingFiles"]) 124 | XCTAssertEqual(configuration.updateOptions.transform.transformer, .swiftgenStructured) 125 | XCTAssertEqual(configuration.updateOptions.transform.supportedLanguageEnumPath, "Sources/SupportingFiles") 126 | XCTAssertEqual(configuration.updateOptions.transform.typeName, "BC") 127 | XCTAssertEqual(configuration.updateOptions.transform.translateMethodName, "t") 128 | XCTAssertEqual(configuration.updateOptions.transform.customLocalizableName, "MyOwnLocalizable") 129 | 130 | XCTAssertEqual(configuration.updateOptions.normalize.paths, ["Sources"]) 131 | XCTAssertEqual(configuration.updateOptions.normalize.sourceLocale, "de") 132 | XCTAssertEqual(configuration.updateOptions.normalize.harmonizeWithSource, false) 133 | XCTAssertEqual(configuration.updateOptions.normalize.sortByKeys, false) 134 | 135 | XCTAssertEqual(configuration.updateOptions.translate!.paths, ["Sources"]) 136 | XCTAssertEqual(configuration.updateOptions.translate!.secret, Secret.microsoftTranslator(secret: "bingSecret")) 137 | XCTAssertEqual(configuration.updateOptions.translate!.sourceLocale, "de") 138 | 139 | XCTAssertEqual(configuration.lintOptions.paths, ["Sources"]) 140 | XCTAssertEqual(configuration.lintOptions.duplicateKeys, false) 141 | XCTAssertEqual(configuration.lintOptions.emptyValues, false) 142 | } 143 | catch { 144 | XCTFail(error.localizedDescription) 145 | } 146 | } 147 | 148 | func testConfigurationTomlContents() { 149 | let tomlContents: String = """ 150 | [update] 151 | tasks = ["interfaces", "code", "transform"] 152 | 153 | [update.interfaces] 154 | paths = ["Sources"] 155 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 156 | defaultToBase = true 157 | ignoreEmptyStrings = true 158 | unstripped = true 159 | ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] 160 | 161 | [update.code] 162 | codePaths = ["Sources"] 163 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 164 | localizablePaths = ["Sources/SupportingFiles"] 165 | defaultToKeys = true 166 | additive = false 167 | customFunction = "MyOwnLocalizedString" 168 | customLocalizableName = "MyOwnLocalizable" 169 | unstripped = true 170 | plistArguments = true 171 | ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"] 172 | overrideComments = false 173 | 174 | [update.transform] 175 | codePaths = ["."] 176 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 177 | localizablePaths = ["."] 178 | transformer = "foundation" 179 | supportedLanguageEnumPath = "." 180 | typeName = "BartyCrouch" 181 | translateMethodName = "translate" 182 | separateWithEmptyLine = true 183 | 184 | [update.translate] 185 | paths = ["Sources"] 186 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 187 | secret = "bingSecret" 188 | sourceLocale = "de" 189 | separateWithEmptyLine = true 190 | 191 | [update.normalize] 192 | paths = ["Sources"] 193 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 194 | sourceLocale = "de" 195 | harmonizeWithSource = false 196 | sortByKeys = false 197 | separateWithEmptyLine = true 198 | 199 | [lint] 200 | paths = ["Sources"] 201 | subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"] 202 | duplicateKeys = false 203 | emptyValues = false 204 | 205 | """ 206 | let toml: Toml = try! Toml(withString: tomlContents) 207 | let configuration: Configuration = try! Configuration.make(toml: toml) 208 | 209 | XCTAssertNoDifference(tomlContents, configuration.tomlContents()) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/BartyCrouchKit/FileHandling/TranslateTransformer.swift: -------------------------------------------------------------------------------- 1 | import BartyCrouchUtility 2 | import Foundation 3 | import HandySwift 4 | import SwiftSyntax 5 | 6 | class TranslateTransformer: SyntaxRewriter { 7 | let transformer: Transformer 8 | let typeName: String 9 | let translateMethodName: String 10 | let caseToLangCode: [String: String] 11 | 12 | var translateEntries: [CodeFileHandler.TranslateEntry] = [] 13 | 14 | init( 15 | transformer: Transformer, 16 | typeName: String, 17 | translateMethodName: String, 18 | caseToLangCode: [String: String] 19 | ) { 20 | self.transformer = transformer 21 | self.typeName = typeName 22 | self.translateMethodName = translateMethodName 23 | self.caseToLangCode = caseToLangCode 24 | } 25 | 26 | // swiftlint:disable:next function_body_length cyclomatic_complexity 27 | override func visit(_ functionCallExpression: FunctionCallExprSyntax) -> ExprSyntax { 28 | let functionCallExpressionMap = Array(functionCallExpression.children.makeIterator().lazy.prefix(3)) 29 | 30 | guard 31 | let memberAccessExpression = functionCallExpressionMap[0].as(MemberAccessExprSyntax.self), 32 | let memberAccessExpressionBase = memberAccessExpression.base, 33 | memberAccessExpressionBase.description.stripped() == typeName, 34 | memberAccessExpression.name.text == translateMethodName 35 | else { 36 | return super.visit(functionCallExpression) 37 | } 38 | 39 | guard let functionCallArgumentList = functionCallExpressionMap[2].as(TupleExprElementListSyntax.self) else { 40 | return super.visit(functionCallExpression) 41 | } 42 | 43 | let functionCallArgumentListMap = Array(functionCallArgumentList.children.makeIterator().lazy.prefix(3)) 44 | 45 | guard 46 | let keyFunctionCallArgument = functionCallArgumentListMap[0].as(TupleExprElementSyntax.self), 47 | let keyStringLiteralExpression = keyFunctionCallArgument.expression.as(StringLiteralExprSyntax.self), 48 | keyFunctionCallArgument.label?.text == "key", 49 | let translationsFunctionCallArgument = functionCallArgumentListMap[1].as(TupleExprElementSyntax.self), 50 | translationsFunctionCallArgument.label?.text == "translations" 51 | else { 52 | return super.visit(functionCallExpression) 53 | } 54 | 55 | let translationsFunctionCallArgumentMap = Array( 56 | translationsFunctionCallArgument.children.makeIterator().lazy.prefix(3) 57 | ) 58 | 59 | guard 60 | let translationsDictionaryExpression = translationsFunctionCallArgumentMap[2].as(DictionaryExprSyntax.self) 61 | else { 62 | return super.visit(functionCallExpression) 63 | } 64 | 65 | let leadingWhitespace = String( 66 | memberAccessExpressionBase.description.prefix(memberAccessExpressionBase.description.count - typeName.count) 67 | ) 68 | let key = keyStringLiteralExpression.text 69 | 70 | guard !key.isEmpty else { 71 | print("Found empty key in translate entry '\(functionCallExpression)'.", level: .warning) 72 | return ExprSyntax(functionCallExpression) 73 | } 74 | 75 | var translations: [CodeFileHandler.TranslationElement] = [] 76 | 77 | let translationsDictionaryExpressionMap = Array( 78 | translationsDictionaryExpression.children.makeIterator().lazy.prefix(3) 79 | ) 80 | 81 | if let translationsDictionaryElementList = translationsDictionaryExpressionMap[1] 82 | .as(DictionaryElementListSyntax.self) 83 | { 84 | for dictionaryElement in translationsDictionaryElementList { 85 | guard let langCase = dictionaryElement.keyExpression.description.components(separatedBy: ".").last?.stripped() 86 | else { 87 | print("LangeCase was not an enum case literal: '\(dictionaryElement.keyExpression)'") 88 | return ExprSyntax(functionCallExpression) 89 | } 90 | 91 | guard let translationLiteralExpression = dictionaryElement.valueExpression.as(StringLiteralExprSyntax.self) 92 | else { 93 | print( 94 | "Translation for langCase '\(langCase)' was not a String literal: '\(dictionaryElement.valueExpression)'" 95 | ) 96 | return ExprSyntax(functionCallExpression) 97 | } 98 | 99 | let translation = translationLiteralExpression.text 100 | 101 | guard !translation.isEmpty else { 102 | print("Translation for langCase '\(langCase)' was empty.", level: .warning) 103 | continue 104 | } 105 | 106 | guard let langCode = caseToLangCode[langCase] else { 107 | print("Could not find a langCode for langCase '\(langCase)' when transforming translation.", level: .warning) 108 | continue 109 | } 110 | 111 | translations.append((langCode: langCode, translation: translation)) 112 | } 113 | } 114 | 115 | var comment: String? 116 | 117 | if functionCallArgumentListMap.count > 2, 118 | let commentFunctionCallArgument = functionCallArgumentListMap[2].as(TupleExprElementSyntax.self), 119 | commentFunctionCallArgument.label?.text == "comment", 120 | let commentStringLiteralExpression = commentFunctionCallArgument.expression.as(StringLiteralExprSyntax.self) 121 | { 122 | comment = commentStringLiteralExpression.text 123 | } 124 | 125 | let translateEntry: CodeFileHandler.TranslateEntry = (key: key, translations: translations, comment: comment) 126 | translateEntries.append(translateEntry) 127 | 128 | print("Found translate entry with key '\(key)' and \(translations.count) translations.", level: .info) 129 | 130 | let transformedExpression: ExprSyntax = { 131 | switch transformer { 132 | case .foundation: 133 | return buildFoundationExpression(key: key, comment: comment, leadingWhitespace: leadingWhitespace) 134 | 135 | case .swiftgenStructured: 136 | return buildSwiftgenStructuredExpression(key: key, leadingWhitespace: leadingWhitespace) 137 | } 138 | }() 139 | 140 | print("Transformed '\(functionCallExpression)' to '\(transformedExpression)'.", level: .info) 141 | 142 | return transformedExpression 143 | } 144 | 145 | private func buildSwiftgenStructuredExpression(key: String, leadingWhitespace: String) -> ExprSyntax { 146 | // e.g. the key could be something like 'ONBOARDING.FIRST_PAGE.HEADER_TITLE' or 'onboarding.first-page.header-title' 147 | let keywordSeparators = CharacterSet(charactersIn: ".") 148 | let casingSeparators = CharacterSet(charactersIn: "-_") 149 | 150 | // e.g. ["ONBOARDING", "FIRST_PAGE", "HEADER_TITLE"] 151 | let keywords: [String] = key.components(separatedBy: keywordSeparators) 152 | 153 | // e.g. [["ONBOARDING"], ["FIRST", "PAGE"], ["HEADER", "TITLE"]] 154 | let keywordsCasingComponents: [[String]] = keywords.map { $0.components(separatedBy: casingSeparators) } 155 | 156 | // e.g. ["Onboarding", "FirstPage", "HeaderTitle"] 157 | var swiftgenKeyComponents: [String] = keywordsCasingComponents.map { $0.map { $0.capitalized }.joined() } 158 | 159 | // e.g. ["Onboarding", "FirstPage", "headerTitle"] 160 | let lastKeyComponentIndex: Int = swiftgenKeyComponents.endIndex - 1 161 | swiftgenKeyComponents[lastKeyComponentIndex] = swiftgenKeyComponents[lastKeyComponentIndex] 162 | .firstCharacterLowercased() 163 | 164 | // e.g. ["L10n", "Onboarding", "FirstPage", "headerTitle"] 165 | swiftgenKeyComponents.insert("\(leadingWhitespace)L10n", at: 0) 166 | 167 | return buildMemberAccessExpression(components: swiftgenKeyComponents) 168 | } 169 | 170 | private func buildMemberAccessExpression(components: [String]) -> ExprSyntax { 171 | let identifierToken = SyntaxFactory.makeIdentifier(components.last!) 172 | guard components.count > 1 else { 173 | return ExprSyntax(SyntaxFactory.makeIdentifierExpr(identifier: identifierToken, declNameArguments: nil)) 174 | } 175 | return ExprSyntax( 176 | SyntaxFactory.makeMemberAccessExpr( 177 | base: buildMemberAccessExpression(components: Array(components.dropLast())), 178 | dot: SyntaxFactory.makePeriodToken(), 179 | name: identifierToken, 180 | declNameArguments: nil 181 | ) 182 | ) 183 | } 184 | 185 | private func buildFoundationExpression(key: String, comment: String?, leadingWhitespace: String) -> ExprSyntax { 186 | let keyArgument = SyntaxFactory.makeTupleExprElement( 187 | label: nil, 188 | colon: nil, 189 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(key)), 190 | trailingComma: SyntaxFactory.makeCommaToken(leadingTrivia: .zero, trailingTrivia: .spaces(1)) 191 | ) 192 | 193 | let commentArgument = SyntaxFactory.makeTupleExprElement( 194 | label: SyntaxFactory.makeIdentifier("comment"), 195 | colon: SyntaxFactory.makeColonToken(leadingTrivia: .zero, trailingTrivia: .spaces(1)), 196 | expression: ExprSyntax(SyntaxFactory.makeStringLiteralExpr(comment ?? "")), 197 | trailingComma: nil 198 | ) 199 | 200 | return ExprSyntax( 201 | SyntaxFactory.makeFunctionCallExpr( 202 | calledExpression: ExprSyntax( 203 | SyntaxFactory.makeIdentifierExpr( 204 | identifier: SyntaxFactory.makeIdentifier("\(leadingWhitespace)NSLocalizedString"), 205 | declNameArguments: nil 206 | ) 207 | ), 208 | leftParen: SyntaxFactory.makeLeftParenToken(), 209 | argumentList: SyntaxFactory.makeTupleExprElementList([keyArgument, commentArgument]), 210 | rightParen: SyntaxFactory.makeRightParenToken(), 211 | trailingClosure: nil, 212 | additionalTrailingClosures: nil 213 | ) 214 | ) 215 | } 216 | } 217 | 218 | extension StringLiteralExprSyntax { 219 | var text: String { 220 | let description: String = self.description 221 | guard description.count > 2 else { return "" } 222 | 223 | let textRange = 224 | description.index(description.startIndex, offsetBy: 1).. $1.message.normalized }, stable: true).enumerated() 144 | { 145 | XCTAssertEqual(printOutput.message, expectedMessages[index]) 146 | XCTAssertEqual(String(printOutput.file!.suffix(from: "/private".endIndex)), expectedPaths[index]) 147 | XCTAssertEqual(printOutput.level, .info) 148 | } 149 | 150 | // TODO: check if files were actually changed correctly 151 | } 152 | 153 | func testTranslateTaskHandlerWithDefaultConfig() { 154 | XCTAssertThrowsError(try TranslateOptions.make(toml: Toml())) 155 | } 156 | 157 | func testTranslateTaskHandlerWithConfiguredSecret() { 158 | let microsoftSubscriptionKey = "" // TODO: load from environment variable 159 | guard !microsoftSubscriptionKey.isEmpty else { return } 160 | 161 | let translateOptions = TranslateOptions( 162 | paths: ["."], 163 | subpathsToIgnore: [], 164 | secret: .microsoftTranslator(secret: microsoftSubscriptionKey), 165 | sourceLocale: "en", 166 | separateWithEmptyLine: true 167 | ) 168 | TranslateTaskHandler(options: translateOptions).perform() 169 | 170 | let expectedMessages: [String] = [ 171 | "Successfully translated 6 values in 2 files.", 172 | "Value for key \'Existing Empty Value Key\' in source translations is empty.", 173 | "Value for key \'Existing Empty Value Key\' in source translations is empty.", 174 | "Successfully translated 2 values in 2 files.", 175 | ] 176 | 177 | let expectedPaths: [String] = [ 178 | DemoTests.testDemoDirectoryUrl.appendingPathComponent("Demo/en.lproj/Main.strings").path, 179 | DemoTests.testDemoDirectoryUrl.appendingPathComponent("Demo/en.lproj/Localizable.strings").path, 180 | DemoTests.testDemoDirectoryUrl.appendingPathComponent("Demo/en.lproj/Localizable.strings").path, 181 | DemoTests.testDemoDirectoryUrl.appendingPathComponent("Demo/en.lproj/Localizable.strings").path, 182 | ] 183 | 184 | let expectedLevels: [PrintLevel] = [.success, .warning, .warning, .success] 185 | let expectedLines: [Int?] = [nil, 15, 15, nil] 186 | 187 | for (index, printOutput) in TestHelper.shared.printOutputs.enumerated() { 188 | XCTAssertEqual(printOutput.message, expectedMessages[index]) 189 | XCTAssertEqual(String(printOutput.file!.suffix(from: "/private".endIndex)), expectedPaths[index]) 190 | XCTAssertEqual(printOutput.level, expectedLevels[index]) 191 | XCTAssertEqual(printOutput.line, expectedLines[index]) 192 | } 193 | 194 | // TODO: check if files were actually changed correctly 195 | } 196 | 197 | func testTransformTaskHandlerWithFoundationTransformer() { 198 | TransformTaskHandler(options: try! TransformOptions.make(toml: Toml())).perform() 199 | 200 | XCTAssertEqual(TestHelper.shared.printOutputs.count, 6) 201 | 202 | XCTAssertEqual( 203 | TestHelper.shared.printOutputs[0].message, 204 | "Found translate entry with key 'onboarding.first-page.header-title' and 2 translations." 205 | ) 206 | XCTAssertEqual( 207 | TestHelper.shared.printOutputs[1].message, 208 | """ 209 | Transformed 'BartyCrouch.translate(key: "onboarding.first-page.header-title", translations: [.english: "Page Title", .german: "Seitentitel"])' to 'NSLocalizedString("onboarding.first-page.header-title", comment: "")'. 210 | """ 211 | ) 212 | 213 | XCTAssertEqual( 214 | TestHelper.shared.printOutputs[2].message, 215 | "Found translate entry with key 'onboarding.first-page.line' and 0 translations." 216 | ) 217 | XCTAssertEqual( 218 | TestHelper.shared.printOutputs[3].message, 219 | """ 220 | Transformed 'BartyCrouch.translate(key: "onboarding.first-page.line", translations: [:], comment: "Line Comment")' to 'NSLocalizedString("onboarding.first-page.line", comment: "Line Comment")'. 221 | """ 222 | ) 223 | 224 | XCTAssertEqual( 225 | TestHelper.shared.printOutputs[4].message, 226 | "Found translate entry with key 'ShortKey' and 1 translations." 227 | ) 228 | XCTAssertEqual( 229 | TestHelper.shared.printOutputs[5].message, 230 | """ 231 | Transformed ' 232 | 233 | BartyCrouch 234 | .translate( 235 | key : "ShortKey", 236 | translations : [ 237 | BartyCrouch.SupportedLanguage.english : 238 | "Some Translation" 239 | ] 240 | )' to ' 241 | 242 | NSLocalizedString("ShortKey", comment: "")'. 243 | """ 244 | ) 245 | 246 | // TODO: check if files were actually changed correctly 247 | } 248 | 249 | func testTransformTaskHandlerWithSwiftgenStructuredTransformer() { 250 | let transformOptions = TransformOptions( 251 | codePaths: ["."], 252 | subpathsToIgnore: [], 253 | localizablePaths: ["."], 254 | transformer: .swiftgenStructured, 255 | supportedLanguageEnumPath: ".", 256 | typeName: "BartyCrouch", 257 | translateMethodName: "translate", 258 | customLocalizableName: nil, 259 | separateWithEmptyLine: true 260 | ) 261 | 262 | TransformTaskHandler(options: transformOptions).perform() 263 | 264 | XCTAssertEqual(TestHelper.shared.printOutputs.count, 6) 265 | 266 | XCTAssertEqual( 267 | TestHelper.shared.printOutputs[0].message, 268 | "Found translate entry with key 'onboarding.first-page.header-title' and 2 translations." 269 | ) 270 | XCTAssertEqual( 271 | TestHelper.shared.printOutputs[1].message, 272 | """ 273 | Transformed 'BartyCrouch.translate(key: "onboarding.first-page.header-title", translations: [.english: "Page Title", .german: "Seitentitel"])' to 'L10n.Onboarding.FirstPage.headerTitle'. 274 | """ 275 | ) 276 | 277 | XCTAssertEqual( 278 | TestHelper.shared.printOutputs[2].message, 279 | "Found translate entry with key 'onboarding.first-page.line' and 0 translations." 280 | ) 281 | XCTAssertEqual( 282 | TestHelper.shared.printOutputs[3].message, 283 | """ 284 | Transformed 'BartyCrouch.translate(key: "onboarding.first-page.line", translations: [:], comment: "Line Comment")' to 'L10n.Onboarding.FirstPage.line'. 285 | """ 286 | ) 287 | 288 | XCTAssertEqual( 289 | TestHelper.shared.printOutputs[4].message, 290 | "Found translate entry with key 'ShortKey' and 1 translations." 291 | ) 292 | XCTAssertEqual( 293 | TestHelper.shared.printOutputs[5].message, 294 | """ 295 | Transformed ' 296 | 297 | BartyCrouch 298 | .translate( 299 | key : "ShortKey", 300 | translations : [ 301 | BartyCrouch.SupportedLanguage.english : 302 | "Some Translation" 303 | ] 304 | )' to ' 305 | 306 | L10n.shortkey'. 307 | """ 308 | ) 309 | 310 | // TODO: check if files were actually changed correctly 311 | } 312 | } 313 | --------------------------------------------------------------------------------