Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Scripts/translation-tool/src/CLI.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

enum Constant {
    static let concurrentRequestLimit = 12
    static let projectIdentifier = "4b899d72e"
    static let repositoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}

@main
struct CLI {

    // MARK: - Entrypoint

    static func main() async throws {
        var remainingArgs = CommandLine.arguments.dropFirst()
        while let arg = remainingArgs.popFirst() {
            switch arg {
            case "upload-metadata":
                try await loadCLI().uploadFiles(metadataFiles)
            case "upload-resources":
                try await loadCLI().uploadFiles(resourceFiles)
            case "download-metadata":
                try MetadataFile.checkForUnusedLocalizations(in: Constant.repositoryURL)
                try await loadCLI().downloadFiles(metadataFiles)
            case "download-resources":
                try ResourceFile.checkForUnusedLocalizations(in: Constant.repositoryURL)
                try await loadCLI().downloadFiles(resourceFiles)
            case "genstrings-pluralaware":
                guard let temporaryDirectoryPath = remainingArgs.popFirst() else {
                    print("Missing temporary directory path")
                    exit(1)
                }
                try Genstrings.filterPluralAware(
                    resourceFile: pluralAwareFile,
                    repositoryURL: Constant.repositoryURL,
                    temporaryDirectoryURL: URL(fileURLWithPath: temporaryDirectoryPath),
                )
            default:
                print("Unknown action: \(arg)")
            }
        }
    }

    private static var loadedCLI: CLI?
    static func loadCLI() throws -> CLI {
        if let result = loadedCLI {
            return result
        }
        guard let (userIdentifier, userSecret) = try loadUserParameters() else {
            showIntructionsForUserParameters()
        }
        let client = Smartling(
            projectIdentifier: Constant.projectIdentifier,
            userIdentifier: userIdentifier,
            userSecret: userSecret,
        )
        let result = CLI(repositoryURL: Constant.repositoryURL, client: client)
        loadedCLI = result
        return result
    }

    // MARK: - Upload & Download

    private static let metadataFiles: [MetadataFile] = [
        MetadataFile(filename: "release_notes.txt"),
        MetadataFile(filename: "description.txt"),
    ]

    private static let pluralAwareFile = ResourceFile(filename: "PluralAware.stringsdict")

    private static let resourceFiles: [ResourceFile] = [
        ResourceFile(filename: "InfoPlist.strings"),
        ResourceFile(filename: "Localizable.strings"),
        pluralAwareFile,
    ]

    var repositoryURL: URL
    var client: Smartling

    private func uploadFiles(_ files: [TranslatableFile]) async throws {
        try await withLimitedThrowingTaskGroup(limit: Constant.concurrentRequestLimit) { taskGroup in
            for translatableFile in files {
                try await taskGroup.addTask {
                    try await client.uploadSourceFile(
                        at: repositoryURL.appendingPathComponent(translatableFile.relativeSourcePath),
                    )
                    print("Uploaded \(translatableFile.filename)")
                }
            }
        }
    }

    private func downloadFiles(_ files: [TranslatableFile]) async throws {
        // Each of these kicks off a bunch of downloads in parallel, so it's fine to do these serially.
        for translatableFile in files {
            try await translatableFile.downloadAllTranslations(to: repositoryURL, using: client)
        }
    }

    // MARK: - Config

    private static func loadUserParameters() throws -> (String, String)? {
        let fileURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".smartling/auth")
        let fileContent: String
        do {
            fileContent = try String(contentsOf: fileURL).trimmingCharacters(in: .whitespacesAndNewlines)
        } catch CocoaError.fileReadNoSuchFile {
            return nil
        }
        let components = fileContent.split(separator: "\n")
        guard components.count == 2 else {
            return nil
        }
        return (String(components[0]), String(components[1]))
    }

    private static func showIntructionsForUserParameters() -> Never {
        print("")
        print("Couldn't load user identifier/user secret from ~/.smartling/auth.")
        print("")
        print("That file should contain two lines: (1) user identifier & (2) user secret.")
        print("You can create a token using Smartling's web interface.")
        print("")
        exit(1)
    }
}