Files
galaxy-game/backend/internal/diplomail/translator/translator.go
T
Ilia Denisov e22f4b7800
Tests · Go / test (push) Successful in 1m59s
Tests · Go / test (pull_request) Successful in 2m0s
Tests · Integration / integration (pull_request) Successful in 1m35s
diplomail (Stage D): language detection + lazy translation cache
Replaces the LangUndetermined placeholder with whatlanggo-backed
body detection on every send path, then adds a translation cache
keyed on (message_id, target_lang) populated lazily on the
per-message read endpoint. The noop translator that ships with
Stage D returns engine="noop", which the service treats as
"translation unavailable" — wiring a real backend (LibreTranslate
HTTP client is the documented next step) is a one-file swap.

GetMessage and ListInbox now accept a targetLang argument; the HTTP
layer resolves the caller's accounts.preferred_language and
forwards it. Inbox uses the cache only (never calls the
translator) so bulk reads stay fast under future SaaS backends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:16:12 +02:00

60 lines
2.2 KiB
Go

// Package translator wraps the per-language rendering for the
// diplomail subsystem. The package exposes a narrow `Translator`
// interface so the actual translation backend (LibreTranslate, an
// in-process model, a SaaS engine, …) can be swapped without
// touching the rest of the codebase.
//
// Stage D ships a `NoopTranslator` that returns the input unchanged.
// The diplomail Service treats a `Name == NoopEngine` result as
// "translation unavailable" and refrains from writing a cache row;
// the inbox handler then returns the original body with a
// `translated == false` payload. The contract lets the rest of the
// system ship without a translation backend; future stages can wire
// a real `Translator` without code changes elsewhere.
package translator
import "context"
// NoopEngine is the engine identifier returned by `NoopTranslator`.
// The diplomail Service checks for this value to decide whether to
// persist a `diplomail_translations` row.
const NoopEngine = "noop"
// Result carries one translated rendering plus the engine identifier
// that produced it. The engine name is persisted as
// `diplomail_translations.translator` so an operator can see which
// backend produced each row.
type Result struct {
Subject string
Body string
Engine string
}
// Translator is the read-only surface diplomail consumes when it
// needs to render a message for a recipient whose
// `preferred_language` differs from `body_lang`. Implementations
// must be safe for concurrent use; `Translate` may be invoked from
// the async worker on many messages at once.
type Translator interface {
// Translate renders `subject` and `body` from `srcLang` into
// `dstLang`. A nil error with `Result.Engine == NoopEngine`
// signals that no real rendering happened.
Translate(ctx context.Context, srcLang, dstLang, subject, body string) (Result, error)
}
// NewNoop returns a Translator that always returns the input
// unchanged with engine name `NoopEngine`.
func NewNoop() Translator {
return noop{}
}
type noop struct{}
func (noop) Translate(_ context.Context, _, _, subject, body string) (Result, error) {
return Result{
Subject: subject,
Body: body,
Engine: NoopEngine,
}, nil
}