feat(glossary): 实现 Rust Glossary V1

This commit is contained in:
2026-09-07 22:38:53 +08:00
parent 8fc93b8f39
commit 94483ff14d
42 changed files with 4543 additions and 92 deletions
+206
View File
@@ -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), &params.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, &params) {
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, &params) {
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 {
+133
View File
@@ -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")
+41
View File
@@ -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
View File
@@ -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:
+34
View File
@@ -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)
}
+6
View File
@@ -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
View File
@@ -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 或完整发布切换验证。
+99
View File
@@ -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
}
]
}