mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
feat(glossary): 实现 Rust Glossary V1
This commit is contained in:
@@ -56,6 +56,9 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/translation/handoff",
|
||||
"/admin/translation/memory/summary",
|
||||
"/admin/translation/memory/query",
|
||||
"/admin/translation/glossary/summary",
|
||||
"/admin/translation/glossary/query",
|
||||
"/admin/translation/glossary/diagnose",
|
||||
"/admin/translation/status",
|
||||
},
|
||||
Controls: []string{
|
||||
@@ -75,6 +78,10 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/control/translation-worker-run",
|
||||
"/admin/control/translation-proofread",
|
||||
"/admin/control/translation-memory-confirm",
|
||||
"/admin/control/translation-glossary-add",
|
||||
"/admin/control/translation-glossary-update",
|
||||
"/admin/control/translation-glossary-approve",
|
||||
"/admin/control/translation-glossary-deprecate",
|
||||
"/admin/control/localized-publish",
|
||||
"/admin/control/localized-rollback",
|
||||
},
|
||||
@@ -124,6 +131,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleAdminTranslationMemoryConfirm(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(action, "translation-glossary-") {
|
||||
s.handleAdminGlossaryControl(w, r, action)
|
||||
return
|
||||
}
|
||||
if action == "localized-publish" {
|
||||
s.handleAdminLocalizedPublish(w, r)
|
||||
return
|
||||
@@ -309,6 +320,201 @@ func (s *Server) handleAdminTranslationMemoryConfirm(w http.ResponseWriter, r *h
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminGlossarySummary(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.(GlossaryBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "glossary_backend_unavailable", "Rust bat Glossary backend is unavailable")
|
||||
return
|
||||
}
|
||||
params := backendrpc.GlossarySummaryParams{
|
||||
GlossaryPath: firstTrimmedQuery(r.URL.Query(), "glossary_path"),
|
||||
}
|
||||
result, err := backend.GlossarySummary(r.Context(), params)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "translation-glossary-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) handleAdminGlossaryQuery(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.(GlossaryBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "glossary_backend_unavailable", "Rust bat Glossary backend is unavailable")
|
||||
return
|
||||
}
|
||||
params := backendrpc.GlossaryQueryParams{
|
||||
GlossaryPath: firstTrimmedQuery(r.URL.Query(), "glossary_path"),
|
||||
SourceText: firstTrimmedQuery(r.URL.Query(), "source_text"),
|
||||
Category: firstTrimmedQuery(r.URL.Query(), "category"),
|
||||
ReviewStatus: firstTrimmedQuery(r.URL.Query(), "review_status"),
|
||||
}
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||
limit, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || limit == 0 || limit > 1000 {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_query", "limit must be in 1..=1000")
|
||||
return
|
||||
}
|
||||
params.Limit = &limit
|
||||
}
|
||||
result, err := backend.GlossaryQuery(r.Context(), params)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "translation-glossary-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) handleAdminGlossaryDiagnose(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.(GlossaryBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "glossary_backend_unavailable", "Rust bat Glossary backend is unavailable")
|
||||
return
|
||||
}
|
||||
params := backendrpc.GlossaryDiagnoseParams{
|
||||
GlossaryPath: firstTrimmedQuery(r.URL.Query(), "glossary_path"),
|
||||
SourceText: firstTrimmedQuery(r.URL.Query(), "source_text"),
|
||||
}
|
||||
if params.SourceText == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_query", "source_text is required")
|
||||
return
|
||||
}
|
||||
if raw := firstTrimmedQuery(r.URL.Query(), "context", "source_context"); raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), ¶ms.Context); err != nil || params.Context == nil {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_query", "context must be a JSON object with string values")
|
||||
return
|
||||
}
|
||||
}
|
||||
result, err := backend.GlossaryDiagnose(r.Context(), params)
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, "translation-glossary-diagnose", 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) handleAdminGlossaryControl(w http.ResponseWriter, r *http.Request, action string) {
|
||||
backend, ok := s.backend.(GlossaryBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "glossary_backend_unavailable", "Rust bat Glossary backend is unavailable")
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case "translation-glossary-add", "translation-glossary-update":
|
||||
var params backendrpc.GlossaryTermMutationParams
|
||||
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(params.TermID) == "" || strings.TrimSpace(params.SourceTerm) == "" ||
|
||||
strings.TrimSpace(params.RecommendedTranslation) == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_params", "term_id, source_term and recommended_translation are required")
|
||||
return
|
||||
}
|
||||
var result *backendrpc.GlossaryMutationReport
|
||||
var err error
|
||||
if action == "translation-glossary-add" {
|
||||
result, err = backend.GlossaryAdd(r.Context(), params)
|
||||
} else {
|
||||
if strings.TrimSpace(params.Reviewer) == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_params", "reviewer is required for update")
|
||||
return
|
||||
}
|
||||
result, err = backend.GlossaryUpdate(r.Context(), params)
|
||||
}
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, action, err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: action,
|
||||
RPCMethod: glossaryControlRPCMethod(action),
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
case "translation-glossary-approve", "translation-glossary-deprecate":
|
||||
var params backendrpc.GlossaryReviewParams
|
||||
if !decodeAdminTranslationJSON(w, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(params.TermID) == "" || strings.TrimSpace(params.Reviewer) == "" {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_glossary_params", "term_id and reviewer are required")
|
||||
return
|
||||
}
|
||||
var result *backendrpc.GlossaryMutationReport
|
||||
var err error
|
||||
if action == "translation-glossary-approve" {
|
||||
result, err = backend.GlossaryApprove(r.Context(), params)
|
||||
} else {
|
||||
result, err = backend.GlossaryDeprecate(r.Context(), params)
|
||||
}
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, action, err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: action,
|
||||
RPCMethod: glossaryControlRPCMethod(action),
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
default:
|
||||
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown glossary control action")
|
||||
}
|
||||
}
|
||||
|
||||
func glossaryControlRPCMethod(action string) string {
|
||||
switch action {
|
||||
case "translation-glossary-add":
|
||||
return "translation.glossary.add"
|
||||
case "translation-glossary-update":
|
||||
return "translation.glossary.update"
|
||||
case "translation-glossary-approve":
|
||||
return "translation.glossary.approve"
|
||||
case "translation-glossary-deprecate":
|
||||
return "translation.glossary.deprecate"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminLocalizedPublish(w http.ResponseWriter, r *http.Request) {
|
||||
backend, ok := s.backend.(LocalizedBackend)
|
||||
if !ok || backend == nil {
|
||||
|
||||
@@ -637,6 +637,11 @@ type controlBackend struct {
|
||||
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
|
||||
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
|
||||
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
|
||||
glossarySummaryParams []backendrpc.GlossarySummaryParams
|
||||
glossaryQueryParams []backendrpc.GlossaryQueryParams
|
||||
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
|
||||
glossaryMutationParams []backendrpc.GlossaryTermMutationParams
|
||||
glossaryReviewParams []backendrpc.GlossaryReviewParams
|
||||
localizedPublishParams []backendrpc.LocalizedPublishParams
|
||||
localizedRollbackParams []backendrpc.LocalizedRollbackParams
|
||||
}
|
||||
@@ -825,6 +830,81 @@ func (b *controlBackend) TranslationMemoryConfirm(ctx context.Context, params ba
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.summary")
|
||||
b.glossarySummaryParams = append(b.glossarySummaryParams, params)
|
||||
schemaVersion := uint64(1)
|
||||
return &backendrpc.GlossarySummaryReport{
|
||||
Available: true,
|
||||
Path: params.GlossaryPath,
|
||||
SchemaVersion: &schemaVersion,
|
||||
Summary: &backendrpc.GlossarySummary{
|
||||
SchemaVersion: schemaVersion,
|
||||
TermCount: 2,
|
||||
ApprovedCount: 1,
|
||||
DraftCount: 1,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.query")
|
||||
b.glossaryQueryParams = append(b.glossaryQueryParams, params)
|
||||
return &backendrpc.GlossaryQueryReport{
|
||||
Available: true,
|
||||
Path: params.GlossaryPath,
|
||||
Terms: []backendrpc.GlossaryTerm{{
|
||||
TermID: "term-sensei",
|
||||
GlossaryTermSnapshot: backendrpc.GlossaryTermSnapshot{
|
||||
SourceTerm: "Sensei",
|
||||
RecommendedTranslation: "老师",
|
||||
Priority: 10,
|
||||
},
|
||||
ReviewStatus: backendrpc.GlossaryStatusApproved,
|
||||
Source: backendrpc.GlossarySourceRecord{
|
||||
SourceKind: "manual",
|
||||
ObservedUnixSeconds: 100,
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.diagnose")
|
||||
b.glossaryDiagnoseParams = append(b.glossaryDiagnoseParams, params)
|
||||
return &backendrpc.GlossaryDiagnoseReport{
|
||||
Available: true,
|
||||
Path: params.GlossaryPath,
|
||||
SourceText: params.SourceText,
|
||||
Context: params.Context,
|
||||
Evaluation: json.RawMessage(`{"constraints":[],"diagnostics":[],"blocked":false}`),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.add")
|
||||
b.glossaryMutationParams = append(b.glossaryMutationParams, params)
|
||||
return &backendrpc.GlossaryMutationReport{Available: true, Path: params.GlossaryPath}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.update")
|
||||
b.glossaryMutationParams = append(b.glossaryMutationParams, params)
|
||||
return &backendrpc.GlossaryMutationReport{Available: true, Path: params.GlossaryPath}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.approve")
|
||||
b.glossaryReviewParams = append(b.glossaryReviewParams, params)
|
||||
return &backendrpc.GlossaryMutationReport{Available: true, Path: params.GlossaryPath}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
b.calls = append(b.calls, "translation.glossary.deprecate")
|
||||
b.glossaryReviewParams = append(b.glossaryReviewParams, params)
|
||||
return &backendrpc.GlossaryMutationReport{Available: true, Path: params.GlossaryPath}, nil
|
||||
}
|
||||
|
||||
func (b *controlBackend) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
b.calls = append(b.calls, "localized.status")
|
||||
return json.RawMessage(`{"localized_release_status":"localized","status_code":"localized.published"}`), nil
|
||||
@@ -1044,6 +1124,48 @@ func TestAdminTranslationQueryEndpointsProxyAuthenticatedRequests(t *testing.T)
|
||||
t.Fatalf("missing TM query source status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/summary?glossary_path=%2Fvar%2Flib%2Fbat%2Fglossary.sqlite", nil)
|
||||
request.Header.Set("Authorization", "Bearer translation-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK ||
|
||||
!strings.Contains(recorder.Body.String(), `"approved_count":1`) ||
|
||||
len(backend.glossarySummaryParams) != 1 ||
|
||||
backend.glossarySummaryParams[0].GlossaryPath != "/var/lib/bat/glossary.sqlite" {
|
||||
t.Fatalf("Glossary summary status=%d body=%s params=%#v", recorder.Code, recorder.Body.String(), backend.glossarySummaryParams)
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/query?source_text=Sensei&limit=20", nil)
|
||||
request.Header.Set("Authorization", "Bearer translation-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK ||
|
||||
len(backend.glossaryQueryParams) != 1 ||
|
||||
backend.glossaryQueryParams[0].SourceText != "Sensei" ||
|
||||
backend.glossaryQueryParams[0].Limit == nil ||
|
||||
*backend.glossaryQueryParams[0].Limit != 20 {
|
||||
t.Fatalf("Glossary query status=%d body=%s params=%#v", recorder.Code, recorder.Body.String(), backend.glossaryQueryParams)
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/diagnose?source_text=Sensei&context=%7B%22destination%22%3A%22Bundle%2Fdialogue.bundle%22%7D", nil)
|
||||
request.Header.Set("Authorization", "Bearer translation-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK ||
|
||||
len(backend.glossaryDiagnoseParams) != 1 ||
|
||||
backend.glossaryDiagnoseParams[0].SourceText != "Sensei" ||
|
||||
backend.glossaryDiagnoseParams[0].Context["destination"] != "Bundle/dialogue.bundle" {
|
||||
t.Fatalf("Glossary diagnose status=%d body=%s params=%#v", recorder.Code, recorder.Body.String(), backend.glossaryDiagnoseParams)
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/diagnose", nil)
|
||||
request.Header.Set("Authorization", "Bearer translation-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("missing Glossary diagnose source status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/admin/translation/tasks?limit=0", nil)
|
||||
request.Header.Set("Authorization", "Bearer translation-token")
|
||||
recorder = httptest.NewRecorder()
|
||||
@@ -1818,6 +1940,10 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
{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 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: "translation glossary add", action: "translation-glossary-add", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.add", call: "translation.glossary.add"},
|
||||
{name: "translation glossary update", action: "translation-glossary-update", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","reviewer":"reviewer","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.update", call: "translation.glossary.update"},
|
||||
{name: "translation glossary approve", action: "translation-glossary-approve", body: `{"term_id":"term-sensei","reviewer":"reviewer","reason":"approved"}`, rpcMethod: "translation.glossary.approve", call: "translation.glossary.approve"},
|
||||
{name: "translation glossary deprecate", action: "translation-glossary-deprecate", body: `{"term_id":"term-sensei","reviewer":"reviewer","reason":"retired"}`, rpcMethod: "translation.glossary.deprecate", call: "translation.glossary.deprecate"},
|
||||
{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"},
|
||||
}
|
||||
@@ -1854,6 +1980,13 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
|
||||
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
|
||||
t.Fatalf("TM confirm params=%#v", backend.translationMemoryConfirmParams)
|
||||
}
|
||||
if len(backend.glossaryMutationParams) != 2 ||
|
||||
backend.glossaryMutationParams[0].TermID != "term-sensei" ||
|
||||
backend.glossaryMutationParams[1].Reviewer != "reviewer" ||
|
||||
len(backend.glossaryReviewParams) != 2 ||
|
||||
backend.glossaryReviewParams[0].TermID != "term-sensei" {
|
||||
t.Fatalf("Glossary params mutation=%#v review=%#v", backend.glossaryMutationParams, backend.glossaryReviewParams)
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/admin/control/translation-task-update", strings.NewReader(`{"task_id":""}`))
|
||||
request.Header.Set("Authorization", "Bearer control-token")
|
||||
|
||||
@@ -198,3 +198,44 @@ func TestTranslationMemoryRustContractMirror(t *testing.T) {
|
||||
t.Fatalf("missing TM summary=%+v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlossaryRustContractMirror(t *testing.T) {
|
||||
raw := readContractFixture(t, "glossary-query.json")
|
||||
if bytes.Contains(raw, []byte("/tmp/")) {
|
||||
t.Fatal("Glossary mirror contains a local temporary path")
|
||||
}
|
||||
var report backendrpc.GlossaryQueryReport
|
||||
if err := json.Unmarshal(raw, &report); err != nil {
|
||||
t.Fatalf("decode Glossary query mirror: %v", err)
|
||||
}
|
||||
if !report.Available || report.Path != "${GLOSSARY_PATH}" ||
|
||||
report.SourceText != "${SOURCE_TEXT}" || len(report.Terms) != 1 {
|
||||
t.Fatalf("Glossary report=%+v", report)
|
||||
}
|
||||
term := report.Terms[0]
|
||||
if term.TermID != "${TERM_ID}" ||
|
||||
term.SourceTerm != "${SOURCE_TERM}" ||
|
||||
term.RecommendedTranslation != "${RECOMMENDED_TRANSLATION}" ||
|
||||
term.ReviewStatus != backendrpc.GlossaryStatusApproved ||
|
||||
term.Source.SourceKind != "manual" ||
|
||||
term.Source.ObservedUnixSeconds != 100 ||
|
||||
len(term.History) != 2 ||
|
||||
term.History[0].ReviewStatus != backendrpc.GlossaryStatusDraft ||
|
||||
term.History[1].Action != "approved" ||
|
||||
term.History[1].ReviewStatus != backendrpc.GlossaryStatusApproved ||
|
||||
term.History[1].Snapshot.Priority != 10 {
|
||||
t.Fatalf("Glossary term=%+v", term)
|
||||
}
|
||||
var missing backendrpc.GlossarySummaryReport
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"available": false,
|
||||
"path": "${GLOSSARY_PATH}",
|
||||
"reason": "database_missing"
|
||||
}`), &missing); err != nil {
|
||||
t.Fatalf("decode missing Glossary summary mirror: %v", err)
|
||||
}
|
||||
if missing.Available || missing.Summary != nil || missing.SchemaVersion != nil ||
|
||||
missing.Reason != "database_missing" {
|
||||
t.Fatalf("missing Glossary summary=%+v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
+130
-1
@@ -461,6 +461,86 @@ paths:
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
description: Rust bat Translation Memory backend is unavailable.
|
||||
/admin/translation/glossary/summary:
|
||||
get:
|
||||
summary: Read Rust-owned Glossary summary
|
||||
parameters:
|
||||
- name: glossary_path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Glossary availability and review-state counts.
|
||||
"401":
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
description: Rust bat Glossary backend is unavailable.
|
||||
/admin/translation/glossary/query:
|
||||
get:
|
||||
summary: Query Rust-owned Glossary terms
|
||||
parameters:
|
||||
- name: source_text
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: category
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: review_status
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [draft, approved, deprecated, rejected]
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
default: 100
|
||||
- name: glossary_path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Glossary terms with source and review history.
|
||||
"400":
|
||||
description: Invalid Glossary query.
|
||||
"401":
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
description: Rust bat Glossary backend is unavailable.
|
||||
/admin/translation/glossary/diagnose:
|
||||
get:
|
||||
summary: Run deterministic Glossary diagnostics
|
||||
parameters:
|
||||
- name: source_text
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: context
|
||||
in: query
|
||||
description: JSON object whose values are strings.
|
||||
schema:
|
||||
type: string
|
||||
- name: glossary_path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Glossary constraints, diagnostics, and blocked decision.
|
||||
"400":
|
||||
description: Missing source text or invalid context.
|
||||
"401":
|
||||
description: Missing or invalid admin token.
|
||||
"503":
|
||||
description: Rust bat Glossary backend is unavailable.
|
||||
/admin/translation/status:
|
||||
get:
|
||||
summary: Read Rust-owned localized release status
|
||||
@@ -480,7 +560,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
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, translation-memory-confirm, 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, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, localized-publish, localized-rollback]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -573,8 +653,57 @@ paths:
|
||||
type: string
|
||||
translation_memory_path:
|
||||
type: string
|
||||
glossary_path:
|
||||
type: string
|
||||
record_id:
|
||||
type: string
|
||||
term_id:
|
||||
type: string
|
||||
source_term:
|
||||
type: string
|
||||
aliases:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
recommended_translation:
|
||||
type: string
|
||||
allowed_translations:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
source_language:
|
||||
type: string
|
||||
target_language:
|
||||
type: string
|
||||
category:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
format: int64
|
||||
scope:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
review_status:
|
||||
type: string
|
||||
enum: [draft, approved, deprecated, rejected]
|
||||
source:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [source_kind, observed_unix_seconds]
|
||||
properties:
|
||||
source_kind:
|
||||
type: string
|
||||
enum: [manual, imported]
|
||||
source_ref:
|
||||
type: string
|
||||
source_author:
|
||||
type: string
|
||||
source_note:
|
||||
type: string
|
||||
observed_unix_seconds:
|
||||
type: integer
|
||||
format: int64
|
||||
reviewer:
|
||||
type: string
|
||||
reason:
|
||||
|
||||
@@ -92,6 +92,18 @@ type TranslationMemoryBackend interface {
|
||||
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
|
||||
}
|
||||
|
||||
// GlossaryBackend exposes Rust-owned Glossary management and diagnostics.
|
||||
// Go forwards these typed calls and never opens the Glossary database.
|
||||
type GlossaryBackend interface {
|
||||
GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error)
|
||||
GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error)
|
||||
GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error)
|
||||
GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error)
|
||||
GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error)
|
||||
GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error)
|
||||
GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error)
|
||||
}
|
||||
|
||||
// LocalizedBackend exposes localized release status and the explicit
|
||||
// publish/rollback controls used by the authenticated dashboard.
|
||||
type LocalizedBackend interface {
|
||||
@@ -203,6 +215,28 @@ func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendr
|
||||
return r.Client.TranslationMemoryConfirm(ctx, params)
|
||||
}
|
||||
|
||||
func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
|
||||
return r.Client.GlossarySummary(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error) {
|
||||
return r.Client.GlossaryQuery(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error) {
|
||||
return r.Client.GlossaryDiagnose(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
return r.Client.GlossaryAdd(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
return r.Client.GlossaryUpdate(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
return r.Client.GlossaryApprove(ctx, params)
|
||||
}
|
||||
func (r RPCClient) GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
|
||||
return r.Client.GlossaryDeprecate(ctx, params)
|
||||
}
|
||||
|
||||
func (r RPCClient) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
|
||||
return r.Client.LocalizedStatus(ctx)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,9 @@ func (s *Server) Handler() http.Handler {
|
||||
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/glossary/summary", s.handleAdminGlossarySummary)
|
||||
mux.HandleFunc("/admin/translation/glossary/query", s.handleAdminGlossaryQuery)
|
||||
mux.HandleFunc("/admin/translation/glossary/diagnose", s.handleAdminGlossaryDiagnose)
|
||||
mux.HandleFunc("/admin/translation/status", s.handleAdminLocalizedStatus)
|
||||
mux.HandleFunc("/admin/control/", s.handleAdminControl)
|
||||
mux.HandleFunc("/admin/", s.handleAdminIndex)
|
||||
@@ -155,6 +158,9 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
"/admin/translation/handoff",
|
||||
"/admin/translation/memory/summary",
|
||||
"/admin/translation/memory/query",
|
||||
"/admin/translation/glossary/summary",
|
||||
"/admin/translation/glossary/query",
|
||||
"/admin/translation/glossary/diagnose",
|
||||
"/admin/translation/status",
|
||||
"/admin/control/{action}",
|
||||
},
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
|
||||
- `resource.manifest` 第一页分页响应。
|
||||
- 对应 release 的 `official-sync-snapshot.json`。
|
||||
- `launcher_metadata` 与 `game_main_config_bootstrap` 的 Go mirror 解码。
|
||||
- Rust Glossary V1 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
|
||||
|
||||
这些 fixture 只用于 schema / mirror 回归,不代表真实资源版本,也不替代 live
|
||||
daemon socket 或完整发布切换验证。
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"available": true,
|
||||
"path": "${GLOSSARY_PATH}",
|
||||
"source_text": "${SOURCE_TEXT}",
|
||||
"terms": [
|
||||
{
|
||||
"term_id": "${TERM_ID}",
|
||||
"source_term": "${SOURCE_TERM}",
|
||||
"aliases": [
|
||||
"${ALIAS}"
|
||||
],
|
||||
"recommended_translation": "${RECOMMENDED_TRANSLATION}",
|
||||
"allowed_translations": [
|
||||
"${ALLOWED_TRANSLATION}"
|
||||
],
|
||||
"source_language": "en",
|
||||
"target_language": "zh-Hans",
|
||||
"category": "character",
|
||||
"priority": 10,
|
||||
"scope": {
|
||||
"destination": "${DESTINATION}"
|
||||
},
|
||||
"review_status": "approved",
|
||||
"source": {
|
||||
"source_kind": "manual",
|
||||
"source_ref": "${SOURCE_REF}",
|
||||
"source_author": "${SOURCE_AUTHOR}",
|
||||
"source_note": "${SOURCE_NOTE}",
|
||||
"observed_unix_seconds": 100
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
"history_id": "${HISTORY_CREATED_ID}",
|
||||
"action": "created",
|
||||
"source": {
|
||||
"source_kind": "manual",
|
||||
"source_ref": "${SOURCE_REF}",
|
||||
"source_author": "${SOURCE_AUTHOR}",
|
||||
"source_note": "${SOURCE_NOTE}",
|
||||
"observed_unix_seconds": 100
|
||||
},
|
||||
"review_status": "draft",
|
||||
"snapshot": {
|
||||
"source_term": "${SOURCE_TERM}",
|
||||
"aliases": [
|
||||
"${ALIAS}"
|
||||
],
|
||||
"recommended_translation": "${RECOMMENDED_TRANSLATION}",
|
||||
"allowed_translations": [
|
||||
"${ALLOWED_TRANSLATION}"
|
||||
],
|
||||
"source_language": "en",
|
||||
"target_language": "zh-Hans",
|
||||
"category": "character",
|
||||
"priority": 10,
|
||||
"scope": {
|
||||
"destination": "${DESTINATION}"
|
||||
}
|
||||
},
|
||||
"observed_unix_seconds": 100
|
||||
},
|
||||
{
|
||||
"history_id": "${HISTORY_APPROVED_ID}",
|
||||
"action": "approved",
|
||||
"reviewer": "${REVIEWER}",
|
||||
"reason": "reviewed",
|
||||
"source": {
|
||||
"source_kind": "manual",
|
||||
"source_ref": "${SOURCE_REF}",
|
||||
"source_author": "${SOURCE_AUTHOR}",
|
||||
"source_note": "${SOURCE_NOTE}",
|
||||
"observed_unix_seconds": 100
|
||||
},
|
||||
"review_status": "approved",
|
||||
"snapshot": {
|
||||
"source_term": "${SOURCE_TERM}",
|
||||
"aliases": [
|
||||
"${ALIAS}"
|
||||
],
|
||||
"recommended_translation": "${RECOMMENDED_TRANSLATION}",
|
||||
"allowed_translations": [
|
||||
"${ALLOWED_TRANSLATION}"
|
||||
],
|
||||
"source_language": "en",
|
||||
"target_language": "zh-Hans",
|
||||
"category": "character",
|
||||
"priority": 10,
|
||||
"scope": {
|
||||
"destination": "${DESTINATION}"
|
||||
}
|
||||
},
|
||||
"observed_unix_seconds": 101
|
||||
}
|
||||
],
|
||||
"created_unix_seconds": 100,
|
||||
"updated_unix_seconds": 101
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user