diplomail (Stage E): LibreTranslate client + async translation worker
Synchronous translation on read (Stage D) blocks the HTTP handler on translator I/O. Stage E switches to "send moments-fast, deliver when translated": recipients whose preferred_language differs from the detected body_lang are inserted with available_at=NULL, and an async worker turns them on once a LibreTranslate call materialises the cache row (or fails terminally after 5 retries). Schema delta on diplomail_recipients: available_at, translation_attempts, next_translation_attempt_at, plus a snapshot recipient_preferred_language so the worker queries do not need a join. Read paths (ListInbox, GetMessage, UnreadCount) filter on available_at IS NOT NULL. Push fan-out is moved from Service to the worker so the recipient only sees the toast when the inbox row is actually visible. Translator backend is now a configurable choice: empty BACKEND_DIPLOMAIL_TRANSLATOR_URL → noop (deliver original); populated → LibreTranslate HTTP client. Per-attempt timeout, max attempts, and worker interval all live in DiplomailConfig. The HTTP client itself is unit-tested via httptest (happy path, BCP47 normalisation, unsupported pair, 5xx, identical src/dst, missing URL); worker delivery + fallback paths are covered by the testcontainers-backed e2e tests in diplomail_e2e_test.go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,14 +77,15 @@ type GameSnapshot struct {
|
||||
// ActiveMembership is the slim view of a single (user, game) roster
|
||||
// row the diplomail package needs at send time: it confirms the
|
||||
// participant is active in the game and captures the snapshot fields
|
||||
// (`game_name`, `user_name`, `race_name`) that we persist on each new
|
||||
// message / recipient row.
|
||||
// (`game_name`, `user_name`, `race_name`, `preferred_language`) that
|
||||
// we persist on each new message / recipient row.
|
||||
type ActiveMembership struct {
|
||||
UserID uuid.UUID
|
||||
GameID uuid.UUID
|
||||
GameName string
|
||||
UserName string
|
||||
RaceName string
|
||||
UserID uuid.UUID
|
||||
GameID uuid.UUID
|
||||
GameName string
|
||||
UserName string
|
||||
RaceName string
|
||||
PreferredLanguage string
|
||||
}
|
||||
|
||||
// MembershipLookup is the read-only surface diplomail uses to verify
|
||||
@@ -123,14 +124,17 @@ const (
|
||||
// MemberSnapshot is the slim view of a membership row that survives
|
||||
// all three status values. RaceName is the immutable string captured
|
||||
// at registration time; an empty value is legal for rare cases where
|
||||
// the row was inserted without one.
|
||||
// the row was inserted without one. PreferredLanguage is included so
|
||||
// the broadcast and lifecycle paths can decide whether the recipient
|
||||
// needs to wait for a translation before delivery.
|
||||
type MemberSnapshot struct {
|
||||
UserID uuid.UUID
|
||||
GameID uuid.UUID
|
||||
GameName string
|
||||
UserName string
|
||||
RaceName string
|
||||
Status string
|
||||
UserID uuid.UUID
|
||||
GameID uuid.UUID
|
||||
GameName string
|
||||
UserName string
|
||||
RaceName string
|
||||
PreferredLanguage string
|
||||
Status string
|
||||
}
|
||||
|
||||
// NotificationPublisher is the outbound surface diplomail uses to
|
||||
|
||||
Reference in New Issue
Block a user