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
+196 -3
View File
@@ -267,9 +267,10 @@ type ScheduleRunParams struct {
// TranslationTaskUnitResultParam is the dashboard/manual-review subset of one
// TextUnit result accepted by Rust translation.task.update.
type TranslationTaskUnitResultParam struct {
UnitID string `json:"unit_id"`
SourceText string `json:"source_text"`
TranslatedText string `json:"translated_text"`
UnitID string `json:"unit_id"`
SourceText string `json:"source_text"`
TranslatedText string `json:"translated_text"`
GlossaryOverride *GlossaryOverride `json:"glossary_override,omitempty"`
}
// TranslationTaskUpdateParams is used by translation.task.update to persist
@@ -310,6 +311,7 @@ type TranslationWorkerRunParams struct {
Provider string `json:"provider,omitempty"`
FixturePath string `json:"fixture_path,omitempty"`
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
GlossaryPath string `json:"glossary_path,omitempty"`
Concurrency *uint64 `json:"concurrency,omitempty"`
MaxAttempts *uint64 `json:"max_attempts,omitempty"`
LeaseSeconds *uint64 `json:"lease_seconds,omitempty"`
@@ -323,6 +325,7 @@ type TranslationWorkerConfig struct {
Provider string `json:"provider"`
FixturePath string `json:"fixture_path,omitempty"`
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
GlossaryPath string `json:"glossary_path,omitempty"`
Concurrency uint64 `json:"concurrency"`
MaxAttempts uint64 `json:"max_attempts"`
LeaseSeconds uint64 `json:"lease_seconds"`
@@ -476,6 +479,154 @@ type TranslationMemoryConfirmReport struct {
Entry TranslationMemoryEntry `json:"entry"`
}
// GlossaryReviewStatus is the Rust-owned term review state.
type GlossaryReviewStatus string
const (
GlossaryStatusDraft GlossaryReviewStatus = "draft"
GlossaryStatusApproved GlossaryReviewStatus = "approved"
GlossaryStatusDeprecated GlossaryReviewStatus = "deprecated"
GlossaryStatusRejected GlossaryReviewStatus = "rejected"
)
// GlossarySourceRecord identifies the source/provenance of a term.
type GlossarySourceRecord struct {
SourceKind string `json:"source_kind"`
SourceRef *string `json:"source_ref,omitempty"`
SourceAuthor *string `json:"source_author,omitempty"`
SourceNote *string `json:"source_note,omitempty"`
ObservedUnixSeconds uint64 `json:"observed_unix_seconds"`
}
// GlossaryTermSnapshot is the versioned definition shared by Rust and Go.
type GlossaryTermSnapshot struct {
SourceTerm string `json:"source_term"`
Aliases []string `json:"aliases,omitempty"`
RecommendedTranslation string `json:"recommended_translation"`
AllowedTranslations []string `json:"allowed_translations,omitempty"`
SourceLanguage *string `json:"source_language,omitempty"`
TargetLanguage *string `json:"target_language,omitempty"`
Category *string `json:"category,omitempty"`
Priority int `json:"priority"`
Scope map[string]string `json:"scope,omitempty"`
}
// GlossaryOverride records explicit human approval for a deviation.
type GlossaryOverride struct {
Reviewer string `json:"reviewer"`
Reason string `json:"reason"`
Provenance string `json:"provenance"`
ConfirmedUnixSeconds uint64 `json:"confirmed_unix_seconds"`
}
// GlossaryTerm mirrors a persisted Rust term and its source history.
type GlossaryTerm struct {
TermID string `json:"term_id"`
GlossaryTermSnapshot
ReviewStatus GlossaryReviewStatus `json:"review_status"`
Source GlossarySourceRecord `json:"source"`
History []GlossaryHistoryRecord `json:"history,omitempty"`
CreatedUnixSeconds uint64 `json:"created_unix_seconds"`
UpdatedUnixSeconds uint64 `json:"updated_unix_seconds"`
}
// GlossaryHistoryRecord is one durable term mutation.
type GlossaryHistoryRecord struct {
HistoryID string `json:"history_id"`
Action string `json:"action"`
Reviewer *string `json:"reviewer,omitempty"`
Reason *string `json:"reason,omitempty"`
Source GlossarySourceRecord `json:"source"`
ReviewStatus GlossaryReviewStatus `json:"review_status"`
Snapshot GlossaryTermSnapshot `json:"snapshot"`
ObservedUnixSeconds uint64 `json:"observed_unix_seconds"`
}
// GlossarySummary mirrors translation.glossary.summary.
type GlossarySummary struct {
SchemaVersion uint64 `json:"schema_version"`
TermCount uint64 `json:"term_count"`
ApprovedCount uint64 `json:"approved_count"`
DraftCount uint64 `json:"draft_count"`
DeprecatedCount uint64 `json:"deprecated_count"`
RejectedCount uint64 `json:"rejected_count"`
}
type GlossarySummaryParams struct {
GlossaryPath string `json:"glossary_path,omitempty"`
}
type GlossaryQueryParams struct {
GlossaryPath string `json:"glossary_path,omitempty"`
SourceText string `json:"source_text,omitempty"`
Category string `json:"category,omitempty"`
ReviewStatus string `json:"review_status,omitempty"`
Limit *uint64 `json:"limit,omitempty"`
}
type GlossaryDiagnoseParams struct {
GlossaryPath string `json:"glossary_path,omitempty"`
SourceText string `json:"source_text"`
Context map[string]string `json:"context,omitempty"`
}
type GlossaryTermMutationParams struct {
GlossaryPath string `json:"glossary_path,omitempty"`
TermID string `json:"term_id"`
SourceTerm string `json:"source_term"`
Aliases []string `json:"aliases,omitempty"`
RecommendedTranslation string `json:"recommended_translation"`
AllowedTranslations []string `json:"allowed_translations,omitempty"`
SourceLanguage *string `json:"source_language,omitempty"`
TargetLanguage *string `json:"target_language,omitempty"`
Category *string `json:"category,omitempty"`
Priority int `json:"priority"`
Scope map[string]string `json:"scope,omitempty"`
ReviewStatus string `json:"review_status"`
Source GlossarySourceRecord `json:"source"`
Reviewer string `json:"reviewer,omitempty"`
Reason string `json:"reason,omitempty"`
}
type GlossaryReviewParams struct {
GlossaryPath string `json:"glossary_path,omitempty"`
TermID string `json:"term_id"`
Reviewer string `json:"reviewer"`
Reason string `json:"reason,omitempty"`
}
type GlossarySummaryReport struct {
Available bool `json:"available"`
Path string `json:"path"`
SchemaVersion *uint64 `json:"schema_version,omitempty"`
Summary *GlossarySummary `json:"summary,omitempty"`
Reason string `json:"reason,omitempty"`
}
type GlossaryQueryReport struct {
Available bool `json:"available"`
Path string `json:"path"`
SourceText string `json:"source_text"`
Terms []GlossaryTerm `json:"terms"`
Reason string `json:"reason,omitempty"`
}
type GlossaryMutationReport struct {
Available bool `json:"available"`
Path string `json:"path"`
SchemaVersion *uint64 `json:"schema_version,omitempty"`
Term GlossaryTerm `json:"term"`
}
type GlossaryDiagnoseReport struct {
Available bool `json:"available"`
Path string `json:"path"`
SourceText string `json:"source_text"`
Context map[string]string `json:"context,omitempty"`
Evaluation json.RawMessage `json:"evaluation"`
Reason string `json:"reason,omitempty"`
}
// LocalizedPublishParams selects the source of one localized release
// publication. TranslationFile and FromWorker are mutually exclusive.
type LocalizedPublishParams struct {
@@ -751,6 +902,48 @@ func (c *Client) TranslationMemoryConfirm(ctx context.Context, params Translatio
return &out, err
}
func (c *Client) GlossarySummary(ctx context.Context, params GlossarySummaryParams) (*GlossarySummaryReport, error) {
var out GlossarySummaryReport
_, err := c.Call(ctx, "translation.glossary.summary", params, &out)
return &out, err
}
func (c *Client) GlossaryQuery(ctx context.Context, params GlossaryQueryParams) (*GlossaryQueryReport, error) {
var out GlossaryQueryReport
_, err := c.Call(ctx, "translation.glossary.query", params, &out)
return &out, err
}
func (c *Client) GlossaryDiagnose(ctx context.Context, params GlossaryDiagnoseParams) (*GlossaryDiagnoseReport, error) {
var out GlossaryDiagnoseReport
_, err := c.Call(ctx, "translation.glossary.diagnose", params, &out)
return &out, err
}
func (c *Client) GlossaryAdd(ctx context.Context, params GlossaryTermMutationParams) (*GlossaryMutationReport, error) {
var out GlossaryMutationReport
_, err := c.Call(ctx, "translation.glossary.add", params, &out)
return &out, err
}
func (c *Client) GlossaryUpdate(ctx context.Context, params GlossaryTermMutationParams) (*GlossaryMutationReport, error) {
var out GlossaryMutationReport
_, err := c.Call(ctx, "translation.glossary.update", params, &out)
return &out, err
}
func (c *Client) GlossaryApprove(ctx context.Context, params GlossaryReviewParams) (*GlossaryMutationReport, error) {
var out GlossaryMutationReport
_, err := c.Call(ctx, "translation.glossary.approve", params, &out)
return &out, err
}
func (c *Client) GlossaryDeprecate(ctx context.Context, params GlossaryReviewParams) (*GlossaryMutationReport, error) {
var out GlossaryMutationReport
_, err := c.Call(ctx, "translation.glossary.deprecate", params, &out)
return &out, err
}
func (c *Client) LocalizedPublish(ctx context.Context, params LocalizedPublishParams) (json.RawMessage, error) {
return c.rawData(ctx, "localized.publish", params)
}
+173
View File
@@ -823,6 +823,179 @@ func TestLocalizedRollbackSendsExpectedRelease(t *testing.T) {
}
}
func TestGlossaryTypedContract(t *testing.T) {
limit := uint64(20)
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
switch req.Method {
case "translation.glossary.summary":
var params GlossarySummaryParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode summary params: %v", err)
}
if params.GlossaryPath != "/var/lib/bat/glossary.sqlite" {
t.Fatalf("summary params=%#v", params)
}
return testResponse{Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-glossary-summary",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"schema_version": 1,
"summary": map[string]any{
"schema_version": 1,
"term_count": 2,
"approved_count": 1,
"draft_count": 1,
},
},
}}
case "translation.glossary.query":
var params GlossaryQueryParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode query params: %v", err)
}
if params.SourceText != "Sensei" ||
params.GlossaryPath != "/var/lib/bat/glossary.sqlite" ||
params.Limit == nil || *params.Limit != limit {
t.Fatalf("query params=%#v", params)
}
return testResponse{Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-glossary-query",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"terms": []any{map[string]any{
"term_id": "term-sensei",
"source_term": "Sensei",
"recommended_translation": "老师",
"priority": 10,
"review_status": "approved",
"source": map[string]any{
"source_kind": "manual",
"observed_unix_seconds": 100,
},
}},
},
}}
case "translation.glossary.diagnose":
var params GlossaryDiagnoseParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode diagnose params: %v", err)
}
if params.SourceText != "Sensei" || params.Context["destination"] != "Bundle/dialogue.bundle" {
t.Fatalf("diagnose params=%#v", params)
}
return testResponse{Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-glossary-diagnose",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"source_text": "Sensei",
"evaluation": map[string]any{
"constraints": []any{map[string]any{
"term_id": "term-sensei",
"matched_source": "Sensei",
"recommended_translation": "老师",
}},
"diagnostics": []any{},
"blocked": false,
},
},
}}
case "translation.glossary.add":
var params GlossaryTermMutationParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode add params: %v", err)
}
if params.TermID != "term-sensei" || params.Source.SourceKind != "manual" ||
params.Source.ObservedUnixSeconds != 100 {
t.Fatalf("add params=%#v", params)
}
return testResponse{Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-glossary-add",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"term": map[string]any{
"term_id": "term-sensei",
"source_term": "Sensei",
"recommended_translation": "老师",
"review_status": "draft",
"source": map[string]any{
"source_kind": "manual",
"observed_unix_seconds": 100,
},
},
},
}}
case "translation.glossary.approve":
var params GlossaryReviewParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode approve params: %v", err)
}
if params.TermID != "term-sensei" || params.Reviewer != "reviewer" {
t.Fatalf("approve params=%#v", params)
}
return testResponse{Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-glossary-approve",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"term": map[string]any{"term_id": "term-sensei", "review_status": "approved"},
},
}}
default:
t.Fatalf("unexpected method %q", req.Method)
return testResponse{}
}
})
summary, err := client.GlossarySummary(context.Background(), GlossarySummaryParams{
GlossaryPath: "/var/lib/bat/glossary.sqlite",
})
if err != nil || summary.Summary == nil || summary.Summary.ApprovedCount != 1 {
t.Fatalf("summary=%#v err=%v", summary, err)
}
query, err := client.GlossaryQuery(context.Background(), GlossaryQueryParams{
GlossaryPath: "/var/lib/bat/glossary.sqlite",
SourceText: "Sensei",
Limit: &limit,
})
if err != nil || len(query.Terms) != 1 || query.Terms[0].ReviewStatus != GlossaryStatusApproved {
t.Fatalf("query=%#v err=%v", query, err)
}
diagnose, err := client.GlossaryDiagnose(context.Background(), GlossaryDiagnoseParams{
GlossaryPath: "/var/lib/bat/glossary.sqlite",
SourceText: "Sensei",
Context: TranslationMemoryContext{"destination": "Bundle/dialogue.bundle"},
})
if err != nil || !json.Valid(diagnose.Evaluation) {
t.Fatalf("diagnose=%#v err=%v", diagnose, err)
}
add, err := client.GlossaryAdd(context.Background(), GlossaryTermMutationParams{
GlossaryPath: "/var/lib/bat/glossary.sqlite",
TermID: "term-sensei",
SourceTerm: "Sensei",
RecommendedTranslation: "老师",
ReviewStatus: "draft",
Source: GlossarySourceRecord{
SourceKind: "manual",
ObservedUnixSeconds: 100,
},
})
if err != nil || add.Term.TermID != "term-sensei" {
t.Fatalf("add=%#v err=%v", add, err)
}
approved, err := client.GlossaryApprove(context.Background(), GlossaryReviewParams{
GlossaryPath: "/var/lib/bat/glossary.sqlite",
TermID: "term-sensei",
Reviewer: "reviewer",
})
if err != nil || approved.Term.ReviewStatus != GlossaryStatusApproved {
t.Fatalf("approve=%#v err=%v", approved, err)
}
}
func TestApplicationErrorReturnsAPIError(t *testing.T) {
client := newTestClient(t, func(t *testing.T, req testRequest) testResponse {
if req.Method != "task.status" {