mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
feat(api): proxy Translation Memory over bat.sock
This commit is contained in:
@@ -404,6 +404,56 @@ paths:
|
|||||||
description: Missing or invalid admin token.
|
description: Missing or invalid admin token.
|
||||||
"503":
|
"503":
|
||||||
description: Rust bat translation backend is unavailable.
|
description: Rust bat translation backend is unavailable.
|
||||||
|
/admin/translation/memory/summary:
|
||||||
|
get:
|
||||||
|
summary: Read Rust-owned Translation Memory summary
|
||||||
|
parameters:
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory availability and candidate/trusted counts.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
|
/admin/translation/memory/query:
|
||||||
|
get:
|
||||||
|
summary: Query Rust-owned Translation Memory records
|
||||||
|
parameters:
|
||||||
|
- name: source_text
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: source_context
|
||||||
|
in: query
|
||||||
|
description: JSON object whose values are strings.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 1
|
||||||
|
maximum: 1000
|
||||||
|
default: 100
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory matches with reuse decision and provenance.
|
||||||
|
"400":
|
||||||
|
description: Missing source text or invalid context/limit.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
/admin/translation/status:
|
/admin/translation/status:
|
||||||
get:
|
get:
|
||||||
summary: Read Rust-owned localized release status
|
summary: Read Rust-owned localized release status
|
||||||
@@ -423,7 +473,7 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, localized-publish, localized-rollback]
|
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, localized-publish, localized-rollback]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
content:
|
content:
|
||||||
@@ -514,6 +564,14 @@ paths:
|
|||||||
minimum: 1
|
minimum: 1
|
||||||
worker_id:
|
worker_id:
|
||||||
type: string
|
type: string
|
||||||
|
translation_memory_path:
|
||||||
|
type: string
|
||||||
|
record_id:
|
||||||
|
type: string
|
||||||
|
reviewer:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
translation_file:
|
translation_file:
|
||||||
type: string
|
type: string
|
||||||
from_worker:
|
from_worker:
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/parse/errors",
|
"/admin/parse/errors",
|
||||||
"/admin/translation/tasks",
|
"/admin/translation/tasks",
|
||||||
"/admin/translation/handoff",
|
"/admin/translation/handoff",
|
||||||
|
"/admin/translation/memory/summary",
|
||||||
|
"/admin/translation/memory/query",
|
||||||
"/admin/translation/status",
|
"/admin/translation/status",
|
||||||
},
|
},
|
||||||
Controls: []string{
|
Controls: []string{
|
||||||
@@ -72,6 +74,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/control/translation-task-update",
|
"/admin/control/translation-task-update",
|
||||||
"/admin/control/translation-worker-run",
|
"/admin/control/translation-worker-run",
|
||||||
"/admin/control/translation-proofread",
|
"/admin/control/translation-proofread",
|
||||||
|
"/admin/control/translation-memory-confirm",
|
||||||
"/admin/control/localized-publish",
|
"/admin/control/localized-publish",
|
||||||
"/admin/control/localized-rollback",
|
"/admin/control/localized-rollback",
|
||||||
},
|
},
|
||||||
@@ -117,6 +120,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleAdminTranslationProofread(w, r)
|
s.handleAdminTranslationProofread(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if action == "translation-memory-confirm" {
|
||||||
|
s.handleAdminTranslationMemoryConfirm(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
if action == "localized-publish" {
|
if action == "localized-publish" {
|
||||||
s.handleAdminLocalizedPublish(w, r)
|
s.handleAdminLocalizedPublish(w, r)
|
||||||
return
|
return
|
||||||
@@ -274,6 +281,34 @@ func (s *Server) handleAdminTranslationProofread(w http.ResponseWriter, r *http.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationMemoryConfirm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
||||||
|
if !ok || backend == nil {
|
||||||
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var params backendrpc.TranslationMemoryConfirmParams
|
||||||
|
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validateTranslationMemoryConfirmParams(params); err != nil {
|
||||||
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_params", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationMemoryConfirm(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-memory-confirm", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||||
|
Service: "bat-api",
|
||||||
|
Action: "translation-memory-confirm",
|
||||||
|
RPCMethod: "translation.memory.confirm",
|
||||||
|
Status: "accepted",
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleAdminLocalizedPublish(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminLocalizedPublish(w http.ResponseWriter, r *http.Request) {
|
||||||
backend, ok := s.backend.(LocalizedBackend)
|
backend, ok := s.backend.(LocalizedBackend)
|
||||||
if !ok || backend == nil {
|
if !ok || backend == nil {
|
||||||
@@ -656,6 +691,66 @@ func (s *Server) handleAdminTranslationHandoff(w http.ResponseWriter, r *http.Re
|
|||||||
writeNoStoreJSON(w, http.StatusOK, result)
|
writeNoStoreJSON(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationMemorySummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.requireAdminToken(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
||||||
|
if !ok || backend == nil {
|
||||||
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := backendrpc.TranslationMemorySummaryParams{
|
||||||
|
TranslationMemoryPath: firstTrimmedQuery(r.URL.Query(), "translation_memory_path", "tm_path"),
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationMemorySummary(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-memory-summary", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAdminTranslationMemoryQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
|
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.requireAdminToken(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backend, ok := s.backend.(TranslationMemoryBackend)
|
||||||
|
if !ok || backend == nil {
|
||||||
|
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params, err := translationMemoryQueryParams(r)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_query", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := backend.TranslationMemoryQuery(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
s.writeControlBackendError(w, "translation-memory-query", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeNoStoreJSON(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||||
@@ -747,6 +842,38 @@ func translationTaskListParams(r *http.Request) (backendrpc.TranslationTaskListP
|
|||||||
return params, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func translationMemoryQueryParams(r *http.Request) (backendrpc.TranslationMemoryQueryParams, error) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
sourceText := strings.TrimSpace(query.Get("source_text"))
|
||||||
|
if sourceText == "" {
|
||||||
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_text is required")
|
||||||
|
}
|
||||||
|
params := backendrpc.TranslationMemoryQueryParams{
|
||||||
|
TranslationMemoryPath: firstTrimmedQuery(query, "translation_memory_path", "tm_path"),
|
||||||
|
SourceText: sourceText,
|
||||||
|
}
|
||||||
|
if raw := firstTrimmedQuery(query, "source_context", "context"); raw != "" {
|
||||||
|
var context backendrpc.TranslationMemoryContext
|
||||||
|
if raw != "null" {
|
||||||
|
if err := json.Unmarshal([]byte(raw), &context); err != nil {
|
||||||
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_context must be a JSON object with string values")
|
||||||
|
}
|
||||||
|
if context == nil {
|
||||||
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("source_context must be a JSON object")
|
||||||
|
}
|
||||||
|
params.SourceContext = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw := strings.TrimSpace(query.Get("limit")); raw != "" {
|
||||||
|
limit, err := strconv.ParseUint(raw, 10, 64)
|
||||||
|
if err != nil || limit == 0 || limit > 1000 {
|
||||||
|
return backendrpc.TranslationMemoryQueryParams{}, errors.New("limit must be in 1..=1000")
|
||||||
|
}
|
||||||
|
params.Limit = &limit
|
||||||
|
}
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
params := backendrpc.TextUnitQueryParams{
|
params := backendrpc.TextUnitQueryParams{
|
||||||
@@ -938,6 +1065,13 @@ func validateTranslationWorkerRunParams(params backendrpc.TranslationWorkerRunPa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryConfirmParams) error {
|
||||||
|
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
||||||
|
return errors.New("Translation Memory confirm requires record_id and reviewer")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateLocalizedPublishParams(params backendrpc.LocalizedPublishParams) error {
|
func validateLocalizedPublishParams(params backendrpc.LocalizedPublishParams) error {
|
||||||
hasFile := strings.TrimSpace(params.TranslationFile) != ""
|
hasFile := strings.TrimSpace(params.TranslationFile) != ""
|
||||||
if hasFile == params.FromWorker {
|
if hasFile == params.FromWorker {
|
||||||
|
|||||||
+110
-8
@@ -629,13 +629,16 @@ func (f *fakeBackend) TaskCancel(ctx context.Context, taskID string) (*backendrp
|
|||||||
|
|
||||||
type controlBackend struct {
|
type controlBackend struct {
|
||||||
*fakeBackend
|
*fakeBackend
|
||||||
calls []string
|
calls []string
|
||||||
parseTextUnitQueries []backendrpc.TextUnitQueryParams
|
parseTextUnitQueries []backendrpc.TextUnitQueryParams
|
||||||
parseErrorQueries []backendrpc.TextUnitQueryParams
|
parseErrorQueries []backendrpc.TextUnitQueryParams
|
||||||
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
|
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
|
||||||
translationTaskListParams []backendrpc.TranslationTaskListParams
|
translationTaskListParams []backendrpc.TranslationTaskListParams
|
||||||
localizedPublishParams []backendrpc.LocalizedPublishParams
|
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
|
||||||
localizedRollbackParams []backendrpc.LocalizedRollbackParams
|
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
|
||||||
|
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
|
||||||
|
localizedPublishParams []backendrpc.LocalizedPublishParams
|
||||||
|
localizedRollbackParams []backendrpc.LocalizedRollbackParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
|
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
|
||||||
@@ -777,6 +780,51 @@ func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMess
|
|||||||
return json.RawMessage(`{"translation_workflow_status":"manual_proofreading"}`), nil
|
return json.RawMessage(`{"translation_workflow_status":"manual_proofreading"}`), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
|
||||||
|
b.calls = append(b.calls, "translation.memory.summary")
|
||||||
|
b.translationMemorySummaryParams = append(b.translationMemorySummaryParams, params)
|
||||||
|
schemaVersion := uint64(1)
|
||||||
|
return &backendrpc.TranslationMemorySummaryReport{
|
||||||
|
Available: true,
|
||||||
|
Path: params.TranslationMemoryPath,
|
||||||
|
SchemaVersion: &schemaVersion,
|
||||||
|
Summary: &backendrpc.TranslationMemorySummary{
|
||||||
|
SchemaVersion: schemaVersion,
|
||||||
|
RecordCount: 2,
|
||||||
|
TrustedCount: 1,
|
||||||
|
CandidateCount: 1,
|
||||||
|
SupersededCount: 0,
|
||||||
|
RejectedCount: 0,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error) {
|
||||||
|
b.calls = append(b.calls, "translation.memory.query")
|
||||||
|
b.translationMemoryQueryParams = append(b.translationMemoryQueryParams, params)
|
||||||
|
return &backendrpc.TranslationMemoryQueryReport{
|
||||||
|
Available: true,
|
||||||
|
Path: params.TranslationMemoryPath,
|
||||||
|
SourceText: params.SourceText,
|
||||||
|
SourceContext: params.SourceContext,
|
||||||
|
Matches: []backendrpc.TranslationMemoryMatch{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *controlBackend) TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error) {
|
||||||
|
b.calls = append(b.calls, "translation.memory.confirm")
|
||||||
|
b.translationMemoryConfirmParams = append(b.translationMemoryConfirmParams, params)
|
||||||
|
return &backendrpc.TranslationMemoryConfirmReport{
|
||||||
|
Available: true,
|
||||||
|
Path: params.TranslationMemoryPath,
|
||||||
|
Entry: backendrpc.TranslationMemoryEntry{
|
||||||
|
RecordID: params.RecordID,
|
||||||
|
TranslationSourceKind: "provider",
|
||||||
|
TrustStatus: "trusted",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *controlBackend) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
func (b *controlBackend) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
||||||
b.calls = append(b.calls, "localized.status")
|
b.calls = append(b.calls, "localized.status")
|
||||||
return json.RawMessage(`{"localized_release_status":"localized","status_code":"localized.published"}`), nil
|
return json.RawMessage(`{"localized_release_status":"localized","status_code":"localized.published"}`), nil
|
||||||
@@ -960,6 +1008,42 @@ func TestAdminTranslationQueryEndpointsProxyAuthenticatedRequests(t *testing.T)
|
|||||||
t.Fatalf("calls=%v", backend.calls)
|
t.Fatalf("calls=%v", backend.calls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/memory/summary?translation_memory_path=%2Fvar%2Flib%2Fbat%2Ftranslation-memory.sqlite", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("TM summary status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(recorder.Body.String(), `"trusted_count":1`) ||
|
||||||
|
len(backend.translationMemorySummaryParams) != 1 ||
|
||||||
|
backend.translationMemorySummaryParams[0].TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" {
|
||||||
|
t.Fatalf("TM summary body=%s params=%#v", recorder.Body.String(), backend.translationMemorySummaryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/memory/query?source_text=Hello&source_context=%7B%22destination%22%3A%22Bundle%2Fdialogue.bundle%22%7D&limit=25", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("TM query status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if len(backend.translationMemoryQueryParams) != 1 ||
|
||||||
|
backend.translationMemoryQueryParams[0].SourceText != "Hello" ||
|
||||||
|
backend.translationMemoryQueryParams[0].SourceContext["destination"] != "Bundle/dialogue.bundle" ||
|
||||||
|
backend.translationMemoryQueryParams[0].Limit == nil ||
|
||||||
|
*backend.translationMemoryQueryParams[0].Limit != 25 {
|
||||||
|
t.Fatalf("TM query body=%s params=%#v", recorder.Body.String(), backend.translationMemoryQueryParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/memory/query", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("missing TM query source status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/tasks?limit=0", nil)
|
request = httptest.NewRequest(http.MethodGet, "/admin/translation/tasks?limit=0", nil)
|
||||||
request.Header.Set("Authorization", "Bearer translation-token")
|
request.Header.Set("Authorization", "Bearer translation-token")
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
@@ -1502,7 +1586,9 @@ func TestOpenAPIAndAdminReservedEndpoints(t *testing.T) {
|
|||||||
!strings.Contains(rr.Body.String(), "/admin/tasks") ||
|
!strings.Contains(rr.Body.String(), "/admin/tasks") ||
|
||||||
!strings.Contains(rr.Body.String(), "/admin/parse/text-units") ||
|
!strings.Contains(rr.Body.String(), "/admin/parse/text-units") ||
|
||||||
!strings.Contains(rr.Body.String(), "translation_results") ||
|
!strings.Contains(rr.Body.String(), "translation_results") ||
|
||||||
!strings.Contains(rr.Body.String(), "task-cancel") {
|
!strings.Contains(rr.Body.String(), "task-cancel") ||
|
||||||
|
!strings.Contains(rr.Body.String(), "/admin/translation/memory/query") ||
|
||||||
|
!strings.Contains(rr.Body.String(), "translation-memory-confirm") {
|
||||||
t.Fatalf("openapi missing dashboard/task admin routes")
|
t.Fatalf("openapi missing dashboard/task admin routes")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,6 +1610,8 @@ func TestOpenAPIAndAdminReservedEndpoints(t *testing.T) {
|
|||||||
links := strings.Join(admin.Links, "\n")
|
links := strings.Join(admin.Links, "\n")
|
||||||
if !strings.Contains(links, "/admin/translation/tasks") ||
|
if !strings.Contains(links, "/admin/translation/tasks") ||
|
||||||
!strings.Contains(links, "/admin/translation/handoff") ||
|
!strings.Contains(links, "/admin/translation/handoff") ||
|
||||||
|
!strings.Contains(links, "/admin/translation/memory/summary") ||
|
||||||
|
!strings.Contains(links, "/admin/translation/memory/query") ||
|
||||||
!strings.Contains(links, "/admin/dashboard/") ||
|
!strings.Contains(links, "/admin/dashboard/") ||
|
||||||
!strings.Contains(links, "/admin/parse/text-units") ||
|
!strings.Contains(links, "/admin/parse/text-units") ||
|
||||||
!strings.Contains(links, "/admin/tasks/logs") {
|
!strings.Contains(links, "/admin/tasks/logs") {
|
||||||
@@ -1729,6 +1817,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
{name: "translation task update", action: "translation-task-update", body: `{"task_id":"textunit/v-current/Scenario","status":"completed","provider":"manual","provider_run_id":"manual-run-1","translation_results":[{"unit_id":"direct:a#unit:0","source_text":"source","translated_text":"译文"}]}`, rpcMethod: "translation.task.update", call: "translation.task.update"},
|
{name: "translation task update", action: "translation-task-update", body: `{"task_id":"textunit/v-current/Scenario","status":"completed","provider":"manual","provider_run_id":"manual-run-1","translation_results":[{"unit_id":"direct:a#unit:0","source_text":"source","translated_text":"译文"}]}`, rpcMethod: "translation.task.update", call: "translation.task.update"},
|
||||||
{name: "translation worker run", action: "translation-worker-run", body: `{"provider":"mock","concurrency":8,"max_tasks":2,"retry_backoff_seconds":0,"worker_id":"dashboard-worker"}`, rpcMethod: "translation.worker.run", call: "translation.worker.run"},
|
{name: "translation worker run", action: "translation-worker-run", body: `{"provider":"mock","concurrency":8,"max_tasks":2,"retry_backoff_seconds":0,"worker_id":"dashboard-worker"}`, rpcMethod: "translation.worker.run", call: "translation.worker.run"},
|
||||||
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
|
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
|
||||||
|
{name: "translation memory confirm", action: "translation-memory-confirm", body: `{"record_id":"tm-record-1","reviewer":"reviewer","reason":"reviewed"}`, rpcMethod: "translation.memory.confirm", call: "translation.memory.confirm"},
|
||||||
{name: "localized publish", action: "localized-publish", body: `{"from_worker":true,"localized_release_id":"localized-1","force":true}`, rpcMethod: "localized.publish", call: "localized.publish"},
|
{name: "localized publish", action: "localized-publish", body: `{"from_worker":true,"localized_release_id":"localized-1","force":true}`, rpcMethod: "localized.publish", call: "localized.publish"},
|
||||||
{name: "localized rollback", action: "localized-rollback", body: `{"localized_release_id":"localized-1"}`, rpcMethod: "localized.rollback", call: "localized.rollback"},
|
{name: "localized rollback", action: "localized-rollback", body: `{"localized_release_id":"localized-1"}`, rpcMethod: "localized.rollback", call: "localized.rollback"},
|
||||||
}
|
}
|
||||||
@@ -1760,6 +1849,11 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
backend.translationTaskUpdates[0].TranslationResults[0].TranslatedText != "译文" {
|
backend.translationTaskUpdates[0].TranslationResults[0].TranslatedText != "译文" {
|
||||||
t.Fatalf("translation task updates=%#v", backend.translationTaskUpdates)
|
t.Fatalf("translation task updates=%#v", backend.translationTaskUpdates)
|
||||||
}
|
}
|
||||||
|
if len(backend.translationMemoryConfirmParams) != 1 ||
|
||||||
|
backend.translationMemoryConfirmParams[0].RecordID != "tm-record-1" ||
|
||||||
|
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
|
||||||
|
t.Fatalf("TM confirm params=%#v", backend.translationMemoryConfirmParams)
|
||||||
|
}
|
||||||
|
|
||||||
request := httptest.NewRequest(http.MethodPost, "/admin/control/translation-task-update", strings.NewReader(`{"task_id":""}`))
|
request := httptest.NewRequest(http.MethodPost, "/admin/control/translation-task-update", strings.NewReader(`{"task_id":""}`))
|
||||||
request.Header.Set("Authorization", "Bearer control-token")
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
@@ -1785,6 +1879,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
|||||||
t.Fatalf("invalid translation worker status=%d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("invalid translation worker status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request = httptest.NewRequest(http.MethodPost, "/admin/control/translation-memory-confirm", strings.NewReader(`{"record_id":""}`))
|
||||||
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
|
recorder = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("invalid TM confirm status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
request = httptest.NewRequest(http.MethodPost, "/admin/control/localized-publish", strings.NewReader(`{"from_worker":true,"translation_file":"/tmp/workbench.json"}`))
|
request = httptest.NewRequest(http.MethodPost, "/admin/control/localized-publish", strings.NewReader(`{"from_worker":true,"translation_file":"/tmp/workbench.json"}`))
|
||||||
request.Header.Set("Authorization", "Bearer control-token")
|
request.Header.Set("Authorization", "Bearer control-token")
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
|
|||||||
@@ -110,3 +110,91 @@ func TestRustContractFixturesPreserveGoMirror(t *testing.T) {
|
|||||||
t.Fatalf("legacy game_main_config field unexpectedly present: %s", snapshot.LegacyGameMainConfig)
|
t.Fatalf("legacy game_main_config field unexpectedly present: %s", snapshot.LegacyGameMainConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTranslationMemoryRustContractMirror(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"available": true,
|
||||||
|
"path": "${TM_PATH}",
|
||||||
|
"source_text": "${SOURCE_TEXT}",
|
||||||
|
"source_context": {
|
||||||
|
"destination": "${DESTINATION}",
|
||||||
|
"field_path": "${FIELD_PATH}"
|
||||||
|
},
|
||||||
|
"matches": [{
|
||||||
|
"entry": {
|
||||||
|
"record_id": "${RECORD_ID}",
|
||||||
|
"source_text": "${SOURCE_TEXT}",
|
||||||
|
"source_hash": "${SOURCE_HASH}",
|
||||||
|
"normalized_source_text": "${NORMALIZED_SOURCE_TEXT}",
|
||||||
|
"source_context": {
|
||||||
|
"destination": "${DESTINATION}",
|
||||||
|
"field_path": "${FIELD_PATH}"
|
||||||
|
},
|
||||||
|
"source_context_hash": "${SOURCE_CONTEXT_HASH}",
|
||||||
|
"translated_text": "${TRANSLATED_TEXT}",
|
||||||
|
"translation_source_kind": "provider",
|
||||||
|
"trust_status": "trusted",
|
||||||
|
"official_release_id": "${OFFICIAL_RELEASE_ID}",
|
||||||
|
"source_trace": {
|
||||||
|
"official_release_id": "${OFFICIAL_RELEASE_ID}",
|
||||||
|
"unit_id": "${UNIT_ID}",
|
||||||
|
"task_id": "${TASK_ID}",
|
||||||
|
"destination": "${DESTINATION}",
|
||||||
|
"archive_entry": "${ARCHIVE_ENTRY}",
|
||||||
|
"serialized_file": "${SERIALIZED_FILE}",
|
||||||
|
"path_id": 42,
|
||||||
|
"class_id": 114,
|
||||||
|
"field_path": "${FIELD_PATH}",
|
||||||
|
"format": "json",
|
||||||
|
"asset_name": "${ASSET_NAME}",
|
||||||
|
"text_source_kind": "text_asset"
|
||||||
|
},
|
||||||
|
"provider": "${PROVIDER}",
|
||||||
|
"provider_run_id": "${PROVIDER_RUN_ID}",
|
||||||
|
"created_unix_seconds": 100,
|
||||||
|
"updated_unix_seconds": 200,
|
||||||
|
"trusted_unix_seconds": 200,
|
||||||
|
"trusted_by": "${REVIEWER}",
|
||||||
|
"trusted_reason": "${TRUST_REASON}"
|
||||||
|
},
|
||||||
|
"match_kind": "strong_exact",
|
||||||
|
"can_auto_reuse": true
|
||||||
|
}]
|
||||||
|
}`)
|
||||||
|
if bytes.Contains(raw, []byte("/tmp/")) {
|
||||||
|
t.Fatal("TM mirror contains a local temporary path")
|
||||||
|
}
|
||||||
|
|
||||||
|
var report backendrpc.TranslationMemoryQueryReport
|
||||||
|
if err := json.Unmarshal(raw, &report); err != nil {
|
||||||
|
t.Fatalf("decode TM query mirror: %v", err)
|
||||||
|
}
|
||||||
|
if !report.Available || report.Path != "${TM_PATH}" ||
|
||||||
|
report.SourceText != "${SOURCE_TEXT}" ||
|
||||||
|
report.SourceContext["field_path"] != "${FIELD_PATH}" ||
|
||||||
|
len(report.Matches) != 1 {
|
||||||
|
t.Fatalf("TM report=%+v", report)
|
||||||
|
}
|
||||||
|
match := report.Matches[0]
|
||||||
|
if match.MatchKind != "strong_exact" || !match.CanAutoReuse ||
|
||||||
|
match.Entry.TrustStatus != "trusted" ||
|
||||||
|
match.Entry.TranslationSourceKind != "provider" ||
|
||||||
|
match.Entry.SourceTrace.PathID == nil || *match.Entry.SourceTrace.PathID != 42 ||
|
||||||
|
match.Entry.SourceTrace.ClassID == nil || *match.Entry.SourceTrace.ClassID != 114 ||
|
||||||
|
match.Entry.TrustedBy == nil || *match.Entry.TrustedBy != "${REVIEWER}" {
|
||||||
|
t.Fatalf("TM match=%+v", match)
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing backendrpc.TranslationMemorySummaryReport
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"available": false,
|
||||||
|
"path": "${TM_PATH}",
|
||||||
|
"reason": "database_missing"
|
||||||
|
}`), &missing); err != nil {
|
||||||
|
t.Fatalf("decode missing TM summary mirror: %v", err)
|
||||||
|
}
|
||||||
|
if missing.Available || missing.Summary != nil || missing.SchemaVersion != nil ||
|
||||||
|
missing.Reason != "database_missing" {
|
||||||
|
t.Fatalf("missing TM summary=%+v", missing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+59
-1
@@ -411,6 +411,56 @@ paths:
|
|||||||
description: Missing or invalid admin token.
|
description: Missing or invalid admin token.
|
||||||
"503":
|
"503":
|
||||||
description: Rust bat translation backend is unavailable.
|
description: Rust bat translation backend is unavailable.
|
||||||
|
/admin/translation/memory/summary:
|
||||||
|
get:
|
||||||
|
summary: Read Rust-owned Translation Memory summary
|
||||||
|
parameters:
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory availability and candidate/trusted counts.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
|
/admin/translation/memory/query:
|
||||||
|
get:
|
||||||
|
summary: Query Rust-owned Translation Memory records
|
||||||
|
parameters:
|
||||||
|
- name: source_text
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: source_context
|
||||||
|
in: query
|
||||||
|
description: JSON object whose values are strings.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 1
|
||||||
|
maximum: 1000
|
||||||
|
default: 100
|
||||||
|
- name: translation_memory_path
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Translation Memory matches with reuse decision and provenance.
|
||||||
|
"400":
|
||||||
|
description: Missing source text or invalid context/limit.
|
||||||
|
"401":
|
||||||
|
description: Missing or invalid admin token.
|
||||||
|
"503":
|
||||||
|
description: Rust bat Translation Memory backend is unavailable.
|
||||||
/admin/translation/status:
|
/admin/translation/status:
|
||||||
get:
|
get:
|
||||||
summary: Read Rust-owned localized release status
|
summary: Read Rust-owned localized release status
|
||||||
@@ -430,7 +480,7 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, localized-publish, localized-rollback]
|
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, localized-publish, localized-rollback]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
required: false
|
||||||
content:
|
content:
|
||||||
@@ -521,6 +571,14 @@ paths:
|
|||||||
minimum: 1
|
minimum: 1
|
||||||
worker_id:
|
worker_id:
|
||||||
type: string
|
type: string
|
||||||
|
translation_memory_path:
|
||||||
|
type: string
|
||||||
|
record_id:
|
||||||
|
type: string
|
||||||
|
reviewer:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
translation_file:
|
translation_file:
|
||||||
type: string
|
type: string
|
||||||
from_worker:
|
from_worker:
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ type TranslationBackend interface {
|
|||||||
TranslationProofread(ctx context.Context) (json.RawMessage, error)
|
TranslationProofread(ctx context.Context) (json.RawMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TranslationMemoryBackend exposes the Rust-owned Translation Memory query and
|
||||||
|
// explicit confirmation operations. Go forwards typed requests and responses
|
||||||
|
// but never opens or mutates the TM database itself.
|
||||||
|
type TranslationMemoryBackend interface {
|
||||||
|
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
|
||||||
|
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error)
|
||||||
|
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
|
||||||
|
}
|
||||||
|
|
||||||
// LocalizedBackend exposes localized release status and the explicit
|
// LocalizedBackend exposes localized release status and the explicit
|
||||||
// publish/rollback controls used by the authenticated dashboard.
|
// publish/rollback controls used by the authenticated dashboard.
|
||||||
type LocalizedBackend interface {
|
type LocalizedBackend interface {
|
||||||
@@ -182,6 +191,18 @@ func (r RPCClient) TranslationProofread(ctx context.Context) (json.RawMessage, e
|
|||||||
return r.Client.TranslationProofread(ctx)
|
return r.Client.TranslationProofread(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r RPCClient) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
|
||||||
|
return r.Client.TranslationMemorySummary(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RPCClient) TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error) {
|
||||||
|
return r.Client.TranslationMemoryQuery(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error) {
|
||||||
|
return r.Client.TranslationMemoryConfirm(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
func (r RPCClient) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
func (r RPCClient) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
||||||
return r.Client.LocalizedStatus(ctx)
|
return r.Client.LocalizedStatus(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("/admin/schedules", s.handleAdminSchedules)
|
mux.HandleFunc("/admin/schedules", s.handleAdminSchedules)
|
||||||
mux.HandleFunc("/admin/translation/tasks", s.handleAdminTranslationTasks)
|
mux.HandleFunc("/admin/translation/tasks", s.handleAdminTranslationTasks)
|
||||||
mux.HandleFunc("/admin/translation/handoff", s.handleAdminTranslationHandoff)
|
mux.HandleFunc("/admin/translation/handoff", s.handleAdminTranslationHandoff)
|
||||||
|
mux.HandleFunc("/admin/translation/memory/summary", s.handleAdminTranslationMemorySummary)
|
||||||
|
mux.HandleFunc("/admin/translation/memory/query", s.handleAdminTranslationMemoryQuery)
|
||||||
mux.HandleFunc("/admin/translation/status", s.handleAdminLocalizedStatus)
|
mux.HandleFunc("/admin/translation/status", s.handleAdminLocalizedStatus)
|
||||||
mux.HandleFunc("/admin/control/", s.handleAdminControl)
|
mux.HandleFunc("/admin/control/", s.handleAdminControl)
|
||||||
mux.HandleFunc("/admin/", s.handleAdminIndex)
|
mux.HandleFunc("/admin/", s.handleAdminIndex)
|
||||||
@@ -151,6 +153,8 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
"/admin/schedules",
|
"/admin/schedules",
|
||||||
"/admin/translation/tasks",
|
"/admin/translation/tasks",
|
||||||
"/admin/translation/handoff",
|
"/admin/translation/handoff",
|
||||||
|
"/admin/translation/memory/summary",
|
||||||
|
"/admin/translation/memory/query",
|
||||||
"/admin/translation/status",
|
"/admin/translation/status",
|
||||||
"/admin/control/{action}",
|
"/admin/control/{action}",
|
||||||
},
|
},
|
||||||
|
|||||||
+174
-16
@@ -307,26 +307,28 @@ type TranslationTaskListParams struct {
|
|||||||
// Pointer numeric fields preserve explicit zeroes so Rust can reject invalid
|
// Pointer numeric fields preserve explicit zeroes so Rust can reject invalid
|
||||||
// dashboard input instead of receiving omitted defaults.
|
// dashboard input instead of receiving omitted defaults.
|
||||||
type TranslationWorkerRunParams struct {
|
type TranslationWorkerRunParams struct {
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
FixturePath string `json:"fixture_path,omitempty"`
|
FixturePath string `json:"fixture_path,omitempty"`
|
||||||
Concurrency *uint64 `json:"concurrency,omitempty"`
|
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||||
MaxAttempts *uint64 `json:"max_attempts,omitempty"`
|
Concurrency *uint64 `json:"concurrency,omitempty"`
|
||||||
LeaseSeconds *uint64 `json:"lease_seconds,omitempty"`
|
MaxAttempts *uint64 `json:"max_attempts,omitempty"`
|
||||||
RetryBackoffSeconds *uint64 `json:"retry_backoff_seconds,omitempty"`
|
LeaseSeconds *uint64 `json:"lease_seconds,omitempty"`
|
||||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
RetryBackoffSeconds *uint64 `json:"retry_backoff_seconds,omitempty"`
|
||||||
WorkerID string `json:"worker_id,omitempty"`
|
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||||
|
WorkerID string `json:"worker_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationWorkerConfig mirrors the accepted worker config returned by Rust.
|
// TranslationWorkerConfig mirrors the accepted worker config returned by Rust.
|
||||||
type TranslationWorkerConfig struct {
|
type TranslationWorkerConfig struct {
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
FixturePath string `json:"fixture_path,omitempty"`
|
FixturePath string `json:"fixture_path,omitempty"`
|
||||||
Concurrency uint64 `json:"concurrency"`
|
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
|
||||||
MaxAttempts uint64 `json:"max_attempts"`
|
Concurrency uint64 `json:"concurrency"`
|
||||||
LeaseSeconds uint64 `json:"lease_seconds"`
|
MaxAttempts uint64 `json:"max_attempts"`
|
||||||
RetryBackoffSeconds uint64 `json:"retry_backoff_seconds"`
|
LeaseSeconds uint64 `json:"lease_seconds"`
|
||||||
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
RetryBackoffSeconds uint64 `json:"retry_backoff_seconds"`
|
||||||
WorkerID string `json:"worker_id"`
|
MaxTasks *uint64 `json:"max_tasks,omitempty"`
|
||||||
|
WorkerID string `json:"worker_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationWorkerRunResult is returned when translation.worker.run is queued.
|
// TranslationWorkerRunResult is returned when translation.worker.run is queued.
|
||||||
@@ -336,6 +338,144 @@ type TranslationWorkerRunResult struct {
|
|||||||
Worker TranslationWorkerConfig `json:"worker"`
|
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
|
// LocalizedPublishParams selects the source of one localized release
|
||||||
// publication. TranslationFile and FromWorker are mutually exclusive.
|
// publication. TranslationFile and FromWorker are mutually exclusive.
|
||||||
type LocalizedPublishParams struct {
|
type LocalizedPublishParams struct {
|
||||||
@@ -593,6 +733,24 @@ func (c *Client) TranslationWorkerRun(ctx context.Context, params TranslationWor
|
|||||||
return &out, err
|
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) {
|
func (c *Client) LocalizedPublish(ctx context.Context, params LocalizedPublishParams) (json.RawMessage, error) {
|
||||||
return c.rawData(ctx, "localized.publish", params)
|
return c.rawData(ctx, "localized.publish", params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -489,6 +489,7 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if params.Provider != "mock" ||
|
if params.Provider != "mock" ||
|
||||||
params.FixturePath != "/tmp/mock-provider.json" ||
|
params.FixturePath != "/tmp/mock-provider.json" ||
|
||||||
|
params.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
||||||
params.Concurrency == nil || *params.Concurrency != concurrency ||
|
params.Concurrency == nil || *params.Concurrency != concurrency ||
|
||||||
params.MaxAttempts == nil || *params.MaxAttempts != maxAttempts ||
|
params.MaxAttempts == nil || *params.MaxAttempts != maxAttempts ||
|
||||||
params.LeaseSeconds == nil || *params.LeaseSeconds != leaseSeconds ||
|
params.LeaseSeconds == nil || *params.LeaseSeconds != leaseSeconds ||
|
||||||
@@ -506,14 +507,15 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
|||||||
"task_id": "task-worker-1",
|
"task_id": "task-worker-1",
|
||||||
"kind": "translation.worker.run",
|
"kind": "translation.worker.run",
|
||||||
"worker": map[string]any{
|
"worker": map[string]any{
|
||||||
"provider": "mock",
|
"provider": "mock",
|
||||||
"fixture_path": "/tmp/mock-provider.json",
|
"fixture_path": "/tmp/mock-provider.json",
|
||||||
"concurrency": concurrency,
|
"translation_memory_path": "/tmp/translation-memory.sqlite",
|
||||||
"max_attempts": maxAttempts,
|
"concurrency": concurrency,
|
||||||
"lease_seconds": leaseSeconds,
|
"max_attempts": maxAttempts,
|
||||||
"retry_backoff_seconds": retryBackoff,
|
"lease_seconds": leaseSeconds,
|
||||||
"max_tasks": maxTasks,
|
"retry_backoff_seconds": retryBackoff,
|
||||||
"worker_id": "dashboard-worker",
|
"max_tasks": maxTasks,
|
||||||
|
"worker_id": "dashboard-worker",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -521,14 +523,15 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
result, err := client.TranslationWorkerRun(context.Background(), TranslationWorkerRunParams{
|
result, err := client.TranslationWorkerRun(context.Background(), TranslationWorkerRunParams{
|
||||||
Provider: "mock",
|
Provider: "mock",
|
||||||
FixturePath: "/tmp/mock-provider.json",
|
FixturePath: "/tmp/mock-provider.json",
|
||||||
Concurrency: &concurrency,
|
TranslationMemoryPath: "/tmp/translation-memory.sqlite",
|
||||||
MaxAttempts: &maxAttempts,
|
Concurrency: &concurrency,
|
||||||
LeaseSeconds: &leaseSeconds,
|
MaxAttempts: &maxAttempts,
|
||||||
RetryBackoffSeconds: &retryBackoff,
|
LeaseSeconds: &leaseSeconds,
|
||||||
MaxTasks: &maxTasks,
|
RetryBackoffSeconds: &retryBackoff,
|
||||||
WorkerID: "dashboard-worker",
|
MaxTasks: &maxTasks,
|
||||||
|
WorkerID: "dashboard-worker",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("TranslationWorkerRun error: %v", err)
|
t.Fatalf("TranslationWorkerRun error: %v", err)
|
||||||
@@ -536,12 +539,192 @@ func TestTranslationWorkerRunSendsProviderConfig(t *testing.T) {
|
|||||||
if result.TaskID != "task-worker-1" ||
|
if result.TaskID != "task-worker-1" ||
|
||||||
result.Kind != "translation.worker.run" ||
|
result.Kind != "translation.worker.run" ||
|
||||||
result.Worker.Concurrency != concurrency ||
|
result.Worker.Concurrency != concurrency ||
|
||||||
|
result.Worker.TranslationMemoryPath != "/tmp/translation-memory.sqlite" ||
|
||||||
result.Worker.RetryBackoffSeconds != retryBackoff ||
|
result.Worker.RetryBackoffSeconds != retryBackoff ||
|
||||||
result.Worker.MaxTasks == nil || *result.Worker.MaxTasks != maxTasks {
|
result.Worker.MaxTasks == nil || *result.Worker.MaxTasks != maxTasks {
|
||||||
t.Fatalf("unexpected result: %#v", result)
|
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) {
|
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
|
||||||
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
|
||||||
if req.Method != "translation.proofread" {
|
if req.Method != "translation.proofread" {
|
||||||
|
|||||||
Reference in New Issue
Block a user