Skip to content

Fix Indonesian newsletters delivering raw JSON and add language preview tabs - #1055

Merged
kevinhermawan merged 1 commit into
mainfrom
feat/newsletter-email-preview-language-tabs
Jul 27, 2026
Merged

Fix Indonesian newsletters delivering raw JSON and add language preview tabs#1055
kevinhermawan merged 1 commit into
mainfrom
feat/newsletter-email-preview-language-tabs

Conversation

@kevinhermawan

Copy link
Copy Markdown
Contributor

Summary

Indonesian subscribers were receiving the raw JSON newsletter document instead of a formatted email. A translated summary point that ran past 100 characters invalidated the entire body, and the template's legacy plain-text fallback then mailed the blob verbatim while delivery still recorded success. This separates format from length, and adds badged per-language Email Preview tabs to the newsletter detail page so the translated rendering is visible in Hermes.

Related issues

Closes #1054

Important changes

  • MAX_POINT_LENGTH no longer invalidates a document. newsletterDocumentSchema now checks format only: version, canonical section keys, non-empty strings, http(s) URLs, points-per-article, articles-per-section. The per-point character cap moves out. A slightly long point is a wording problem, not a corrupt body, and no longer costs a subscriber their newsletter.
  • The translation pass validates what it builds. translateNewsletter now runs the rebuilt document through newsletterDocumentSchema and throws TranslateNewsletterError with the failing paths, so a malformed translation cannot reach the database. It also tags each point in the prompt with (max 100 chars) and tells the model Indonesian runs longer, so length stays steered where the model can act on it. Titles stay untagged since the schema does not cap them.
  • Delivery no longer mails an unparseable translation. A present-but-invalid body previously went straight to the renderer; it now skips the recipient with skipped_invalid_translation and logs a warning. Behaviour for a missing translation is unchanged, and English is still never substituted for an id subscriber.
  • Two generic table-v1 contract features. detailBlockTabSchema gains an optional badge (label plus variant) and visibleWhen, a rule in the existing sectionRule.when language. Both are domain-agnostic, so any manifest can use them. A visibleWhen that fails to parse leaves the tab visible, so a bad expression never silently hides content.
  • Per-language Email Preview tabs. The Delivery Stage panel now carries Email Preview badged en and id. The id tab renders the Indonesian NewsletterTranslation and appears only when one exists, via visibleWhen: "present(emailPreviewHtmlIndonesian)".

Other changes

  • The preview notice no longer blames the legacy content format for what is actually a validation failure. A body that parses as JSON but breaks the schema now lists the failing field paths, e.g. sections.0.articles.0.url: URL must use http or https.
  • renderEmailPreview accepts a language and passes it to the email template, so the id preview renders with Indonesian chrome rather than English.
  • Folded three copies of the contract-to-UI badge variant mapping into map-badge-variant.ts.

Key files to review

  • packages/shared/email-templates/src/newsletter/newsletter-document.ts - the format/length split. MAX_POINT_LENGTH is still exported and still used, just not as a validation rule.
  • apps/mediapulse/agents/content-generation/src/translate-newsletter.ts - prompt budget tags and the pre-persist validation gate.
  • apps/mediapulse/agents/delivery/src/deliver-newsletter.ts - the invalid-translation skip. Worth confirming the skip is the right call versus falling back to English.
  • packages/hermes/domain-contract/src/detail-blocks.ts - badge and visibleWhen on the tab schema.
  • apps/hermes/dashboard/components/detail-blocks/detail-block-tabs.tsx - tab filtering and the active-index clamp when a tab disappears.
  • apps/mediapulse/domain-api/src/resources/newsletters/build-translation-preview.ts - returns null when no translation exists, which is what drives tab visibility.

How to test

  1. pnpm code-quality from the repo root. All 108 tasks pass, Prettier clean.
  2. Point a subscription at Indonesian: set user_ticker.language = 'id' for a ticker with a generated newsletter.
  3. Open that newsletter in the Hermes dashboard under Newsletters. The Delivery Stage panel shows Email Preview en and Email Preview id; the id tab renders a formatted Indonesian email, not a JSON dump.
  4. Open a newsletter that has no Indonesian translation. Only the en tab is present.
  5. Regression on the original bug: store a translation whose point runs past 100 characters. It should validate, preview, and deliver normally.
  6. Break the format instead, e.g. set an article url to javascript:alert(1). The preview should name the failing field, and delivery should skip that recipient with skipped_invalid_translation.

…ew tabs

Point length was a hard document rule, so a translated point over 100
characters invalidated the whole body. Delivery then fell through the
template's legacy plain-text branch and mailed the JSON blob while still
reporting success.

Format is now the contract and length is generation-time guidance: the
schema validates structure only, the translation prompt tags each point
with its budget, translateNewsletter validates what it rebuilds, and
delivery skips a translation that fails validation instead of mailing it.

Adds badged per-language Email Preview tabs to the newsletter detail page,
backed by two generic table-v1 features: tab badges and visibleWhen.

Closes #1054
@kevinhermawan
kevinhermawan merged commit 548a962 into main Jul 27, 2026
24 checks passed
@kevinhermawan
kevinhermawan deleted the feat/newsletter-email-preview-language-tabs branch July 27, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Indonesian newsletters deliver raw JSON when a translated point exceeds the length cap

2 participants