How to Translate Markdown Files Without Breaking Code, Links, or Front Matter
What to translate in a Markdown or MDX file, what to leave alone, and how to check the result. With a sample file, a free checker, and a 9-run local LLM test.

To translate a Markdown file, translate the prose and leave the machinery alone: front matter keys, code, URLs, link labels, component tags, and footnote labels must come back byte for byte. Then check three things that a careful translation still breaks: links to headings, code comments, and, in Chinese, Japanese, or Korean, the spacing around bold text and number ranges.
We translated a 73-line MDX page into Simplified Chinese nine times with a local model, using three different prompts, and checked every result with a script. The parts people usually worry about survived in all nine runs: code, inline code, URLs, the table, the JSX component, and the task list. Three other things went wrong. The link to a heading broke in all nine runs. The code comments were translated in all six runs that sent the whole file, including three runs where the prompt said not to touch code. And the prompt that hid the syntax behind placeholders, which sounds like the safest option, broke the front matter in all three of its runs.
Quick Answer: What to Translate and What to Leave Alone
| Part of the file | Example from our sample | Translate it? |
|---|---|---|
| Front matter keys | slug:, sidebar_position:, tags: | No |
| Front matter values | title: "Export Your Library" | Only reader-facing ones, such as title and description |
| Imports and exports (MDX) | import Callout from '@site/src/components/Callout'; | No |
| Code blocks | shelfmark export --format epub | No. Decide separately about comments and user-facing strings. |
| Inline code | library.json, --covers | No |
| Link and image targets | (./export-options.md), (https://www.w3.org/TR/epub-33/ "EPUB 3.3") | No, except the optional title in quotes |
| Link text and image alt text | [glossary section], ![Folder layout after an export] | Yes |
| Reference labels and definitions | [project glossary][glossary], [glossary]: ./glossary.md#terms | Keep the label [glossary] and the URL; translate the visible text |
| Headings | ## Keep one glossary for the whole book | Yes, but give them fixed IDs first (see below) |
| JSX components | <Callout type="warning" title="Back up first"> | Keep tag and attribute names; translate reader-facing attribute values such as title |
| Admonition markers | :::tip and ::: | No |
| Footnote labels | [^sync] | No |
| MDX comments | {/* Translator note: ... */} | Optional. Readers never see them. |
Everything else is ordinary prose: paragraphs, list items, table cells, and blockquotes.
What We Tested
We wrote sample-source.mdx, a documentation page for a fictional reading app called Shelfmark. It is written in the MDX style used by Docusaurus and similar documentation sites, with one example of each construct in the table above, plus a two-space hard line break, a bold link, a bold phrase ending in a parenthesis, two number ranges written with en dashes, and an in-page link to the heading "Keep one glossary for the whole book."
Then we asked a local model to translate it into Simplified Chinese in three ways:
- Whole file. Paste the entire file and ask for the translated file back.
- Instructed. The same, plus six rules: keep front matter keys, change nothing inside code, keep URLs and reference definitions, keep imports, tags, and comments, keep footnote labels and admonition markers, and translate headings, link text, alt text, and the
titleattribute. - Protected. Replace code, inline code, URLs, tags, comments, footnote labels, admonition markers, and front matter lines with numbered placeholders such as
⟦12⟧, translate the rest, then put the originals back. Thetitleanddescriptionvalues were left exposed for translation, and so, as it turned out, were the front matter's---lines and the closing quotation marks.
The model was Qwen 3.6 27B (Q4_K_M) in LM Studio, with thinking turned off and temperature 0.3, on an Apple M4 Max with 64 GB of memory. We ran each condition three times, nine runs in total, on October 25, 2026. The script is run_llm_markdown_test.py, and all nine outputs are published unedited; the README lists every file. If you have never set up a local model, our local LLM translation guide covers the settings.
This was a small test of one model, one language pair, and one page. It shows which problems happen and how to catch them. It does not rank models or prompts.
Result: The Syntax Survived; the Links and Decisions Did Not
We checked every output with check_markdown_translation.py and rendered each one with remark and the MDX compiler. The full checker output is in check-results.txt, and results.csv has one row per run.
| Check | Whole file | Instructed | Protected |
|---|---|---|---|
| Compiles as MDX | 3 of 3 | 3 of 3 | 3 of 3 |
| Code, inline code, URLs, table, tags, task list unchanged | 3 of 3 | 3 of 3 | 3 of 3 |
| Bold and italics still render | 3 of 3 | 3 of 3 | 3 of 3 |
| Front matter intact | 3 of 3 | 3 of 3 | 0 of 3 |
| In-page link still points at its heading | 0 of 3 | 0 of 3 | 0 of 3 |
| Code comments left as written | 0 of 3 | 0 of 3 | 3 of 3 |
Callout title translated | 3 of 3 | 3 of 3 | 0 of 3 |
The first rows are the good news. This model returned the Markdown syntax intact when we pasted the whole file; we found nothing to fix in code, URLs, or tables. The last four rows are where the work is, and none of them shows up as an error when you build the site.
1. Translated headings break links to them
The sample links to its glossary section with [glossary section](#keep-one-glossary-for-the-whole-book). Every run translated the heading, as it should, and every run kept the link target, as it should. The result is a link to an ID that no longer exists.
Most Markdown tools build heading IDs from the heading text. Pandoc's documentation describes the identifier as "based on the heading text," and Docusaurus warns that you may want to change or translate a heading "without updating the existing ID to avoid breaking links." Once the heading reads 为整本书保留一个术语表, its generated ID is Chinese, and #keep-one-glossary-for-the-whole-book scrolls nowhere. The page still builds, so nothing tells you.
You cannot fix this by editing the link after translation either. Across nine runs the model wrote the same heading four different ways, so the generated ID would change again with every retranslation. The fix belongs before translation: give each heading that is linked to a fixed ID.
- In Docusaurus MDX, use
## Keep one glossary for the whole book {/* #keep-one-glossary */}. Docusaurus notes that the older{#id}form is "not valid MDX syntax anymore," and itswrite-heading-idscommand adds IDs to every document for you. - In Pandoc and in Markdown processors that follow PHP Markdown Extra, use
{#keep-one-glossary}at the end of the heading.
The ID then stays the same in every language, and the checker treats it as the link target.
2. Code comments get translated, and the visible string does not
The sample has a shell comment and a YAML comment:
library:
name: "My books" # shown in the sidebar
sort: title
All six runs that sent the whole file translated both comments, including the three where the prompt said "Do not change anything inside fenced code blocks." The code itself was untouched in all nine runs. Meanwhile "My books", which the comment says is shown in the sidebar, stayed in English in all nine runs. The model did the opposite of what a localized page needs: it translated the note for developers and left the label readers will see.
Neither choice is wrong in every case. For a tutorial whose readers will copy the code into their own project, translated comments help. For reference documentation whose code must match a repository, they make the page disagree with the source. Make the decision once for the project, and look at string values in configuration examples by hand, because no tool can tell a display label from an identifier. The checker reports comment-only changes as warnings for this reason.
3. The safe-looking method broke the front matter
The protected condition did what it promised: all 32 placeholders came back in all three runs, so code, URLs, and tags were restored perfectly. But our script left two small things outside the placeholders: the --- line that opens the front matter, and the closing quotation mark after the translated title and description. The model dropped all of them in all three runs. The file began like this:
title: "导出您的书库
description: "在更换设备之前,请备份您的笔记和书籍。
slug: export-library
Without the opening ---, a Markdown parser no longer sees front matter. It reads the six YAML lines plus the closing --- as a heading, so the page would open with a large heading containing the YAML. It still compiled as MDX. Putting the --- back is not enough either: the YAML parser rejects the two unclosed quotes.
The lesson is not to avoid placeholders. It is that whatever you leave exposed may come back changed, and that front matter should not go to the model as part of the page at all. Parse it, send only the reader-facing values (title, description) as separate strings, and write the translations back into the original structure. That is a few lines of code and removes the whole class of problems.
Bold Text and Ranges in Chinese, Japanese, and Korean
Two more problems never appeared in our nine runs, but they are worth checking for because they break pages silently, and both have hit BookTranslator's own blog. The blog is written in MDX and machine-translated into 27 languages, and in one scheduled translation run two Korean articles failed our structure check three retries in a row. The causes were the two patterns below.
Bold that ends in punctuation. CommonMark decides whether ** can open or close bold by looking at the characters on either side of it. A closing ** that comes right after punctuation must be followed by a space, punctuation, or the end of the line; this is the right-flanking rule. English always has a space there. Chinese, Japanese, and Korean often do not. So 打开**设置 → 导出(测试版)**并选择 shows literal asterisks, because the bold ends in a full-width parenthesis and is followed directly by 并. The same line without the parenthesis renders bold, and so does the line with a space after the **. A bold link has the same problem, because a link ends in ): 点击**[导出](./export-options.md)**打开 fails, and so does the Korean **[API](./api.md)**는.
Our model happened to put a space on both sides of every bold phrase, so its output rendered correctly. An editor who later removes those spaces to follow Chinese typesetting conventions brings the bug back. You can try all seven variants in render-cases.md.
Number ranges with a tilde. Chinese and Japanese text often writes ranges as 3~5. GitHub Flavored Markdown sites built on remark turn single tildes into strikethrough by default: the remark-gfm option singleTilde is true unless you change it. Two ranges in one paragraph, such as 需要 3~5 分钟,生成 10~20 MB 的文件夹, render with the text between them struck through. Our sample used en dashes, and eight runs kept them while the ninth wrote 3 到 5. Ask for en dashes or words in ranges, or set singleTilde: false in your remark configuration.
The structural checker flags both patterns. To confirm what actually renders, render_check.mjs counts bold, italics, strikethrough, and leftover asterisks in the source and the translation, and tries an MDX compile:
npm install @mdx-js/mdx@3 unified@11 remark-parse@11 remark-gfm@4
node render_check.mjs source.mdx translated.mdx
Fewer bold nodes or more literal asterisks in the translation means some emphasis stopped closing.
A Workflow That Holds Up
-
Fix heading IDs first. Add explicit IDs to every heading that something links to, in the syntax your site generator supports. Do it in the source language, once, and commit it before anything is translated.
-
Take front matter out of the page. Translate only reader-facing values such as
titleanddescription, as separate strings, and write them back into the original YAML. -
Decide the code policy. Comments translated or not; display strings in configuration examples translated or not. Write it down in your translation glossary next to the product names that must not change, such as Shelfmark in our sample.
-
Translate the body. Use a tool that parses Markdown, or send the whole page with explicit rules. In our test, sending the whole page kept the syntax intact; if you use placeholders, protect complete constructs and leave nothing half-exposed.
-
Run the structural check.
python3 check_markdown_translation.py source.mdx translated.mdx --keep ShelfmarkIt compares front matter keys and values, imports, code blocks, inline code, link targets, reference definitions, heading levels, in-page link targets, JSX tags and attributes, MDX comments, admonition markers, footnote labels, table shape, task list boxes, hard line breaks, emphasis that cannot close, tilde ranges, and terms you ask it to keep. It does not judge translation quality.
-
Check the rendering for CJK targets with
render_check.mjs, and add a space after any closing**that follows punctuation. -
Build the site and click the links. Open the page in each language, click every in-page link, and read the configuration examples. A build that succeeds proves only that the file compiles.
For documentation where accuracy matters, the terminology and review steps in our technical translation workflow apply on top of this.
Where BookTranslator Fits
BookTranslator's TXT translator accepts .txt, .md, and .mdx files. According to the product page, it translates them while keeping paragraphs, line breaks, special characters, and text structure, and it splits large files into segments for context-aware translation, so you can upload a whole manual or a book written in Markdown instead of pasting it in pieces.
The product page does not describe special handling for front matter, code blocks, or MDX components, and we did not run this sample through BookTranslator for this article. Whatever tool you use, do steps 1 to 3 before translating and steps 5 to 7 afterwards. A heading without a fixed ID still loses its links, and a range written with tildes still turns into strikethrough. If you are publishing a book in several languages, our guide to self-publishing in multiple languages covers the rest of that job.
Get the Test Files
Everything from this test is free to reuse:
- sample-source.mdx: the 73-line English sample;
- check_markdown_translation.py: the structural checker (Python standard library only);
- render_check.mjs and render-cases.md: the rendering check and the seven CJK test cases;
- run_llm_markdown_test.py: the three prompt conditions for any OpenAI-compatible local server;
- the nine outputs in the outputs folder, with check-results.txt and results.csv.
Saistītie ieraksti





