Skip to content

fix(i18n): translated pages render images as broken [Image: url] text instead of actual images #165

@luandro

Description

@luandro

Summary

On Portuguese (/pt/) and Spanish (/es/) translated documentation pages, images render as raw text [Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/...] instead of actual <img> elements. English pages display images correctly.

Example broken page: /pt/docs/understanding-comapeos-core-concepts-and-functions

Symptom

Locale Markdown in file Rendered
English ![](/images/understandingcomapeo_0.png) ✅ image
Portuguese [Image: https://prod-files-secure.s3.us-west-2.amazonaws.com/c1033c29-9030-4781-...] ❌ linked text

The embedded S3 URLs are expired (X-Amz-Expires=3600), so they cannot be recovered from the URLs alone.

Affected files (confirmed)

  • i18n/pt/docusaurus-plugin-content-docs/current/understanding-comapeos-core-concepts-and-functions.md (line 32)
  • i18n/pt/docusaurus-plugin-content-docs/current/troubleshooting/teste-ttulo-da-pgina-no-menu-e-nas-migalhas-de-navegao.md
  • i18n/pt/docusaurus-plugin-content-docs/current/getting-started-essentials/initial-use-and-comapeo-settings.md
  • Additional ES files likely affected (not yet fully enumerated)

Root Cause: Two compounding pipeline failures

Failure 1 — markdownToNotion.ts deliberately degrades images to paragraph text

File: scripts/notion-translate/markdownToNotion.ts lines 340–361, 453–464, 739–748

When notion-translate pushes the translated page to Notion, the image case handler converts every markdown image node into a paragraph block containing literal [Image: url] text:

case "image": {
  // For translations, we'll just convert images to text to avoid Notion API issues
  notionBlocks.push({
    paragraph: {
      rich_text: [{ type: "text", text: { content: `[Image: ${altText || imageUrl}]` } }],
    },
  });
}

For pages translated before image stabilization was added, imageUrl was the raw S3 presigned URL (which expires in 1 hour).
For newer translations, imageUrl is /images/file.png — the correct local path — but it is still stored as paragraph text, not as a Notion image block.

Failure 2 — notion-fetch-all overwrites the correct i18n/ files

saveTranslatedContentToDisk (index.ts line 1222) does write correct ![](/images/...) files to i18n/pt/ immediately after translation. However, when notion-fetch-all or notion-fetch-auto-translation-children subsequently re-fetches all Notion pages, it overwrites those files with content read back from the Notion database — and those Notion PT pages now have [Image: url] paragraph blocks (not image blocks). The custom imageTransformer in notionClient.ts (lines 341–438) only fires for blocks of type === "image", so it never triggers on these paragraphs. processAndReplaceImages also cannot match them (its regex only matches ![](url) syntax). The broken text is written verbatim to disk.

Corruption Sequence

┌─────────────────────────────────────────────────────────────────────────────────┐
│  notion:translate runs                                                          │
│                                                                                 │
│  Notion EN page                                                                 │
│  [image block: s3-url] ──► n2m imageTransformer ──► ![alt](s3-url)             │
│                  │                                                              │
│         processAndReplaceImages                                                 │
│         downloads → static/images/file.png                                     │
│                  │                                                              │
│         markdownContent: ![alt](/images/file.png)                              │
│                  │                                                              │
│         translateText (OpenAI) ──► translatedContent: ![alt](/images/file.png) │
│                  │                                                              │
│     ┌────────────┴────────────────────────────────┐                            │
│     ▼ (A) saveTranslatedContentToDisk             ▼ (B) createNotionPageWithBlocks │
│     i18n/pt/file.md ✅ CORRECT (temporary)        markdownToNotion.ts converts: │
│     ![alt](/images/file.png)                      paragraph: "[Image: /images/file.png]" │
│                                                   pushed to Notion PT page ❌  │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
│  notion-fetch-all runs LATER (overwrites i18n/pt/)                             │
│                                                                                 │
│  Notion PT page                                                                 │
│  [paragraph: "[Image: /images/file.png]"]                                      │
│         │                                                                       │
│         ▼ n2m renders paragraph as plain text                                  │
│         imageTransformer NOT triggered (block type ≠ "image")                  │
│         processAndReplaceImages: no ![](url) pattern found                     │
│         │                                                                       │
│         ▼                                                                       │
│  i18n/pt/file.md ❌  ← OVERWRITES the correct file from step (A)              │
│  [Image: /images/file.png]                                                     │
│  (or expired S3 URL for older translations)                                    │
└─────────────────────────────────────────────────────────────────────────────────┘

Key files involved

File Lines Role in bug
scripts/notion-translate/markdownToNotion.ts 340–361, 453–464, 739–748 Converts image nodes → [Image: url] paragraph text when uploading to Notion
scripts/notion-translate/translateBlocks.ts 159–217 Converts Notion image blocks → callout blocks with 🖼️ icon; never re-converted back to images on fetch
scripts/notion-fetch/imageReplacer.ts 105–289 extractImageMatches regex only matches ![](url)[Image: url] plain text is invisible to it
scripts/notion-translate/index.ts 1054–1069, 1222 Correct files written to disk by saveTranslatedContentToDisk, but overwritten by subsequent fetch runs
scripts/notionClient.ts 341–438 Custom imageTransformer only fires for Notion image blocks; paragraph blocks with [Image: url] text bypass it
docusaurus.config.ts 290 remarkFixImagePaths applies to all docs incl. i18n, but only fixes images//images/ prefix — cannot recover [Image: url] text

Design intent vs. actual behavior

Intent: All locales reference the same static/images/ files. Translated markdown uses ![alt-in-target-language](/images/same-filename.png) — only text changes, not images.

Actual: The Notion round-trip (translate → push to Notion → re-fetch) loses image information, because markdownToNotion.ts converts images to unrecoverable paragraph text.

Proposed next steps

Short-term: Post-process existing broken i18n files

Write a script (scripts/fix-i18n-images.ts) that:

  1. Scans i18n/pt/ and i18n/es/ for files containing [Image: ...] patterns
  2. For each broken file, finds the corresponding English doc in docs/
  3. Extracts image references in order from the English file (![](/images/xxx.png))
  4. Replaces each [Image: ...] occurrence (in ordinal position) with the matching English image reference

This works because static images are shared across all locales — no per-locale image download needed.

Long-term: Fix the pipeline to prevent recurrence

Two options:

  • Option A: Make generateBlocks.ts / notionClient.ts recognize the [Image: /images/file.png] paragraph pattern and convert it back to proper ![](/images/file.png) markdown when fetching translated pages.
  • Option B: Instead of writing image-as-text to Notion, store translated image blocks as Notion callout blocks with a recognizable sentinel (e.g. 🖼️ emoji prefix + local path) and teach notion-fetch to convert these sentinels back to image markdown.
  • Option C: Make notion-fetch-all skip overwriting i18n/ files where image references are already correct, or make saveTranslatedContentToDisk the authoritative source (run it after, not before, the Notion upload).

The translateBlocks.ts callout approach (Option B, partially implemented) is promising but currently the callout is not converted back to an image on fetch — that conversion step is missing.

Acceptance criteria

  • /pt/docs/understanding-comapeos-core-concepts-and-functions displays images correctly
  • No [Image: ...] text in any file under i18n/
  • Running notion-fetch-all after a notion:translate does not re-introduce broken image text
  • Short-term fix script can be run without Notion API access (uses only local files)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions