Content Localization uses raw R2 bucket URL instead of S3 CDN URL in translated posts

AI-generated summary

A bug report by David_Ghost highlights an issue where Discourse’s Content Localization feature, when used with Cloudflare R2 as S3-compatible storage, generates translated posts containing raw R2 bucket URLs instead of the configured s3_cdn_url (custom domain). This causes images to appear broken for users viewing localized content. The reporter details their environment, including enabling s3_use_cdn_url_for_all_uploads, and provides steps to reproduce the issue. As a temporary workaround, David_Ghost shares a theme component that uses JavaScript to replace the incorrect R2 endpoint with the correct custom domain in image sources within cooked content.

Hello,

When using content Localization with Discourse AI translations and Cloudflare R2 as S3-compatible storage, the translated post content stores the raw R2 bucket endpoint URL instead of the configured s3_cdn_url (custom domain). Images render broken for all users viewing translations.

Environment

  • Discourse: latest
  • Cloudflare R2 configured via S3 compatible storage
  • s3_cdn_url: https://files.mydomain.com
  • s3_use_cdn_url_for_all_uploads: enabled
  • Content Localization + Discourse AI translation: enabled

Steps to Reproduce

  1. Configure Cloudflare R2 with a custom domain (s3_cdn_url)
  2. Enable Content Localization and AI translation
  3. Create a post with an image
  4. Wait for the AI translation to generate the localized version
  5. Switch language — images in the translated version are broken

Observed behavior

The translated content contains URLs like:

//files.myendpoint.r2.cloudflarestorage.com/original/1X/ instead of https://files.mydomain.com/original/1X/

Related bug: Chat thumbnails bypass s3_cdn_url & use raw S3 bucket URLs

a temporary fix is to do a remap or use this tc:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer("1.8.0", (api) => {
  const badDomain = "files.myendpoint.r2.cloudflarestorage.com";
  const goodDomain = "files.mydomain.com";

  function fixImages() {
    document.querySelectorAll(`img[src*="${badDomain}"]`).forEach(img => {
      img.src = img.src.replace(badDomain, goodDomain);
    });
    document.querySelectorAll(`img[data-src*="${badDomain}"]`).forEach(img => {
      img.dataset.src = img.dataset.src.replace(badDomain, goodDomain);
    });
    document.querySelectorAll(`img[data-large-src*="${badDomain}"]`).forEach(img => {
      img.dataset.largeSrc = img.dataset.largeSrc.replace(badDomain, goodDomain);
    });
  }

  api.decorateCooked(() => fixImages(), { id: "fix-r2-translated-images" });
  api.onPageChange(() => fixImages());
});