Skip to content

Add a review command to evaluate the quality of translations #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

# Secrets
.secret*
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
reference = "container:Tests/SwiftStringCatalogTests/SwiftStringCatalog.xctestplan"
default = "YES">
</TestPlanReference>
<TestPlanReference
reference = "container:Tests/SwiftStringCatalog copy.xctestplan">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Tests/SwiftStringCatalogTests/SwiftStringCatalog.xctestplan"
default = "YES">
</TestPlanReference>
<TestPlanReference
reference = "container:Tests/TranslatorServicesTests/TranslatorServicesTests.xctestplan">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
Expand All @@ -67,6 +75,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TranslatorServicesTests"
BuildableName = "TranslatorServicesTests"
BlueprintName = "TranslatorServicesTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
36 changes: 20 additions & 16 deletions .swiftpm/xcode/xcshareddata/xcschemes/swift-translate.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,22 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SwiftStringCatalogTests"
BuildableName = "SwiftStringCatalogTests"
BlueprintName = "SwiftStringCatalogTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:Tests/SwiftStringCatalogTests/SwiftStringCatalog.xctestplan"
default = "YES">
</TestPlanReference>
<TestPlanReference
reference = "container:Tests/TranslatorServicesTests/TranslatorServicesTests.xctestplan">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
Expand All @@ -53,6 +47,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TranslatorServicesTests"
BuildableName = "TranslatorServicesTests"
BlueprintName = "TranslatorServicesTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/MacPaw/OpenAI.git",
"state" : {
"revision" : "ac5892fd0de8d283362ddc30f8e9f1a0eaba8cc0",
"version" : "0.2.5"
"revision" : "fd13a41e987004d14f1793c570721953a2767b03",
"version" : "0.2.9"
}
},
{
Expand Down
16 changes: 9 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ let package = Package(
.package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMajor(from: "4.0.0")),
],
targets: [

// Main Plugin

.plugin(
name: "SwiftTranslate",
capability: .command(
Expand All @@ -48,9 +46,7 @@ let package = Package(
.target(name: "swift-translate")
]
),

// Libraries

.executableTarget(
name: "swift-translate",
dependencies: [
Expand All @@ -61,18 +57,24 @@ let package = Package(
],
path: "Sources/SwiftTranslate"
),

.target(
name: "SwiftStringCatalog"
),

// Tests

.testTarget(
name: "SwiftStringCatalogTests",
dependencies: ["SwiftStringCatalog"],
exclude: ["SwiftStringCatalog.xctestplan"],
resources: [.process("Resources")]
),
.testTarget(
name: "TranslatorServicesTests",
dependencies: ["swift-translate"],
exclude: [
"TranslatorServicesTests.xctestplan",
"Resources/TheGoodTheBadAndTheUgly.xcstrings"
],
resources: [.copy("Resources")]
)
]
)
31 changes: 30 additions & 1 deletion Sources/SwiftStringCatalog/Bootstrap/StringCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,36 @@ public final class StringCatalog {
self.sourceLanguage = sourceLanguage
self.targetLanguages = targetLanguages
}


// MARK: Actions

@discardableResult
public func markNeedsReview(_ languagesOption: LanguagesOption) -> Int {
let languagesToMark: Set<Language>
switch languagesOption {
case .allCommon:
languagesToMark = Set(Language.allCommon)
case .allInStringCatalog:
languagesToMark = Set(targetLanguages)
case .languages(let languages):
languagesToMark = Set(languages)
}

var markedStringsCount = 0
for group in localizableStringGroups.values {
for string in group.strings {
guard languagesToMark.contains(string.targetLanguage) else {
continue
}
if string.state == .translated {
string.setNeedsReview()
markedStringsCount += 1
}
}
}
return markedStringsCount
}

// MARK: Loading

private func detectedTargetLanguages(in catalog: _StringCatalog) -> Set<Language> {
Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftStringCatalog/Models/Language.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import Foundation

public enum LanguagesOption {
case allCommon, allInStringCatalog, languages([Language])
}

public struct Language: Codable, Equatable, Hashable, RawRepresentable {

Expand Down
10 changes: 9 additions & 1 deletion Sources/SwiftStringCatalog/Models/LocalizableString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ public final class LocalizableString {
translatedValue = translation
state = .translated
}


public func setTranslated() {
state = .translated
}

public func setNeedsReview() {
state = .needsReview
}

// MARK: Utility

func convertKindToSubstitution(argNum: Int, formatSpecifier: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ public struct LocalizableStringGroup {
self.extractionState = extractionState
self.strings = strings
}

// MARK: Getters

public func string(for language: Language) -> LocalizableString? {
strings.first(where: { $0.targetLanguage == language })
}

}
130 changes: 130 additions & 0 deletions Sources/SwiftTranslate/Bootstrap/ActionCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// Copyright © 2024 Hidden Spectrum, LLC.
//

import ArgumentParser
import Foundation
import Rainbow
import SwiftStringCatalog


struct ActionCoordinator {

// MARK: Internal

enum Action {
case translateFileOrDirectory(URL, Set<Language>?, overwrite: Bool)
case translateText(String, Set<Language>)
case reviewFileOrDirectory(URL, Set<Language>?, overwrite: Bool)
}

let action: Action
let translator: TranslationService
let skipConfirmation: Bool
let verbose: Bool

// MARK: Lifecycle

init(action: Action, translator: TranslationService, skipConfirmation: Bool, verbose: Bool) {
self.action = action
self.translator = translator
self.skipConfirmation = skipConfirmation
self.verbose = verbose
}

// MARK: Main

func process() async throws {
let startDate = Date()
var keysCount: Int = 1
let logPrefix: String

switch action {
case .translateFileOrDirectory(let fileOrDirectoryUrl, let targetLanguages, let overwrite):
logPrefix = "Translated"
keysCount = try await translateFiles(at: fileOrDirectoryUrl, to: targetLanguages, overwrite: overwrite)
case .translateText(let string, let targetLanguages):
logPrefix = "Translated"
try await translate(string, to: targetLanguages)
case .reviewFileOrDirectory(let url, let languages, let overwrite):
logPrefix = "Reviewed"
keysCount = try await reviewFiles(at: url, languages: languages, overwrite: overwrite)

if keysCount == 0 {
Log.info(newline: .both, "Found no keys marked as NEEDS REVIEW")
}
}
if keysCount > 0 {
Log.success(newline: .after, startDate: startDate, "\(logPrefix) \(keysCount) key(s)")
}
}

// MARK: Translate Text

private func translate(_ string: String, to targetLanguages: Set<Language>) async throws {
Log.info(newline: .before, "Translating `", string, "`:")
for language in targetLanguages {
let translation = try await translator.translate(string, to: language, comment: nil)
Log.structured(
.init(width: 8, language.rawValue + ":"),
.init(translation)
)
}
}

// MARK: Translate Files

private func translateFiles(at url: URL, to targetLanguages: Set<Language>?, overwrite: Bool) async throws -> Int {
let fileFinder = TranslatableFileFinder(fileOrDirectoryURL: url, type: .stringCatalog)
let translatableFiles = try fileFinder.findTranslatableFiles()

if translatableFiles.isEmpty {
return 0
}

let fileTranslator = StringCatalogTranslator(
with: translator,
targetLanguages: targetLanguages,
overwrite: overwrite,
skipConfirmations: skipConfirmation,
verbose: verbose
)

var translatedKeys = 0
for file in translatableFiles {
translatedKeys += try await fileTranslator.translate(fileAt: file)
}

return translatedKeys
}

// MARK: Evaluate translations

private func reviewFiles(
at url: URL,
languages: Set<Language>?,
overwrite: Bool
) async throws -> Int {
let fileFinder = TranslatableFileFinder(fileOrDirectoryURL: url, type: .stringCatalog)
let files = try fileFinder.findTranslatableFiles()

guard let translator = translator as? EvaluationService else {
throw SwiftTranslateError.evaluationIsNotSupported
}

let evaluator = StringCatalogEvaluator(
with: translator,
languages: languages,
overwrite: overwrite,
skipConfirmations: skipConfirmation,
verbose: verbose
)

var numberOfVerifiedStrings = 0
for file in files {
numberOfVerifiedStrings += try await evaluator.process(fileAt: file)
}
return numberOfVerifiedStrings
}

}
23 changes: 23 additions & 0 deletions Sources/SwiftTranslate/Bootstrap/CatalogTranslationOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// CatalogTranslationOptions.swift
//
//
// Created by Jonas Bromö on 2024-05-17.
//

import ArgumentParser

struct CatalogTranslationOptions: ParsableArguments {

@Flag(
name: [.customLong("overwrite")],
help: "Overwrite string catalog files instead of creating a new file"
)
var overwriteExisting: Bool = false

@Argument(
parsing: .remaining,
help: "File or directory containing string catalogs to translate"
)
var fileOrDirectory: [String] = []
}
Loading