mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:54:55 +08:00
feat(api): proxy Translation Memory over bat.sock
This commit is contained in:
+174
-16
@@ -307,26 +307,28 @@ type TranslationTaskListParams struct {
|
||||
// Pointer numeric fields preserve explicit zeroes so Rust can reject invalid
|
||||
// dashboard input instead of receiving omitted defaults.
|
||||
type TranslationWorkerRunParams struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
FixturePath string `json:"fixture_path,omitempty"`
|
||||
Concurrency *uint64 `json:"concurrency,omitempty"`
|
||||
MaxAttempts *uint64 `json:"max_attempts,omitempty"`
|
||||
LeaseSeconds *uint64 `json:"lease_seconds,omitempty"`
|
||||
RetryBackoffSeconds *uint64 `json:"retry_backoff_seconds,omitempty"`
|
||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||
WorkerID string `json:"worker_id,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
FixturePath string `json:"fixture_path,omitempty"`
|
||||
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||
Concurrency *uint64 `json:"concurrency,omitempty"`
|
||||
MaxAttempts *uint64 `json:"max_attempts,omitempty"`
|
||||
LeaseSeconds *uint64 `json:"lease_seconds,omitempty"`
|
||||
RetryBackoffSeconds *uint64 `json:"retry_backoff_seconds,omitempty"`
|
||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||
WorkerID string `json:"worker_id,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationWorkerConfig mirrors the accepted worker config returned by Rust.
|
||||
type TranslationWorkerConfig struct {
|
||||
Provider string `json:"provider"`
|
||||
FixturePath string `json:"fixture_path,omitempty"`
|
||||
Concurrency uint64 `json:"concurrency"`
|
||||
MaxAttempts uint64 `json:"max_attempts"`
|
||||
LeaseSeconds uint64 `json:"lease_seconds"`
|
||||
RetryBackoffSeconds uint64 `json:"retry_backoff_seconds"`
|
||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||
WorkerID string `json:"worker_id"`
|
||||
Provider string `json:"provider"`
|
||||
FixturePath string `json:"fixture_path,omitempty"`
|
||||
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||
Concurrency uint64 `json:"concurrency"`
|
||||
MaxAttempts uint64 `json:"max_attempts"`
|
||||
LeaseSeconds uint64 `json:"lease_seconds"`
|
||||
RetryBackoffSeconds uint64 `json:"retry_backoff_seconds"`
|
||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||
WorkerID string `json:"worker_id"`
|
||||
}
|
||||
|
||||
// TranslationWorkerRunResult is returned when translation.worker.run is queued.
|
||||
@@ -336,6 +338,144 @@ type TranslationWorkerRunResult struct {
|
||||
Worker TranslationWorkerConfig `json:"worker"`
|
||||
}
|
||||
|
||||
// TranslationMemoryContext is the stable TextUnit context sent to Rust.
|
||||
type TranslationMemoryContext map[string]string
|
||||
|
||||
// TranslationMemorySourceKind identifies who supplied the translation.
|
||||
type TranslationMemorySourceKind string
|
||||
|
||||
const (
|
||||
TranslationMemorySourceProvider TranslationMemorySourceKind = "provider"
|
||||
TranslationMemorySourceManual TranslationMemorySourceKind = "manual"
|
||||
TranslationMemorySourceImported TranslationMemorySourceKind = "imported"
|
||||
)
|
||||
|
||||
// TranslationMemoryTrustStatus is the Rust-owned review state.
|
||||
type TranslationMemoryTrustStatus string
|
||||
|
||||
const (
|
||||
TranslationMemoryStatusCandidate TranslationMemoryTrustStatus = "candidate"
|
||||
TranslationMemoryStatusTrusted TranslationMemoryTrustStatus = "trusted"
|
||||
TranslationMemoryStatusSuperseded TranslationMemoryTrustStatus = "superseded"
|
||||
TranslationMemoryStatusRejected TranslationMemoryTrustStatus = "rejected"
|
||||
)
|
||||
|
||||
// TranslationMemoryMatchKind describes why a record was returned.
|
||||
type TranslationMemoryMatchKind string
|
||||
|
||||
const (
|
||||
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
|
||||
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
|
||||
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
|
||||
)
|
||||
|
||||
// TranslationMemorySummaryParams selects an optional Rust-owned TM database.
|
||||
type TranslationMemorySummaryParams struct {
|
||||
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemoryQueryParams queries Rust-owned TM records by raw source and
|
||||
// optional complete TextUnit context.
|
||||
type TranslationMemoryQueryParams struct {
|
||||
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||
SourceText string `json:"source_text"`
|
||||
SourceContext TranslationMemoryContext `json:"source_context,omitempty"`
|
||||
Limit *uint64 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemoryConfirmParams explicitly promotes one candidate record.
|
||||
type TranslationMemoryConfirmParams struct {
|
||||
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||
RecordID string `json:"record_id"`
|
||||
Reviewer string `json:"reviewer"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemorySummary mirrors translation.memory.summary data.
|
||||
type TranslationMemorySummary struct {
|
||||
SchemaVersion uint64 `json:"schema_version"`
|
||||
RecordCount uint64 `json:"record_count"`
|
||||
TrustedCount uint64 `json:"trusted_count"`
|
||||
CandidateCount uint64 `json:"candidate_count"`
|
||||
SupersededCount uint64 `json:"superseded_count"`
|
||||
RejectedCount uint64 `json:"rejected_count"`
|
||||
}
|
||||
|
||||
// TranslationMemorySummaryReport distinguishes a missing database from an
|
||||
// available database with an empty summary.
|
||||
type TranslationMemorySummaryReport struct {
|
||||
Available bool `json:"available"`
|
||||
Path string `json:"path"`
|
||||
SchemaVersion *uint64 `json:"schema_version,omitempty"`
|
||||
Summary *TranslationMemorySummary `json:"summary,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemorySourceTrace mirrors the Rust TextUnit/provider provenance.
|
||||
type TranslationMemorySourceTrace struct {
|
||||
OfficialReleaseID string `json:"official_release_id"`
|
||||
UnitID *string `json:"unit_id,omitempty"`
|
||||
TaskID *string `json:"task_id,omitempty"`
|
||||
Destination *string `json:"destination,omitempty"`
|
||||
ArchiveEntry *string `json:"archive_entry,omitempty"`
|
||||
SerializedFile *string `json:"serialized_file,omitempty"`
|
||||
PathID *int64 `json:"path_id,omitempty"`
|
||||
ClassID *int32 `json:"class_id,omitempty"`
|
||||
FieldPath *string `json:"field_path,omitempty"`
|
||||
Format *string `json:"format,omitempty"`
|
||||
AssetName *string `json:"asset_name,omitempty"`
|
||||
TextSourceKind *string `json:"text_source_kind,omitempty"`
|
||||
SourceURL *string `json:"source_url,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemoryEntry mirrors a persisted Rust TM record.
|
||||
type TranslationMemoryEntry struct {
|
||||
RecordID string `json:"record_id"`
|
||||
SourceText string `json:"source_text"`
|
||||
SourceHash string `json:"source_hash"`
|
||||
NormalizedSourceText string `json:"normalized_source_text"`
|
||||
SourceContext TranslationMemoryContext `json:"source_context,omitempty"`
|
||||
SourceContextHash string `json:"source_context_hash"`
|
||||
TranslatedText string `json:"translated_text"`
|
||||
TranslationSourceKind TranslationMemorySourceKind `json:"translation_source_kind"`
|
||||
TrustStatus TranslationMemoryTrustStatus `json:"trust_status"`
|
||||
OfficialReleaseID string `json:"official_release_id"`
|
||||
SourceTrace TranslationMemorySourceTrace `json:"source_trace"`
|
||||
Provider *string `json:"provider,omitempty"`
|
||||
ProviderRunID *string `json:"provider_run_id,omitempty"`
|
||||
CreatedUnixSeconds uint64 `json:"created_unix_seconds"`
|
||||
UpdatedUnixSeconds uint64 `json:"updated_unix_seconds"`
|
||||
TrustedUnixSeconds *uint64 `json:"trusted_unix_seconds,omitempty"`
|
||||
TrustedBy *string `json:"trusted_by,omitempty"`
|
||||
TrustedReason *string `json:"trusted_reason,omitempty"`
|
||||
SupersedesRecordID *string `json:"supersedes_record_id,omitempty"`
|
||||
SupersededByRecordID *string `json:"superseded_by_record_id,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemoryMatch is one Rust-selected match and its reuse decision.
|
||||
type TranslationMemoryMatch struct {
|
||||
Entry TranslationMemoryEntry `json:"entry"`
|
||||
MatchKind TranslationMemoryMatchKind `json:"match_kind"`
|
||||
CanAutoReuse bool `json:"can_auto_reuse"`
|
||||
}
|
||||
|
||||
// TranslationMemoryQueryReport mirrors translation.memory.query data.
|
||||
type TranslationMemoryQueryReport struct {
|
||||
Available bool `json:"available"`
|
||||
Path string `json:"path"`
|
||||
SourceText string `json:"source_text"`
|
||||
SourceContext TranslationMemoryContext `json:"source_context,omitempty"`
|
||||
Matches []TranslationMemoryMatch `json:"matches"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// TranslationMemoryConfirmReport mirrors translation.memory.confirm data.
|
||||
type TranslationMemoryConfirmReport struct {
|
||||
Available bool `json:"available"`
|
||||
Path string `json:"path"`
|
||||
Entry TranslationMemoryEntry `json:"entry"`
|
||||
}
|
||||
|
||||
// LocalizedPublishParams selects the source of one localized release
|
||||
// publication. TranslationFile and FromWorker are mutually exclusive.
|
||||
type LocalizedPublishParams struct {
|
||||
@@ -593,6 +733,24 @@ func (c *Client) TranslationWorkerRun(ctx context.Context, params TranslationWor
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TranslationMemorySummary(ctx context.Context, params TranslationMemorySummaryParams) (*TranslationMemorySummaryReport, error) {
|
||||
var out TranslationMemorySummaryReport
|
||||
_, err := c.Call(ctx, "translation.memory.summary", params, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TranslationMemoryQuery(ctx context.Context, params TranslationMemoryQueryParams) (*TranslationMemoryQueryReport, error) {
|
||||
var out TranslationMemoryQueryReport
|
||||
_, err := c.Call(ctx, "translation.memory.query", params, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) TranslationMemoryConfirm(ctx context.Context, params TranslationMemoryConfirmParams) (*TranslationMemoryConfirmReport, error) {
|
||||
var out TranslationMemoryConfirmReport
|
||||
_, err := c.Call(ctx, "translation.memory.confirm", params, &out)
|
||||
return &out, err
|
||||
}
|
||||
|
||||
func (c *Client) LocalizedPublish(ctx context.Context, params LocalizedPublishParams) (json.RawMessage, error) {
|
||||
return c.rawData(ctx, "localized.publish", params)
|
||||
}
|
||||
|
||||
@@ -489,6 +489,7 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
||||
}
|
||||
if params.Provider != "mock" ||
|
||||
params.FixturePath != "/tmp/mock-provider.json" ||
|
||||
params.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
||||
params.Concurrency == nil || *params.Concurrency != concurrency ||
|
||||
params.MaxAttempts == nil || *params.MaxAttempts != maxAttempts ||
|
||||
params.LeaseSeconds == nil || *params.LeaseSeconds != leaseSeconds ||
|
||||
@@ -506,14 +507,15 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
||||
"task_id": "task-worker-1",
|
||||
"kind": "translation.worker.run",
|
||||
"worker": map[string]any{
|
||||
"provider": "mock",
|
||||
"fixture_path": "/tmp/mock-provider.json",
|
||||
"concurrency": concurrency,
|
||||
"max_attempts": maxAttempts,
|
||||
"lease_seconds": leaseSeconds,
|
||||
"retry_backoff_seconds": retryBackoff,
|
||||
"max_tasks": maxTasks,
|
||||
"worker_id": "dashboard-worker",
|
||||
"provider": "mock",
|
||||
"fixture_path": "/tmp/mock-provider.json",
|
||||
"translation_memory_path": "/tmp/translation-memory.sqlite",
|
||||
"concurrency": concurrency,
|
||||
"max_attempts": maxAttempts,
|
||||
"lease_seconds": leaseSeconds,
|
||||
"retry_backoff_seconds": retryBackoff,
|
||||
"max_tasks": maxTasks,
|
||||
"worker_id": "dashboard-worker",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -521,14 +523,15 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
result, err := client.TranslationWorkerRun(context.Background(), TranslationWorkerRunParams{
|
||||
Provider: "mock",
|
||||
FixturePath: "/tmp/mock-provider.json",
|
||||
Concurrency: &concurrency,
|
||||
MaxAttempts: &maxAttempts,
|
||||
LeaseSeconds: &leaseSeconds,
|
||||
RetryBackoffSeconds: &retryBackoff,
|
||||
MaxTasks: &maxTasks,
|
||||
WorkerID: "dashboard-worker",
|
||||
Provider: "mock",
|
||||
FixturePath: "/tmp/mock-provider.json",
|
||||
TranslationMemoryPath: "/tmp/translation-memory.sqlite",
|
||||
Concurrency: &concurrency,
|
||||
MaxAttempts: &maxAttempts,
|
||||
LeaseSeconds: &leaseSeconds,
|
||||
RetryBackoffSeconds: &retryBackoff,
|
||||
MaxTasks: &maxTasks,
|
||||
WorkerID: "dashboard-worker",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("TranslationWorkerRun error: %v", err)
|
||||
@@ -536,12 +539,192 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
||||
if result.TaskID != "task-worker-1" ||
|
||||
result.Kind != "translation.worker.run" ||
|
||||
result.Worker.Concurrency != concurrency ||
|
||||
result.Worker.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
||||
result.Worker.RetryBackoffSeconds != retryBackoff ||
|
||||
result.Worker.MaxTasks == nil || *result.Worker.MaxTasks != maxTasks {
|
||||
t.Fatalf("unexpected result: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslationMemoryTypedContract(t *testing.T) {
|
||||
limit := uint64(25)
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
switch req.Method {
|
||||
case "translation.memory.summary":
|
||||
var params TranslationMemorySummaryParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode summary params: %v", err)
|
||||
}
|
||||
if params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" {
|
||||
t.Fatalf("summary params=%#v", params)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true, Status: "ok", RequestID: "req-tm-summary",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||
"schema_version": 1,
|
||||
"summary": map[string]any{
|
||||
"schema_version": 1,
|
||||
"record_count": 3,
|
||||
"trusted_count": 1,
|
||||
"candidate_count": 1,
|
||||
"superseded_count": 1,
|
||||
"rejected_count": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case "translation.memory.query":
|
||||
var params TranslationMemoryQueryParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode query params: %v", err)
|
||||
}
|
||||
if params.SourceText != "Hello" ||
|
||||
params.SourceContext["destination"] != "Bundle/dialogue.bundle" ||
|
||||
params.Limit == nil || *params.Limit != limit {
|
||||
t.Fatalf("query params=%#v", params)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true, Status: "ok", RequestID: "req-tm-query",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||
"source_text": "Hello",
|
||||
"source_context": map[string]any{
|
||||
"destination": "Bundle/dialogue.bundle",
|
||||
"field_path": "Dialog.Message",
|
||||
},
|
||||
"matches": []any{
|
||||
map[string]any{
|
||||
"entry": map[string]any{
|
||||
"record_id": "tm-record-1",
|
||||
"source_text": "Hello",
|
||||
"source_hash": "hash-source",
|
||||
"normalized_source_text": "hello",
|
||||
"source_context": map[string]any{
|
||||
"destination": "Bundle/dialogue.bundle",
|
||||
"field_path": "Dialog.Message",
|
||||
},
|
||||
"source_context_hash": "hash-context",
|
||||
"translated_text": "你好",
|
||||
"translation_source_kind": "provider",
|
||||
"trust_status": "trusted",
|
||||
"official_release_id": "release-1",
|
||||
"source_trace": map[string]any{
|
||||
"official_release_id": "release-1",
|
||||
"unit_id": "textunit-1",
|
||||
"task_id": "task-1",
|
||||
"destination": "Bundle/dialogue.bundle",
|
||||
"archive_entry": "dialogue.json",
|
||||
"serialized_file": "globalgamemanagers",
|
||||
"path_id": 42,
|
||||
"class_id": 114,
|
||||
"field_path": "Dialog.Message",
|
||||
"format": "json",
|
||||
"asset_name": "Dialogue",
|
||||
"text_source_kind": "text_asset",
|
||||
},
|
||||
"provider": "mock",
|
||||
"provider_run_id": "provider-run-1",
|
||||
"created_unix_seconds": 100,
|
||||
"updated_unix_seconds": 200,
|
||||
"trusted_unix_seconds": 200,
|
||||
"trusted_by": "reviewer",
|
||||
"trusted_reason": "reviewed",
|
||||
},
|
||||
"match_kind": "strong_exact",
|
||||
"can_auto_reuse": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
case "translation.memory.confirm":
|
||||
var params TranslationMemoryConfirmParams
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode confirm params: %v", err)
|
||||
}
|
||||
if params.RecordID != "tm-record-1" ||
|
||||
params.Reviewer != "reviewer" ||
|
||||
params.Reason != "reviewed" ||
|
||||
params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" {
|
||||
t.Fatalf("confirm params=%#v", params)
|
||||
}
|
||||
return testResponse{
|
||||
Result: testEnvelope{
|
||||
OK: true, Status: "ok", RequestID: "req-tm-confirm",
|
||||
Data: map[string]any{
|
||||
"available": true,
|
||||
"path": "/var/lib/bat/translation-memory.sqlite",
|
||||
"entry": map[string]any{
|
||||
"record_id": "tm-record-1",
|
||||
"translation_source_kind": "provider",
|
||||
"trust_status": "trusted",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected method %q", req.Method)
|
||||
return testResponse{}
|
||||
}
|
||||
})
|
||||
|
||||
summary, err := client.TranslationMemorySummary(context.Background(), TranslationMemorySummaryParams{
|
||||
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("TranslationMemorySummary error: %v", err)
|
||||
}
|
||||
if !summary.Available || summary.Summary == nil ||
|
||||
summary.Summary.TrustedCount != 1 || summary.Summary.CandidateCount != 1 {
|
||||
t.Fatalf("summary=%#v", summary)
|
||||
}
|
||||
|
||||
query, err := client.TranslationMemoryQuery(context.Background(), TranslationMemoryQueryParams{
|
||||
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
||||
SourceText: "Hello",
|
||||
SourceContext: TranslationMemoryContext{
|
||||
"destination": "Bundle/dialogue.bundle",
|
||||
"field_path": "Dialog.Message",
|
||||
},
|
||||
Limit: &limit,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("TranslationMemoryQuery error: %v", err)
|
||||
}
|
||||
if len(query.Matches) != 1 ||
|
||||
query.Matches[0].MatchKind != "strong_exact" ||
|
||||
!query.Matches[0].CanAutoReuse ||
|
||||
query.Matches[0].Entry.TrustStatus != "trusted" ||
|
||||
query.Matches[0].Entry.TranslationSourceKind != "provider" ||
|
||||
query.Matches[0].Entry.SourceTrace.UnitID == nil ||
|
||||
*query.Matches[0].Entry.SourceTrace.UnitID != "textunit-1" ||
|
||||
query.Matches[0].Entry.SourceTrace.PathID == nil ||
|
||||
*query.Matches[0].Entry.SourceTrace.PathID != 42 ||
|
||||
query.Matches[0].Entry.TrustedBy == nil ||
|
||||
*query.Matches[0].Entry.TrustedBy != "reviewer" {
|
||||
t.Fatalf("query=%#v", query)
|
||||
}
|
||||
|
||||
confirmed, err := client.TranslationMemoryConfirm(context.Background(), TranslationMemoryConfirmParams{
|
||||
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
|
||||
RecordID: "tm-record-1",
|
||||
Reviewer: "reviewer",
|
||||
Reason: "reviewed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("TranslationMemoryConfirm error: %v", err)
|
||||
}
|
||||
if !confirmed.Available || confirmed.Entry.TrustStatus != "trusted" ||
|
||||
confirmed.Entry.RecordID != "tm-record-1" {
|
||||
t.Fatalf("confirmed=%#v", confirmed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||
if req.Method != "translation.proofread" {
|
||||
|
||||
Reference in New Issue
Block a user