├── 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 |
--------------------------------------------------------------------------------