Skip to content

Fix markdown links being destroyed by machine translation (#2299)#5033

Draft
aseckin wants to merge 1 commit into
mainfrom
fix/2299-translation-breaks-markdown-links
Draft

Fix markdown links being destroyed by machine translation (#2299)#5033
aseckin wants to merge 1 commit into
mainfrom
fix/2299-translation-breaks-markdown-links

Conversation

@aseckin

@aseckin aseckin commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fix markdown links being destroyed by machine translation

Fixes #2299

Problem

Links in translated content render as underlined text that isn't clickable.

agoogle_translate_text passed mimeType as an HTTP header instead of a JSON body field. Google Cloud Translation v3 reads it from the body and defaults to text/html — so all of our content has always been translated as HTML, even though it's markdown.

In HTML mode an inline tag splits the markdown into separate text nodes, and the service drops the ones without prose in them. Confirmed against question 35557:

Spanish original un [<u>candidato ultranacionalista…</u>](https://ro.wikipedia.org/…)
Stored English an <u>ultranationalist candidate…</u> , won

The [ and ](url) are gone; the orphaned <u> is why the text still looks like a link. The existing comment in that function ("it translates text as if it was HTML") had spotted the symptom — the \n<br> hack and html.unescape() were built to cope with it. The same function was the cause.

Scope (the issue's two open questions)

Every language pair, not just Spanish-first. agoogle_translate_text is the single chokepoint; the source language is irrelevant. English-origin questions are broken too, just differently — [<u>significant amount of controversy</u>](url) becomes [ <u>gran cantidad de controversia</u> ] (url) in Spanish, where the injected spaces break the syntax just as fatally. English→Chinese happens to survive. es→en is simply the most visible pair to a mostly-English audience.

Every translated markdown field, not just Question Info — Question, GroupOfQuestions, Post, Notebook, Comment, KeyFactorDriver, Project.

Worth knowing: plain [text](url) links are fine and survive translation. Breakage requires an HTML tag inside the link label, which is what makes a targeted backfill viable instead of re-translating everything.

Changes

  • utils/translation.py — send mimeType: "text/plain" in the payload so markdown is treated as opaque text. The <br> hack goes with it: instead of round-tripping newlines through a tag, split the text on newlines and send the lines via the contents array (still one request — the API declares contents as a list). Line structure is preserved by construction rather than depending on how the service handles whitespace. html.unescape() stays — it's a no-op when the response is already unescaped, and removing it is an unverified risk.
  • misc/management/commands/retranslate_html_links.py — disposable backfill. Existing rows stay broken otherwise, since content_last_md5 still matches their unchanged original. Nulls the hash so queryset_filter_outdated_translations picks them up. Reports counts by default, only writes with --apply. Skips manually-translated content and content with no detected language (never translated, so nothing to repair).
  • tests/unit/test_utils/test_translation.py — first tests for this module.

No frontend changes, no migration. The renderer is fine — it was handed text that no longer contained a link.

Testing

786 unit tests pass, including 16 new ones. Also ran the real pipeline against the actual question 35557 row with a stubbed HTTP layer: the link now reaches the API in one piece and survives reassembly, mimeType is in the body, no <br> injection, paragraphs intact.

Backfill dry run reports 850 affected objects (261 notebooks, 309 questions, 58 groups, 222 comments). Spot-checked the regex — every sample was a genuine [<u>…</u>](url), no false positives.

Before merging

Plain-text mode still needs one check against the live API on staging — no GOOGLE_TRANSLATE_SERVICE_ACCOUNT_KEY available locally. The segmented contents design removes the newline-preservation risk, but staging is where it first meets the real service. Re-translate 35557 and confirm description_en comes back with the link intact.

agoogle_translate_text passed mimeType as an HTTP header rather than a
JSON body field. Google Cloud Translation v3 reads it from the body and
defaults to "text/html", so all content has been translated as HTML.

Our content is markdown. In HTML mode an inline tag splits it into
separate text nodes, and the service drops the ones without prose. A link
written as [<u>label</u>](url) loses the "[" and "](url)" around the tag
entirely (es -> en), or gains spaces that break the syntax (en -> es),
leaving underlined text that is no longer clickable. This affects every
language pair where source != target, and every translated markdown
field, not just question descriptions.

Send mimeType in the payload so markdown is treated as opaque text. The
\n -> <br> hack goes with it: instead of round-tripping newlines through
a tag, split the text on newlines and send the lines via the contents
array (one request, as the API declares contents as a list). Line
structure is then preserved by construction rather than relying on how
the service handles whitespace.

html.unescape stays. It is a no-op when the response is already
unescaped, and removing it is an unverified risk.

Existing rows stay broken until re-translated, so add a disposable
retranslate_html_links command that nulls content_last_md5 for the
affected originals so queryset_filter_outdated_translations picks them up
again. It reports counts by default and only runs with --apply. It skips
manually-translated content and content with no detected language, which
was never translated and so has nothing to repair.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8261df7e-8771-402c-9fe1-f1d0538e97a2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2299-translation-breaks-markdown-links

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-5033-fix-2299-translation-breaks-ma-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:fix-2299-translation-breaks-markdown-links-6c7f7da
🗄️ PostgreSQL NeonDB branch preview/pr-5033-fix-2299-translation-breaks-ma
Redis Fly Redis mtc-redis-pr-5033-fix-2299-translation-breaks-ma

Details

  • Commit: 07043294d42ca88aa9904872aad715a7cd2d3da0
  • Branch: fix/2299-translation-breaks-markdown-links
  • Fly App: metaculus-pr-5033-fix-2299-translation-breaks-ma

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@aseckin

aseckin commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

will wait until we decide how much value we get from auto-translations

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.

Links aren't clickable in english translated version of some spanish first questions

1 participant