Summary
DiscourseAi::Translation::PostLocalizer (content localization) occasionally
produces truncated translations with no error logged anywhere (Rails log,
Sidekiq log, Sidekiq dead/retry queues). The truncation happens mid-sentence,
silently, and the partial result is saved as if it were a complete, successful
translation.
Observed on: Discourse core + discourse-ai 46bea6613 (2026-07-09), LLM:
Gemini 2.5 Flash via Google provider.
Reproduction evidence
Two real cases on our instance:
- A ~2090-char German post translated to Spanish produced only 50 chars,
cutting off immediately before a quoted word (“aufwendiger”) in the source
text. - A ~1830-char German post translated to English (83 chars) and Slovak
(97 chars) — both cut off at the exact same point in the sentence,
immediately before an emoji shortcode (
) in the source text.
All other locales (fr, it, pt, ru, he) for the same post translated
completely (1600–1950 chars).
Root cause (as far as I can trace it)
In lib/completions/structured_output.rb, StructuredOutput#read_buffered_property
falls back to DiscourseAi::Utils::BestEffortJsonParser.extract_key whenever
@partial_json_tracker.broken? is true. This fallback returns whatever was
parsed before the JSON stream broke — with no exception, no warning, no
indication anywhere that the output is incomplete. In
lib/translation/base_translator.rb#get_translation, this value is used as-is
as the final translation and persisted via PostLocalization#save!.
Both truncation points in our data land immediately before a “special”
character in the source text (a straight quote, an emoji shortcode),
suggesting the JSON streaming tracker becomes confused by certain
characters/sequences in the model’s streamed output and marks the stream
broken? prematurely.
Impact
Content is published with visibly incomplete, sometimes nonsensical
translations, and there’s no operational signal (log, dead job, admin
notification) to catch it. We only discovered it because a community member
spotted mid-sentence garbage in the UI.
Suggested fixes
- Surface a warning/error (Rails.logger + Discourse-error-log) whenever
StructuredOutput#broken? is true and the best-effort fallback is used, so
admins can detect this instead of silently shipping partial content. - Consider validating translated length against a sanity threshold relative
to source length before saving a PostLocalization.
Happy to provide full raw/translated text pairs privately if useful for
debugging the JSON tracker.